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.
- arxo_codegen/__init__.py +5 -0
- arxo_codegen/__main__.py +7 -0
- arxo_codegen/active-outcomes.json +6837 -0
- arxo_codegen/check.py +158 -0
- arxo_codegen/check_active_canaries.py +262 -0
- arxo_codegen/check_api.py +76 -0
- arxo_codegen/check_conformance.py +414 -0
- arxo_codegen/check_matrix.py +131 -0
- arxo_codegen/check_packages.py +281 -0
- arxo_codegen/check_semantic_probes.py +293 -0
- arxo_codegen/check_xlsx.py +320 -0
- arxo_codegen/consumer_runner.py +316 -0
- arxo_codegen/core_support.py +203 -0
- arxo_codegen/coverage.py +341 -0
- arxo_codegen/deliver.py +180 -0
- arxo_codegen/examples/xlsx/pricing.lawir.json +409 -0
- arxo_codegen/examples/xlsx/pricing.xlsx.json +52 -0
- arxo_codegen/excel_plan.py +602 -0
- arxo_codegen/harness/verify/ci/gates/backends/check_backends.py +1248 -0
- arxo_codegen/harness/verify/ci/gates/backends/l1_fixtures.py +463 -0
- arxo_codegen/inert-nodes.json +62 -0
- arxo_codegen/inert.py +91 -0
- arxo_codegen/inputs.py +59 -0
- arxo_codegen/package.py +565 -0
- arxo_codegen/plan.py +4053 -0
- arxo_codegen/recertify_registry.py +100 -0
- arxo_codegen/runtime-cert.json +5259 -0
- arxo_codegen/runtime_cert.py +233 -0
- arxo_codegen/survey.py +272 -0
- arxo_codegen/target_runner.py +101 -0
- arxo_codegen/targets/bsl/cert.bsl +113 -0
- arxo_codegen/targets/bsl/emit.py +356 -0
- arxo_codegen/targets/bsl/json.bsl +243 -0
- arxo_codegen/targets/bsl/runner.bsl +210 -0
- arxo_codegen/targets/bsl/runtime.bsl +858 -0
- arxo_codegen/targets/cs/Cert.cs +90 -0
- arxo_codegen/targets/cs/Runner.cs +154 -0
- arxo_codegen/targets/cs/Runtime.cs +243 -0
- arxo_codegen/targets/cs/emit.py +223 -0
- arxo_codegen/targets/dart/cert.dart +99 -0
- arxo_codegen/targets/dart/emit.py +206 -0
- arxo_codegen/targets/dart/json.dart +166 -0
- arxo_codegen/targets/dart/runner.dart +154 -0
- arxo_codegen/targets/dart/runtime.dart +293 -0
- arxo_codegen/targets/erl/cert.erl +64 -0
- arxo_codegen/targets/erl/emit.py +216 -0
- arxo_codegen/targets/erl/rt.erl +238 -0
- arxo_codegen/targets/erl/runner.erl +130 -0
- arxo_codegen/targets/ex/cert.exs +62 -0
- arxo_codegen/targets/ex/emit.py +204 -0
- arxo_codegen/targets/ex/runner.exs +124 -0
- arxo_codegen/targets/ex/runtime.ex +227 -0
- arxo_codegen/targets/go/apiBase.go +483 -0
- arxo_codegen/targets/go/apicheck.go +97 -0
- arxo_codegen/targets/go/cert.go +218 -0
- arxo_codegen/targets/go/emit.py +1806 -0
- arxo_codegen/targets/go/evaluate.go +1826 -0
- arxo_codegen/targets/go/runner.go +38 -0
- arxo_codegen/targets/go/runtime.go +2612 -0
- arxo_codegen/targets/java/Cert.java +91 -0
- arxo_codegen/targets/java/Json.java +148 -0
- arxo_codegen/targets/java/Runner.java +151 -0
- arxo_codegen/targets/java/Runtime.java +243 -0
- arxo_codegen/targets/java/emit.py +198 -0
- arxo_codegen/targets/kt/Cert.kt +67 -0
- arxo_codegen/targets/kt/Json.kt +103 -0
- arxo_codegen/targets/kt/Runner.kt +126 -0
- arxo_codegen/targets/kt/Runtime.kt +196 -0
- arxo_codegen/targets/kt/emit.py +195 -0
- arxo_codegen/targets/lua/cert.lua +82 -0
- arxo_codegen/targets/lua/emit.py +250 -0
- arxo_codegen/targets/lua/json.lua +133 -0
- arxo_codegen/targets/lua/runner.lua +167 -0
- arxo_codegen/targets/lua/runtime.lua +541 -0
- arxo_codegen/targets/php/cert.php +84 -0
- arxo_codegen/targets/php/emit.py +229 -0
- arxo_codegen/targets/php/json.php +144 -0
- arxo_codegen/targets/php/runner.php +189 -0
- arxo_codegen/targets/php/runtime.php +660 -0
- arxo_codegen/targets/py/api_base.py +334 -0
- arxo_codegen/targets/py/apicheck.py +51 -0
- arxo_codegen/targets/py/cert.py +140 -0
- arxo_codegen/targets/py/emit.py +1140 -0
- arxo_codegen/targets/py/evaluate.py +980 -0
- arxo_codegen/targets/py/runner.py +38 -0
- arxo_codegen/targets/py/runtime.py +1634 -0
- arxo_codegen/targets/rb/cert.rb +73 -0
- arxo_codegen/targets/rb/emit.py +203 -0
- arxo_codegen/targets/rb/runner.rb +130 -0
- arxo_codegen/targets/rb/runtime.rb +255 -0
- arxo_codegen/targets/rs/Cargo.lock +142 -0
- arxo_codegen/targets/rs/Cargo.toml +16 -0
- arxo_codegen/targets/rs/cert.rs +106 -0
- arxo_codegen/targets/rs/emit.py +233 -0
- arxo_codegen/targets/rs/prefetch.rs +1 -0
- arxo_codegen/targets/rs/runner.rs +181 -0
- arxo_codegen/targets/rs/runtime.rs +448 -0
- arxo_codegen/targets/scala/Cert.scala +77 -0
- arxo_codegen/targets/scala/Json.scala +140 -0
- arxo_codegen/targets/scala/Runner.scala +121 -0
- arxo_codegen/targets/scala/Runtime.scala +269 -0
- arxo_codegen/targets/scala/emit.py +193 -0
- arxo_codegen/targets/swift/cert.swift +105 -0
- arxo_codegen/targets/swift/emit.py +231 -0
- arxo_codegen/targets/swift/json.swift +208 -0
- arxo_codegen/targets/swift/runner.swift +133 -0
- arxo_codegen/targets/swift/runtime.swift +528 -0
- arxo_codegen/targets/ts/apiBase.ts +250 -0
- arxo_codegen/targets/ts/apicheck.ts +37 -0
- arxo_codegen/targets/ts/cert.ts +96 -0
- arxo_codegen/targets/ts/emit.py +1214 -0
- arxo_codegen/targets/ts/evaluate.ts +894 -0
- arxo_codegen/targets/ts/package-lock.json +45 -0
- arxo_codegen/targets/ts/package.json +9 -0
- arxo_codegen/targets/ts/runner.ts +31 -0
- arxo_codegen/targets/ts/runtime.ts +1369 -0
- arxo_codegen/targets/xlsx/README.ru.md +176 -0
- arxo_codegen/targets/xlsx/emit.py +198 -0
- arxo_codegen/temporal.py +59 -0
- arxo_codegen/test_alias_types.py +171 -0
- arxo_codegen/test_api_contract.py +89 -0
- arxo_codegen/test_calendar_policy.py +48 -0
- arxo_codegen/test_check.py +49 -0
- arxo_codegen/test_check_active_canaries.py +96 -0
- arxo_codegen/test_check_conformance.py +132 -0
- arxo_codegen/test_check_semantic_probes.py +85 -0
- arxo_codegen/test_check_xlsx.py +52 -0
- arxo_codegen/test_consumer_runner.py +175 -0
- arxo_codegen/test_core_support.py +80 -0
- arxo_codegen/test_e0212_decision_time.py +63 -0
- arxo_codegen/test_head_aggregate.py +71 -0
- arxo_codegen/test_inert.py +143 -0
- arxo_codegen/test_instant_types.py +113 -0
- arxo_codegen/test_key_conflict_scope.py +92 -0
- arxo_codegen/test_key_conflict_targets.py +17 -0
- arxo_codegen/test_package_layout.py +163 -0
- arxo_codegen/test_plan_language_version.py +44 -0
- arxo_codegen/test_positions_refusal.py +43 -0
- arxo_codegen/test_recertify_registry.py +77 -0
- arxo_codegen/test_ruby_target.py +41 -0
- arxo_codegen/test_runtime_cert.py +83 -0
- arxo_codegen/test_secondary_key_conflict_scope.py +17 -0
- arxo_codegen/test_stage_r.py +108 -0
- arxo_codegen/test_standalone_inputs.py +71 -0
- arxo_codegen/test_support.py +150 -0
- arxo_codegen/test_survey_inputs.py +115 -0
- arxo_codegen-0.4.0.dist-info/LICENSE +202 -0
- arxo_codegen-0.4.0.dist-info/METADATA +1673 -0
- arxo_codegen-0.4.0.dist-info/RECORD +152 -0
- arxo_codegen-0.4.0.dist-info/WHEEL +5 -0
- arxo_codegen-0.4.0.dist-info/entry_points.txt +13 -0
- 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
|
+
}
|