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,448 @@
1
+ #![allow(dead_code)]
2
+ //! Рантайм L0 для Rust. Тот же контракт, что у runtime.py: точная десятичная
3
+ //! арифметика §58 на `num-bigint`, деньги с валютой, названный режим
4
+ //! округления, календарный день, член перечисления. Отказ — `Err(Refused)`,
5
+ //! не panic: panic остаётся крахом.
6
+ use num_bigint::BigInt;
7
+ use num_integer::Integer;
8
+ use num_traits::{One, Signed, ToPrimitive, Zero};
9
+ use std::collections::BTreeSet;
10
+
11
+ #[derive(Clone, Debug)]
12
+ pub enum Val {
13
+ Text(String),
14
+ Dec { u: BigInt, s: usize },
15
+ Money { u: BigInt, s: usize, cur: String },
16
+ /// Календарный день §2.15 в канонической записи ISO YYYY-MM-DD.
17
+ Date(String),
18
+ /// Член перечисления §38 либо ссылка const_ref без объявления.
19
+ Sym(String),
20
+ }
21
+
22
+ /// Названный отказ рантайма: то, что срез не считает, а не сбой.
23
+ #[derive(Debug)]
24
+ pub struct Refused(pub String);
25
+
26
+ pub type R<T> = Result<T, Refused>;
27
+
28
+ fn refuse<T>(msg: impl Into<String>) -> R<T> {
29
+ Err(Refused(msg.into()))
30
+ }
31
+
32
+ fn p10(n: usize) -> BigInt {
33
+ num_traits::pow(BigInt::from(10), n)
34
+ }
35
+
36
+ pub fn dec(lit: &str) -> Val {
37
+ let (i, f) = match lit.split_once('.') {
38
+ Some((i, f)) => (i, f),
39
+ None => (lit, ""),
40
+ };
41
+ let neg = i.starts_with('-');
42
+ let digits = format!("{}{}", if neg { &i[1..] } else { i }, f);
43
+ let mut u: BigInt = digits.parse().expect("десятичный литерал");
44
+ if neg {
45
+ u = -u;
46
+ }
47
+ Val::Dec { u, s: f.len() }
48
+ }
49
+
50
+ pub fn money(lit: &str, cur: &str) -> Val {
51
+ match dec(lit) {
52
+ Val::Dec { u, s } => Val::Money { u, s, cur: cur.to_string() },
53
+ _ => unreachable!(),
54
+ }
55
+ }
56
+
57
+ pub fn date(lit: &str) -> Val {
58
+ Val::Date(lit.to_string())
59
+ }
60
+
61
+ pub fn sym(id: &str) -> Val {
62
+ Val::Sym(id.to_string())
63
+ }
64
+
65
+ pub fn text(t: &str) -> Val {
66
+ Val::Text(t.to_string())
67
+ }
68
+
69
+ // --- виды -------------------------------------------------------------------
70
+
71
+ /// Числовое значение: мантисса, масштаб, валюта.
72
+ fn num(v: &Val) -> Option<(&BigInt, usize, Option<&str>)> {
73
+ match v {
74
+ Val::Dec { u, s } => Some((u, *s, None)),
75
+ Val::Money { u, s, cur } => Some((u, *s, Some(cur))),
76
+ _ => None,
77
+ }
78
+ }
79
+
80
+ /// §48/§58: сложение, вычитание, min/max требуют совпадения видов; у денег — и валюты.
81
+ fn same(a: &Val, b: &Val, op: &str) -> R<()> {
82
+ match (num(a), num(b)) {
83
+ (Some((_, _, ca)), Some((_, _, cb))) if ca.is_some() == cb.is_some() => {
84
+ if ca.is_some() && ca != cb {
85
+ return refuse(format!("§58: разные валюты: {op}"));
86
+ }
87
+ Ok(())
88
+ }
89
+ _ => refuse(format!("§58: виды операндов не совпадают: {op}")),
90
+ }
91
+ }
92
+
93
+ fn pair(a: &Val, b: &Val) -> (BigInt, BigInt, usize) {
94
+ let (au, sa, _) = num(a).expect("число");
95
+ let (bu, sb, _) = num(b).expect("число");
96
+ let s = sa.max(sb);
97
+ (au * p10(s - sa), bu * p10(s - sb), s)
98
+ }
99
+
100
+ fn mk(u: BigInt, s: usize, like: &Val) -> Val {
101
+ match like {
102
+ Val::Money { cur, .. } => Val::Money { u, s, cur: cur.clone() },
103
+ _ => Val::Dec { u, s },
104
+ }
105
+ }
106
+
107
+ /// Целое значение: дробная часть нулевая (защита в глубину под статикой плана).
108
+ fn integral(v: &Val) -> Option<BigInt> {
109
+ match v {
110
+ Val::Dec { u, s } => {
111
+ let (q, r) = u.div_rem(&p10(*s));
112
+ if r.is_zero() {
113
+ Some(q)
114
+ } else {
115
+ None
116
+ }
117
+ }
118
+ _ => None,
119
+ }
120
+ }
121
+
122
+ // --- арифметика §58 ---------------------------------------------------------
123
+
124
+ pub fn add(a: &Val, b: &Val) -> R<Val> {
125
+ same(a, b, "add")?;
126
+ let (x, y, s) = pair(a, b);
127
+ Ok(mk(x + y, s, a))
128
+ }
129
+
130
+ pub fn sub(a: &Val, b: &Val) -> R<Val> {
131
+ same(a, b, "sub")?;
132
+ let (x, y, s) = pair(a, b);
133
+ Ok(mk(x - y, s, a))
134
+ }
135
+
136
+ /// Правило скаляра §49: безразмерное масштабирует размерное; масштаб результата —
137
+ /// сумма масштабов. Два размерных не перемножаются.
138
+ pub fn mul(a: &Val, b: &Val) -> R<Val> {
139
+ let (Some((au, sa, ca)), Some((bu, sb, cb))) = (num(a), num(b)) else {
140
+ return refuse("§58: виды операндов не совпадают: mul");
141
+ };
142
+ if ca.is_some() && cb.is_some() {
143
+ return refuse("§58: произведение двух размерных величин не определено");
144
+ }
145
+ let like = if ca.is_some() { a } else { b };
146
+ Ok(mk(au * bu, sa + sb, like))
147
+ }
148
+
149
+ /// Точное десятичное частное num/den при 10-гладком знаменателе (§18).
150
+ fn exact(num: BigInt, den: BigInt) -> Option<(BigInt, usize)> {
151
+ let g = num.abs().gcd(&den.abs());
152
+ let g = if g.is_zero() { BigInt::one() } else { g };
153
+ let mut num = &num / &g;
154
+ let mut den = &den / &g;
155
+ if den.is_negative() {
156
+ num = -num;
157
+ den = -den;
158
+ }
159
+ let (two, five) = (BigInt::from(2), BigInt::from(5));
160
+ let (mut twos, mut fives) = (0usize, 0usize);
161
+ while (&den % &two).is_zero() {
162
+ den /= &two;
163
+ twos += 1;
164
+ }
165
+ while (&den % &five).is_zero() {
166
+ den /= &five;
167
+ fives += 1;
168
+ }
169
+ if !den.is_one() {
170
+ return None;
171
+ }
172
+ let k = twos.max(fives);
173
+ Some((num * num_traits::pow(two, k - twos) * num_traits::pow(five, k - fives), k))
174
+ }
175
+
176
+ /// Таблица делимых §58 в срезе: Decimal/Decimal, Money/Decimal, Money/Money одной
177
+ /// валюты. Integer/Integer даёт Rational — вне среза, отвергается планом статически.
178
+ pub fn div(a: &Val, b: &Val) -> R<Val> {
179
+ let (Some((au, sa, ca)), Some((bu, sb, cb))) = (num(a), num(b)) else {
180
+ return refuse("§58: деление вне таблицы делимых");
181
+ };
182
+ if bu.is_zero() {
183
+ return refuse("деление на ноль — evaluation error (§58)");
184
+ }
185
+ let like: Option<&str> = match (ca, cb) {
186
+ (Some(x), Some(y)) => {
187
+ if x != y {
188
+ return refuse("CURRENCY_MISMATCH: §58 делит Money на Money только при тождественной валюте");
189
+ }
190
+ None
191
+ }
192
+ (Some(x), None) => Some(x),
193
+ (None, Some(_)) => return refuse("§58: деление вне таблицы делимых"),
194
+ (None, None) => None,
195
+ };
196
+ match exact(au * p10(sb), bu * p10(sa)) {
197
+ Some((u, s)) => Ok(match like {
198
+ Some(cur) => Val::Money { u, s, cur: cur.to_string() },
199
+ None => Val::Dec { u, s },
200
+ }),
201
+ None => {
202
+ if like.is_some() {
203
+ refuse("INEXACT_DIVISION: округление только явной политикой (§50/§18)")
204
+ } else {
205
+ refuse("§58: частное не 10-гладкое — Rational вне среза")
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ /// Евклидов остаток §58: 0 <= r < |b| при любых знаках операндов.
212
+ pub fn modulo(a: &Val, b: &Val) -> R<Val> {
213
+ let (Some(x), Some(y)) = (integral(a), integral(b)) else {
214
+ return refuse("§58: mod определён только для Integer");
215
+ };
216
+ if y.is_zero() {
217
+ return refuse("остаток от деления на ноль — evaluation error (§58)");
218
+ }
219
+ let m = y.abs();
220
+ let r = ((&x % &m) + &m) % &m;
221
+ Ok(Val::Dec { u: r, s: 0 })
222
+ }
223
+
224
+ pub fn min(a: &Val, b: &Val) -> R<Val> {
225
+ same(a, b, "min")?;
226
+ let (x, y, _) = pair(a, b);
227
+ Ok(if x <= y { a.clone() } else { b.clone() })
228
+ }
229
+
230
+ pub fn max(a: &Val, b: &Val) -> R<Val> {
231
+ same(a, b, "max")?;
232
+ let (x, y, _) = pair(a, b);
233
+ Ok(if x >= y { a.clone() } else { b.clone() })
234
+ }
235
+
236
+ pub fn abs(v: &Val) -> R<Val> {
237
+ let Some((u, s, _)) = num(v) else {
238
+ return refuse("§58: abs — не числовой вид");
239
+ };
240
+ Ok(mk(u.abs(), s, v))
241
+ }
242
+
243
+ /// §58 (E-0075): только Integer pow Integer; порядок проверок — знак, предел,
244
+ /// вычисление, диапазон — часть контракта.
245
+ pub fn pow(base: &Val, exp: &Val) -> R<Val> {
246
+ let (Some(x), Some(e)) = (integral(base), integral(exp)) else {
247
+ return refuse("§58: pow определён только для Integer");
248
+ };
249
+ if e.is_negative() {
250
+ return refuse("§58: pow — показатель отрицателен");
251
+ }
252
+ if e > BigInt::from(4096) {
253
+ return refuse("RESOURCE_LIMIT: pow — показатель превышает 4096");
254
+ }
255
+ let r = num_traits::pow(x, e.to_usize().unwrap());
256
+ if r < BigInt::from(i64::MIN) || r > BigInt::from(i64::MAX) {
257
+ return refuse("RESOURCE_LIMIT: pow — результат вне 64-битного диапазона");
258
+ }
259
+ Ok(Val::Dec { u: r, s: 0 })
260
+ }
261
+
262
+ pub fn round(m: &Val, digits: &Val, mode: &str) -> R<Val> {
263
+ if mode != "HALF_UP" {
264
+ return refuse(format!("§50: режим округления не реализован: {mode}"));
265
+ }
266
+ let Some((mu, ms, _)) = num(m) else {
267
+ return refuse("§50: round — не числовой вид");
268
+ };
269
+ let n = match num(digits) {
270
+ Some((du, ds, _)) => (du / p10(ds)).to_usize().unwrap_or(0),
271
+ None => 0,
272
+ };
273
+ if ms <= n {
274
+ return Ok(mk(mu * p10(n - ms), n, m));
275
+ }
276
+ let drop = p10(ms - n);
277
+ let neg = mu.is_negative();
278
+ let a = mu.abs();
279
+ let (q, r) = a.div_rem(&drop);
280
+ let up = if r * 2 >= drop { q + 1 } else { q }; // HALF_UP — от нуля
281
+ Ok(mk(if neg { -up } else { up }, n, m))
282
+ }
283
+
284
+ // --- сравнение и равенство ---------------------------------------------------
285
+
286
+ fn ordered(op: &str, c: std::cmp::Ordering) -> R<bool> {
287
+ use std::cmp::Ordering::*;
288
+ Ok(match op {
289
+ "lt" => c == Less,
290
+ "le" => c != Greater,
291
+ "gt" => c == Greater,
292
+ "ge" => c != Less,
293
+ "eq" => c == Equal,
294
+ "ne" => c != Equal,
295
+ _ => return refuse(format!("сравнение вне среза рантайма: {op}")),
296
+ })
297
+ }
298
+
299
+ pub fn cmp(a: &Val, op: &str, b: &Val) -> R<bool> {
300
+ if let (Val::Sym(x), Val::Sym(y)) = (a, b) {
301
+ return match op {
302
+ "eq" => Ok(x == y),
303
+ "ne" => Ok(x != y),
304
+ _ => refuse("§57: вид enum не упорядочен"),
305
+ };
306
+ }
307
+ if matches!(a, Val::Sym(_)) || matches!(b, Val::Sym(_)) {
308
+ return match op {
309
+ "eq" => Ok(false),
310
+ "ne" => Ok(true),
311
+ _ => refuse("§57: вид enum не упорядочен"),
312
+ };
313
+ }
314
+ if let (Val::Date(x), Val::Date(y)) = (a, b) {
315
+ return ordered(op, x.cmp(y));
316
+ }
317
+ if matches!(a, Val::Date(_)) || matches!(b, Val::Date(_)) {
318
+ return match op {
319
+ "eq" => Ok(false),
320
+ "ne" => Ok(true),
321
+ _ => refuse("§57: Date сравнивается только с Date"),
322
+ };
323
+ }
324
+ if matches!(a, Val::Text(_)) || matches!(b, Val::Text(_)) {
325
+ let same_text = matches!((a, b), (Val::Text(x), Val::Text(y)) if x == y);
326
+ return match op {
327
+ "eq" => Ok(same_text),
328
+ "ne" => Ok(!same_text),
329
+ _ => refuse(format!("порядок на Text вне среза рантайма: {op}")),
330
+ };
331
+ }
332
+ let (Some((_, _, ca)), Some((_, _, cb))) = (num(a), num(b)) else {
333
+ return refuse(format!("сравнение вне среза рантайма: {op}"));
334
+ };
335
+ if ca.is_some() != cb.is_some() {
336
+ return refuse("§49: сравнение денег с безразмерным");
337
+ }
338
+ if ca.is_some() && ca != cb {
339
+ return refuse("§49: разные валюты");
340
+ }
341
+ let (x, y, _) = pair(a, b);
342
+ ordered(op, x.cmp(&y))
343
+ }
344
+
345
+ pub fn eq(a: &Val, b: &Val) -> bool {
346
+ match (a, b) {
347
+ (Val::Sym(x), Val::Sym(y)) => x == y,
348
+ (Val::Sym(_), _) | (_, Val::Sym(_)) => false,
349
+ (Val::Date(x), Val::Date(y)) => x == y,
350
+ (Val::Date(_), _) | (_, Val::Date(_)) => false,
351
+ (Val::Text(x), Val::Text(y)) => x == y,
352
+ (Val::Text(_), _) | (_, Val::Text(_)) => false,
353
+ _ => {
354
+ let (Some((_, _, ca)), Some((_, _, cb))) = (num(a), num(b)) else { return false };
355
+ if ca.is_some() != cb.is_some() || ca != cb {
356
+ return false;
357
+ }
358
+ let (x, y, _) = pair(a, b);
359
+ x == y
360
+ }
361
+ }
362
+ }
363
+
364
+ fn norm(u: &BigInt, s: usize) -> String {
365
+ let mut u = u.clone();
366
+ let mut s = s;
367
+ let ten = BigInt::from(10);
368
+ while s > 0 && (&u % &ten).is_zero() {
369
+ u /= &ten;
370
+ s -= 1;
371
+ }
372
+ format!("{u}e-{s}")
373
+ }
374
+
375
+ /// Разделитель и экран внутри текста экранируются: иначе ["a|e:b","c"] и
376
+ /// ["a","b|e:c"] склеивались в один ключ, и второй факт терялся.
377
+ fn esc(t: &str) -> String {
378
+ t.replace('\\', "\\\\").replace('|', "\\|")
379
+ }
380
+
381
+ pub fn key(row: &[Val]) -> String {
382
+ row.iter()
383
+ .map(|v| match v {
384
+ Val::Text(t) => format!("e:{}", esc(t)),
385
+ Val::Date(d) => format!("t:{d}"),
386
+ Val::Sym(s) => format!("c:{}", esc(s)),
387
+ Val::Money { u, s, cur } => format!("m:{cur}:{}", norm(u, *s)),
388
+ Val::Dec { u, s } => format!("d:{}", norm(u, *s)),
389
+ })
390
+ .collect::<Vec<_>>()
391
+ .join("|")
392
+ }
393
+
394
+ // --- таблицы и правовое время -----------------------------------------------
395
+
396
+ /// Таблицы фактов в порядке объявления и ключи виденных строк (§211: порядок
397
+ /// обхода детерминирован — векторы, не множества).
398
+ pub struct Db {
399
+ pub tables: Vec<(String, Vec<Vec<Val>>)>,
400
+ seen: BTreeSet<String>,
401
+ legal_time: String,
402
+ }
403
+
404
+ impl Db {
405
+ pub fn new(preds: &[&str]) -> Db {
406
+ Db {
407
+ tables: preds.iter().map(|p| (p.to_string(), Vec::new())).collect(),
408
+ seen: BTreeSet::new(),
409
+ legal_time: String::new(),
410
+ }
411
+ }
412
+
413
+ pub fn rows(&self, pred: &str) -> &[Vec<Val>] {
414
+ self.tables
415
+ .iter()
416
+ .find(|(p, _)| p == pred)
417
+ .map(|(_, rows)| rows.as_slice())
418
+ .unwrap_or(&[])
419
+ }
420
+
421
+ pub fn has(&self, pred: &str) -> bool {
422
+ self.tables.iter().any(|(p, _)| p == pred)
423
+ }
424
+
425
+ pub fn add(&mut self, pred: &str, row: Vec<Val>) -> bool {
426
+ let k = format!("{pred}#{}", key(&row));
427
+ if !self.seen.insert(k) {
428
+ return false;
429
+ }
430
+ if let Some((_, rows)) = self.tables.iter_mut().find(|(p, _)| p == pred) {
431
+ rows.push(row);
432
+ true
433
+ } else {
434
+ false
435
+ }
436
+ }
437
+
438
+ pub fn set_legal_time(&mut self, t: &str) {
439
+ self.legal_time = t.to_string();
440
+ }
441
+
442
+ pub fn legal_time(&self) -> R<String> {
443
+ if self.legal_time.is_empty() {
444
+ return refuse("legalTime не подан делом");
445
+ }
446
+ Ok(self.legal_time.clone())
447
+ }
448
+ }
@@ -0,0 +1,77 @@
1
+ // Харнесс сертификации рантайма: исполняет runtime-cert.json и печатает,
2
+ // что получилось. Ожидания сверяют ворота, харнесс их не читает.
3
+ object Cert {
4
+ sealed trait Arg
5
+ final case class S(s: String) extends Arg
6
+ final case class Val(v: V) extends Arg
7
+ final case class Row(r: Vector[V]) extends Arg
8
+
9
+ def lit(x: J): Arg = x match {
10
+ case JStr(s) => S(s)
11
+ case _ if x("dec").str.isDefined => Val(RT.dec(x("dec").text))
12
+ case _ if x("money").str.isDefined => Val(RT.money(x("money").text, x("cur").text))
13
+ case _ if x("text").str.isDefined => Val(Txt(x("text").text))
14
+ case _ if x("date").str.isDefined => Val(RT.date(x("date").text))
15
+ case _ if x("sym").str.isDefined => Val(RT.sym(x("sym").text))
16
+ case _ if x("row") != JNull => Row(x("row").items.map(c => lit(c) match {
17
+ case Val(v) => v
18
+ case other => throw new IllegalStateException(s"ячейка фикстуры: $other")
19
+ }))
20
+ case _ => throw new IllegalStateException(s"литерал фикстуры: $x")
21
+ }
22
+
23
+ // Отображение с ЯВНЫМ масштабом: 2.00 и 2 — разные записи одного значения.
24
+ def digits(u: BigInt, s: Int): String = {
25
+ val body0 = u.abs.toString
26
+ val body = "0" * math.max(0, s + 1 - body0.length) + body0
27
+ val text = if (s == 0) body else body.dropRight(s) + "." + body.takeRight(s)
28
+ (if (u < 0) "-" else "") + text
29
+ }
30
+
31
+ def render(v: V): String = v match {
32
+ case Txt(t) => Json.str(t)
33
+ case Money(u, s, cur) => Json.str(digits(u, s) + " " + cur)
34
+ case Dec(u, s) => Json.str(digits(u, s))
35
+ case LDate(d) => Json.str(d)
36
+ case Sym(id) => Json.str(id)
37
+ }
38
+
39
+ def v(a: Arg): V = a match { case Val(x) => x; case S(s) => Txt(s); case _ => throw new IllegalStateException("значение") }
40
+ def s(a: Arg): String = a match { case S(x) => x; case _ => throw new IllegalStateException("строка") }
41
+ def row(a: Arg): Vector[V] = a match { case Row(r) => r; case _ => throw new IllegalStateException("строка таблицы") }
42
+
43
+ def run(op: String, a: Vector[Arg]): String = op match {
44
+ case "dec" => render(RT.dec(s(a(0))))
45
+ case "money" => render(RT.money(s(a(0)), s(a(1))))
46
+ case "date" => render(RT.date(s(a(0))))
47
+ case "sym" => render(RT.sym(s(a(0))))
48
+ case "mul" => render(RT.mul(v(a(0)), v(a(1))))
49
+ case "add" => render(RT.add(v(a(0)), v(a(1))))
50
+ case "sub" => render(RT.sub(v(a(0)), v(a(1))))
51
+ case "div" => render(RT.div(v(a(0)), v(a(1))))
52
+ case "mod" => render(RT.mod(v(a(0)), v(a(1))))
53
+ case "min" => render(RT.min(v(a(0)), v(a(1))))
54
+ case "max" => render(RT.max(v(a(0)), v(a(1))))
55
+ case "abs" => render(RT.abs(v(a(0))))
56
+ case "pow" => render(RT.pow(v(a(0)), v(a(1))))
57
+ case "round" => render(RT.round(v(a(0)), v(a(1)), s(a(2))))
58
+ case "cmp" => RT.cmp(v(a(0)), s(a(1)), v(a(2))).toString
59
+ case "eq" => RT.eq(v(a(0)), v(a(1))).toString
60
+ case "keyEq" => (RT.key(row(a(0))) == RT.key(row(a(1)))).toString
61
+ case "legalTime" => Json.str(RT.legalTime())
62
+ case "setLegalTime" => RT.setLegalTime(s(a(0))); "null"
63
+ case other => throw new IllegalStateException(s"неизвестная операция фикстуры: $other")
64
+ }
65
+
66
+ def main(argv: Array[String]): Unit = {
67
+ val fixture = Json.read(argv(0))
68
+ val out = fixture("cases").items.map { c =>
69
+ val got =
70
+ try run(c("op").text, c("args").items.map(lit))
71
+ catch { case e: Refused => "{\"refused\":" + Json.str(e.getMessage) + "}" }
72
+ "{\"id\":" + Json.str(c("id").text) + ",\"got\":" + got + "}"
73
+ }
74
+ Json.out.println(out.mkString("[", ",", "]"))
75
+ Json.out.flush()
76
+ }
77
+ }
@@ -0,0 +1,140 @@
1
+ // Разбор JSON для Scala: в стандартной библиотеке его нет. Число хранится
2
+ // исходной записью (JNum), двоичная плавающая точка его не видит. Печать —
3
+ // байтами UTF-8 явно: кодировка System.out берётся из локали, и в контейнере
4
+ // с POSIX-локалью кириллица отказа вышла бы вопросами.
5
+ import java.io.{FileDescriptor, FileOutputStream, PrintStream}
6
+ import java.nio.charset.StandardCharsets
7
+ import java.nio.file.{Files, Paths}
8
+
9
+ sealed trait J {
10
+ def apply(k: String): J = this match {
11
+ case JObj(m) => m.getOrElse(k, JNull)
12
+ case _ => JNull
13
+ }
14
+ def items: Vector[J] = this match {
15
+ case JArr(a) => a
16
+ case _ => Vector.empty
17
+ }
18
+ /** Текст значения: строка как есть, число — его запись, иначе пусто. */
19
+ def text: String = this match {
20
+ case JStr(s) => s
21
+ case JNum(t) => t
22
+ case JBool(b) => b.toString
23
+ case _ => ""
24
+ }
25
+ def str: Option[String] = this match {
26
+ case JStr(s) => Some(s)
27
+ case _ => None
28
+ }
29
+ }
30
+ case object JNull extends J
31
+ final case class JBool(b: Boolean) extends J
32
+ final case class JNum(t: String) extends J
33
+ final case class JStr(s: String) extends J
34
+ final case class JArr(a: Vector[J]) extends J
35
+ final case class JObj(m: Map[String, J]) extends J
36
+
37
+ object Json {
38
+ val out = new PrintStream(new FileOutputStream(FileDescriptor.out), true, "UTF-8")
39
+
40
+ def read(path: String): J = parse(new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8))
41
+
42
+ def parse(s: String): J = {
43
+ var i = 0
44
+ def fail(what: String): Nothing = throw new IllegalArgumentException(s"JSON: $what в позиции $i")
45
+ def ws(): Unit = while (i < s.length && " \t\r\n".indexOf(s.charAt(i)) >= 0) i += 1
46
+ def string(): String = {
47
+ i += 1
48
+ val sb = new StringBuilder
49
+ while (true) {
50
+ if (i >= s.length) fail("строка не закрыта")
51
+ val c = s.charAt(i)
52
+ if (c == '"') { i += 1; return sb.toString }
53
+ if (c == '\\') {
54
+ val e = s.charAt(i + 1)
55
+ i += 2
56
+ e match {
57
+ case '"' | '\\' | '/' => sb.append(e)
58
+ case 'b' => sb.append('\b')
59
+ case 'f' => sb.append('\f')
60
+ case 'n' => sb.append('\n')
61
+ case 'r' => sb.append('\r')
62
+ case 't' => sb.append('\t')
63
+ // Суррогатные пары складываются сами: строка JVM — UTF-16.
64
+ case 'u' => sb.append(Integer.parseInt(s.substring(i, i + 4), 16).toChar); i += 4
65
+ case _ => fail("плохой экран")
66
+ }
67
+ } else { sb.append(c); i += 1 }
68
+ }
69
+ ""
70
+ }
71
+ def value(): J = {
72
+ ws()
73
+ if (i >= s.length) fail("конец входа")
74
+ s.charAt(i) match {
75
+ case '{' =>
76
+ i += 1
77
+ val b = Map.newBuilder[String, J]
78
+ ws()
79
+ if (s.charAt(i) == '}') { i += 1; return JObj(Map.empty) }
80
+ while (true) {
81
+ ws()
82
+ if (s.charAt(i) != '"') fail("ожидался ключ")
83
+ val k = string()
84
+ ws()
85
+ if (s.charAt(i) != ':') fail("ожидалось ':'")
86
+ i += 1
87
+ b += (k -> value())
88
+ ws()
89
+ val d = s.charAt(i)
90
+ i += 1
91
+ if (d == '}') return JObj(b.result())
92
+ if (d != ',') fail("ожидалось ',' или '}'")
93
+ }
94
+ JNull
95
+ case '[' =>
96
+ i += 1
97
+ val b = Vector.newBuilder[J]
98
+ ws()
99
+ if (s.charAt(i) == ']') { i += 1; return JArr(Vector.empty) }
100
+ while (true) {
101
+ b += value()
102
+ ws()
103
+ val d = s.charAt(i)
104
+ i += 1
105
+ if (d == ']') return JArr(b.result())
106
+ if (d != ',') fail("ожидалось ',' или ']'")
107
+ }
108
+ JNull
109
+ case '"' => JStr(string())
110
+ case _ if s.startsWith("true", i) => i += 4; JBool(true)
111
+ case _ if s.startsWith("false", i) => i += 5; JBool(false)
112
+ case _ if s.startsWith("null", i) => i += 4; JNull
113
+ case _ =>
114
+ val start = i
115
+ while (i < s.length && "+-.eE0123456789".indexOf(s.charAt(i)) >= 0) i += 1
116
+ if (start == i) fail("неизвестное значение")
117
+ JNum(s.substring(start, i))
118
+ }
119
+ }
120
+ val v = value()
121
+ ws()
122
+ if (i != s.length) fail("лишнее после значения")
123
+ v
124
+ }
125
+
126
+ /** Строка JSON: символы как есть, экранируются кавычка, экран и управляющие. */
127
+ def str(s: String): String = {
128
+ val sb = new StringBuilder("\"")
129
+ s.foreach {
130
+ case '"' => sb.append("\\\"")
131
+ case '\\' => sb.append("\\\\")
132
+ case '\n' => sb.append("\\n")
133
+ case '\r' => sb.append("\\r")
134
+ case '\t' => sb.append("\\t")
135
+ case c if c < 0x20 => sb.append("\\u%04x".format(c.toInt))
136
+ case c => sb.append(c)
137
+ }
138
+ sb.append('"').toString
139
+ }
140
+ }