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,166 @@
1
+ // Разбор JSON с числами ТЕКСТОМ: `jsonDecode` ядра превращает запись числа в
2
+ // int (64 бита) или double, а длинное целое — в double с потерей цифр.
3
+ // Число здесь — JNum с исходной записью; строки, массивы, объекты, true/false
4
+ // и null — обычные значения Dart.
5
+ library json;
6
+
7
+ import 'dart:convert';
8
+ import 'dart:io';
9
+
10
+ class JNum {
11
+ final String text;
12
+ const JNum(this.text);
13
+ @override
14
+ String toString() => text;
15
+ }
16
+
17
+ class _P {
18
+ final String s;
19
+ int i = 0;
20
+ _P(this.s);
21
+
22
+ Never fail(String what) => throw FormatException('JSON: $what в позиции $i');
23
+
24
+ void ws() {
25
+ while (i < s.length && ' \t\r\n'.contains(s[i])) {
26
+ i++;
27
+ }
28
+ }
29
+
30
+ Object? value() {
31
+ ws();
32
+ if (i >= s.length) fail('конец входа');
33
+ final c = s[i];
34
+ if (c == '{') {
35
+ i++;
36
+ final obj = <String, Object?>{};
37
+ ws();
38
+ if (s[i] == '}') {
39
+ i++;
40
+ return obj;
41
+ }
42
+ while (true) {
43
+ ws();
44
+ if (s[i] != '"') fail('ожидался ключ');
45
+ final k = string();
46
+ ws();
47
+ if (s[i] != ':') fail("ожидалось ':'");
48
+ i++;
49
+ obj[k] = value();
50
+ ws();
51
+ if (s[i] == '}') {
52
+ i++;
53
+ return obj;
54
+ }
55
+ if (s[i] != ',') fail("ожидалось ',' или '}'");
56
+ i++;
57
+ }
58
+ }
59
+ if (c == '[') {
60
+ i++;
61
+ final arr = <Object?>[];
62
+ ws();
63
+ if (s[i] == ']') {
64
+ i++;
65
+ return arr;
66
+ }
67
+ while (true) {
68
+ arr.add(value());
69
+ ws();
70
+ if (s[i] == ']') {
71
+ i++;
72
+ return arr;
73
+ }
74
+ if (s[i] != ',') fail("ожидалось ',' или ']'");
75
+ i++;
76
+ }
77
+ }
78
+ if (c == '"') return string();
79
+ if (s.startsWith('true', i)) {
80
+ i += 4;
81
+ return true;
82
+ }
83
+ if (s.startsWith('false', i)) {
84
+ i += 5;
85
+ return false;
86
+ }
87
+ if (s.startsWith('null', i)) {
88
+ i += 4;
89
+ return null;
90
+ }
91
+ final m = RegExp(r'-?\d+(\.\d+)?([eE][-+]?\d+)?').matchAsPrefix(s, i);
92
+ if (m == null) fail('неизвестное значение');
93
+ i = m.end;
94
+ return JNum(m.group(0)!);
95
+ }
96
+
97
+ String string() {
98
+ i++;
99
+ final out = StringBuffer();
100
+ while (true) {
101
+ if (i >= s.length) fail('строка не закрыта');
102
+ final c = s[i];
103
+ if (c == '"') {
104
+ i++;
105
+ return out.toString();
106
+ }
107
+ if (c == '\\') {
108
+ final e = s[i + 1];
109
+ i += 2;
110
+ switch (e) {
111
+ case '"':
112
+ case '\\':
113
+ case '/':
114
+ out.write(e);
115
+ break;
116
+ case 'b':
117
+ out.write('\b');
118
+ break;
119
+ case 'f':
120
+ out.write('\f');
121
+ break;
122
+ case 'n':
123
+ out.write('\n');
124
+ break;
125
+ case 'r':
126
+ out.write('\r');
127
+ break;
128
+ case 't':
129
+ out.write('\t');
130
+ break;
131
+ case 'u':
132
+ // Суррогатные пары складываются сами: строка Dart — UTF-16.
133
+ out.writeCharCode(int.parse(s.substring(i, i + 4), radix: 16));
134
+ i += 4;
135
+ break;
136
+ default:
137
+ fail('плохой экран');
138
+ }
139
+ } else {
140
+ out.write(c);
141
+ i++;
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ Object? decode(String s) {
148
+ final p = _P(s);
149
+ final v = p.value();
150
+ p.ws();
151
+ if (p.i != s.length) p.fail('лишнее после значения');
152
+ return v;
153
+ }
154
+
155
+ Object? readJson(String path) => decode(File(path).readAsStringSync(encoding: utf8));
156
+
157
+ /// Текст значения JSON: строка как есть, число — его запись.
158
+ String text(Object? v) => v == null ? '' : v.toString();
159
+
160
+ /// Строка JSON печатается байтами UTF-8 явно: кодировка stdout по умолчанию
161
+ /// берётся из системы, и на чужой машине UTF-8 вышел бы latin1.
162
+ void emitLine(String line) {
163
+ stdout.add(utf8.encode('$line\n'));
164
+ }
165
+
166
+ String str(String s) => jsonEncode(s);
@@ -0,0 +1,154 @@
1
+ // Раннер Dart-бэкенда: тот же вход и тот же ответ, что у runner.rb.
2
+ //
3
+ // Протокол выхода общий для всех целей: ответ — JSON и код 0; названный отказ
4
+ // среза — JSON {"refused": текст} и код 3, текст побайтово одинаков между
5
+ // целями; любое другое исключение — крах, который ворота валят. Код выхода
6
+ // ставится через `exitCode`, а не `exit()`: `exit()` не дожидается записи
7
+ // stdout, и отказ терял бы свой JSON.
8
+ import 'dart:io';
9
+
10
+ import 'json.dart' as J;
11
+ import 'model.dart' as M;
12
+ import 'runtime.dart';
13
+
14
+ final List<String> issues = [];
15
+
16
+ Object? field(Object? m, String k) => m is Map<String, Object?> ? m[k] : null;
17
+
18
+ Object arg(Object? a) {
19
+ final kind = field(a, 'kind');
20
+ if (kind == 'entity_ref') return J.text(field(a, 'id'));
21
+ if (kind == 'const_ref') {
22
+ // §54: разыменование предшествует исполнению; член перечисления §38 либо
23
+ // имя без объявления остаётся идентичностью, как в ключе атома оракула.
24
+ final c = M.CONSTS[field(a, 'id')];
25
+ return c == null ? RT.sym(J.text(field(a, 'id'))) : c[1];
26
+ }
27
+ if (kind != 'value') throw Refused('аргумент вне среза раннера: ${J.text(kind)}');
28
+ final t = field(field(a, 'type'), 'name');
29
+ final v = J.text(field(a, 'value'));
30
+ switch (t) {
31
+ case 'urn:law:std#Money':
32
+ return RT.money(v, J.text(field(a, 'currency')));
33
+ case 'urn:law:std#Decimal':
34
+ case 'urn:law:std#Integer':
35
+ return RT.dec(v);
36
+ case 'urn:law:std#Text':
37
+ return v;
38
+ case 'urn:law:std#Date':
39
+ return RT.date(v);
40
+ }
41
+ throw Refused('аргумент вне среза раннера: ${J.text(t)}');
42
+ }
43
+
44
+ // Тип аргумента, если известен: у value — из узла, у const_ref — из объявления
45
+ // константы; entity_ref типа не несёт (как у оракула).
46
+ String? argType(Object? a) {
47
+ if (field(a, 'kind') == 'value') return field(field(a, 'type'), 'name') as String?;
48
+ if (field(a, 'kind') == 'const_ref') return M.CONSTS[field(a, 'id')]?[0] as String?;
49
+ return null;
50
+ }
51
+
52
+ // Совпадение типа с наследованием сущностей вверх от фактического.
53
+ bool conforms(String actual, String expected) {
54
+ final seen = <String>{};
55
+ String? cur = actual;
56
+ while (cur != null && seen.add(cur)) {
57
+ if (cur == expected) return true;
58
+ cur = M.BASES[cur];
59
+ }
60
+ return false;
61
+ }
62
+
63
+ // Арность и типы аргументов утверждения против сигнатуры §189 (E-0046).
64
+ bool wellTyped(Object? lit) {
65
+ final params = M.PARAMS[field(lit, 'predicate')];
66
+ if (params == null) return true;
67
+ final args = (field(lit, 'args') as List?) ?? const [];
68
+ if (args.length != params.length) {
69
+ issues.add('TYPE_ERROR');
70
+ return false;
71
+ }
72
+ for (var i = 0; i < params.length; i++) {
73
+ final actual = argType(args[i]);
74
+ if (actual != null && !conforms(actual, params[i])) {
75
+ issues.add('TYPE_ERROR');
76
+ return false;
77
+ }
78
+ }
79
+ return true;
80
+ }
81
+
82
+ // Ключ атома у оракула §208 включает ТИП значения: несогласованный запрос
83
+ // атома не находит — NEITHER без issue.
84
+ bool queryTyped(Object? lit) {
85
+ final params = M.PARAMS[field(lit, 'predicate')];
86
+ if (params == null) return false;
87
+ final args = (field(lit, 'args') as List?) ?? const [];
88
+ if (args.length != params.length) return false;
89
+ for (var i = 0; i < params.length; i++) {
90
+ final actual = argType(args[i]);
91
+ if (actual != null && !conforms(actual, params[i])) return false;
92
+ }
93
+ return true;
94
+ }
95
+
96
+ bool keyConflictBlocks(String predicate) {
97
+ final tainted = M.keyConflictPredicates();
98
+ final waiting = <String>[...tainted];
99
+ while (waiting.isNotEmpty) {
100
+ final source = waiting.removeLast();
101
+ for (final consumer in M.KEY_READERS[source] ?? const <String>[]) {
102
+ if (tainted.add(consumer)) waiting.add(consumer);
103
+ }
104
+ }
105
+ return tainted.contains(predicate);
106
+ }
107
+
108
+ void run(List<String> argv) {
109
+ final kase = J.readJson(argv[0]);
110
+ final query = J.readJson(argv[1]);
111
+ RT.setLegalTime(J.text(field(field(kase, 'context'), 'legalTime')));
112
+ M.packageFacts();
113
+ for (final a in field(kase, 'assertions') as List) {
114
+ final lit = field(a, 'literal');
115
+ if (field(lit, 'polarity') != 'positive') throw Refused('§113: отрицательный факт вне L0-среза');
116
+ if (!wellTyped(lit)) continue;
117
+ final pred = field(lit, 'predicate') as String;
118
+ if (!M.PARAMS.containsKey(pred)) continue;
119
+ M.register(pred, (field(lit, 'args') as List).map(arg).toList());
120
+ }
121
+ M.saturate();
122
+ for (var i = M.keyConflicts(); i > 0; i--) {
123
+ issues.add('KEY_CONFLICT');
124
+ }
125
+ if (field(query, 'kind') != 'truth') {
126
+ throw Refused('вид запроса вне среза раннера: ${J.text(field(query, 'kind'))}');
127
+ }
128
+ var hit = false;
129
+ final ql = field(query, 'literal');
130
+ if (queryTyped(ql)) {
131
+ final want = (field(ql, 'args') as List).map(arg).toList();
132
+ final rows = M.TABLES[field(ql, 'predicate')] ?? const <List<Object>>[];
133
+ hit = rows.any((r) {
134
+ if (r.length != want.length) return false;
135
+ for (var i = 0; i < r.length; i++) {
136
+ if (!RT.eq(r[i], want[i])) return false;
137
+ }
138
+ return true;
139
+ });
140
+ }
141
+ // Открытый мир: отсутствие строки — это NEITHER, а не FALSE_ONLY.
142
+ J.emitLine('{"queryId":${J.str(J.text(field(query, 'queryId')))},'
143
+ '"truthStatus":${J.str(hit ? 'TRUE_ONLY' : 'NEITHER')},'
144
+ '"evaluationStatus":${J.str(keyConflictBlocks(field(ql, 'predicate') as String) ? 'NON_EXECUTABLE' : 'COMPUTED')},"issues":[${issues.map(J.str).join(',')}]}');
145
+ }
146
+
147
+ void main(List<String> argv) {
148
+ try {
149
+ run(argv);
150
+ } on Refused catch (e) {
151
+ J.emitLine('{"refused":${J.str(e.message)}}');
152
+ exitCode = 3;
153
+ }
154
+ }
@@ -0,0 +1,293 @@
1
+ // Рантайм L0 для Dart. Тот же контракт, что у runtime.rb: точная десятичная
2
+ // арифметика §58 на BigInt ядра, деньги с валютой, названный режим
3
+ // округления, календарный день, член перечисления.
4
+ //
5
+ // Ловушка Dart: `%` у BigInt евклидов, а `~/` усекает к нулю — остаток с
6
+ // усечением даёт только `remainder`.
7
+ library runtime;
8
+
9
+ /// Названный отказ рантайма: то, что срез не считает, а не сбой. Раннер
10
+ /// печатает его как JSON с кодом выхода 3; любое другое исключение — крах.
11
+ class Refused implements Exception {
12
+ final String message;
13
+ Refused(this.message);
14
+ @override
15
+ String toString() => message;
16
+ }
17
+
18
+ class Dec {
19
+ final BigInt u;
20
+ final int s;
21
+ const Dec(this.u, this.s);
22
+ }
23
+
24
+ class Money {
25
+ final BigInt u;
26
+ final int s;
27
+ final String cur;
28
+ const Money(this.u, this.s, this.cur);
29
+ }
30
+
31
+ /// Календарный день §2.15 в канонической записи ISO YYYY-MM-DD: порядок
32
+ /// записи и есть порядок дня. Не String: у ключа атома и у сравнения свой вид.
33
+ class LDate {
34
+ final String v;
35
+ const LDate(this.v);
36
+ }
37
+
38
+ /// Член перечисления §38 либо иная ссылка const_ref без объявления: значение —
39
+ /// stable qualified identity, равенство по ней (§57), порядок не определён.
40
+ class Sym {
41
+ final String id;
42
+ const Sym(this.id);
43
+ }
44
+
45
+ final BigInt _ten = BigInt.from(10);
46
+ BigInt _p10(int k) => _ten.pow(k);
47
+
48
+ class RT {
49
+ static Dec dec(String lit) {
50
+ final dot = lit.indexOf('.');
51
+ final i = dot < 0 ? lit : lit.substring(0, dot);
52
+ final f = dot < 0 ? '' : lit.substring(dot + 1);
53
+ final neg = i.startsWith('-');
54
+ final digits = (neg ? i.substring(1) : i) + f;
55
+ return Dec(BigInt.parse((neg ? '-' : '') + digits), f.length);
56
+ }
57
+
58
+ static Money money(String lit, String cur) {
59
+ final d = dec(lit);
60
+ return Money(d.u, d.s, cur);
61
+ }
62
+
63
+ static LDate date(String lit) => LDate(lit);
64
+ static Sym sym(String id) => Sym(id);
65
+
66
+ static bool _num(Object v) => v is Dec || v is Money;
67
+ static BigInt _u(Object v) => v is Money ? v.u : (v as Dec).u;
68
+ static int _s(Object v) => v is Money ? v.s : (v as Dec).s;
69
+
70
+ // §48/§58: сложение, вычитание, min/max требуют совпадения видов; у денег — и валюты.
71
+ static void _same(Object a, Object b, String op) {
72
+ if (!(_num(a) && _num(b) && (a is Money) == (b is Money))) {
73
+ throw Refused('§58: виды операндов не совпадают: $op');
74
+ }
75
+ if (a is Money && a.cur != (b as Money).cur) throw Refused('§58: разные валюты: $op');
76
+ }
77
+
78
+ static List<Object> _pair(Object a, Object b) {
79
+ final s = _s(a) > _s(b) ? _s(a) : _s(b);
80
+ return [_u(a) * _p10(s - _s(a)), _u(b) * _p10(s - _s(b)), s];
81
+ }
82
+
83
+ static Object _make(BigInt u, int s, Object like) => like is Money ? Money(u, s, like.cur) : Dec(u, s);
84
+
85
+ // Целое значение: дробная часть нулевая (защита в глубину под статикой плана).
86
+ static bool _integral(Object v) => v is Dec && v.u.remainder(_p10(v.s)) == BigInt.zero;
87
+ static BigInt _toInt(Dec v) => v.u ~/ _p10(v.s);
88
+
89
+ static Object add(Object a, Object b) {
90
+ _same(a, b, 'add');
91
+ final p = _pair(a, b);
92
+ return _make((p[0] as BigInt) + (p[1] as BigInt), p[2] as int, a);
93
+ }
94
+
95
+ static Object sub(Object a, Object b) {
96
+ _same(a, b, 'sub');
97
+ final p = _pair(a, b);
98
+ return _make((p[0] as BigInt) - (p[1] as BigInt), p[2] as int, a);
99
+ }
100
+
101
+ // Правило скаляра §49: безразмерное масштабирует размерное; масштаб результата —
102
+ // сумма масштабов. Два размерных не перемножаются.
103
+ static Object mul(Object a, Object b) {
104
+ if (!(_num(a) && _num(b))) throw Refused('§58: виды операндов не совпадают: mul');
105
+ if (a is Money && b is Money) throw Refused('§58: произведение двух размерных величин не определено');
106
+ return _make(_u(a) * _u(b), _s(a) + _s(b), a is Money ? a : b);
107
+ }
108
+
109
+ // Точное десятичное частное num/den при 10-гладком знаменателе (§18), иначе null.
110
+ static List<Object>? _exact(BigInt num, BigInt den) {
111
+ var g = num.gcd(den);
112
+ if (g == BigInt.zero) g = BigInt.one;
113
+ num = num ~/ g;
114
+ den = den ~/ g;
115
+ if (den.isNegative) {
116
+ num = -num;
117
+ den = -den;
118
+ }
119
+ final two = BigInt.two, five = BigInt.from(5);
120
+ var twos = 0, fives = 0;
121
+ while (den.remainder(two) == BigInt.zero) {
122
+ den = den ~/ two;
123
+ twos++;
124
+ }
125
+ while (den.remainder(five) == BigInt.zero) {
126
+ den = den ~/ five;
127
+ fives++;
128
+ }
129
+ if (den != BigInt.one) return null;
130
+ final k = twos > fives ? twos : fives;
131
+ return [num * two.pow(k - twos) * five.pow(k - fives), k];
132
+ }
133
+
134
+ // Таблица делимых §58 в срезе: Decimal/Decimal, Money/Decimal, Money/Money одной
135
+ // валюты. Integer/Integer даёт Rational — вне среза, отвергается планом статически.
136
+ static Object div(Object a, Object b) {
137
+ if (!(_num(a) && _num(b))) throw Refused('§58: деление вне таблицы делимых');
138
+ if (_u(b) == BigInt.zero) throw Refused('деление на ноль — evaluation error (§58)');
139
+ Money? like;
140
+ if (a is Money && b is Money) {
141
+ if (a.cur != b.cur) {
142
+ throw Refused('CURRENCY_MISMATCH: §58 делит Money на Money только при тождественной валюте');
143
+ }
144
+ } else if (a is Money) {
145
+ like = a;
146
+ } else if (b is Money) {
147
+ throw Refused('§58: деление вне таблицы делимых');
148
+ }
149
+ final q = _exact(_u(a) * _p10(_s(b)), _u(b) * _p10(_s(a)));
150
+ if (q == null) {
151
+ if (like != null) throw Refused('INEXACT_DIVISION: округление только явной политикой (§50/§18)');
152
+ throw Refused('§58: частное не 10-гладкое — Rational вне среза');
153
+ }
154
+ return like == null ? Dec(q[0] as BigInt, q[1] as int) : Money(q[0] as BigInt, q[1] as int, like.cur);
155
+ }
156
+
157
+ // Евклидов остаток §58: 0 <= r < |b| при любых знаках операндов.
158
+ static Object mod(Object a, Object b) {
159
+ if (!(_integral(a) && _integral(b))) throw Refused('§58: mod определён только для Integer');
160
+ final y = _toInt(b as Dec);
161
+ if (y == BigInt.zero) throw Refused('остаток от деления на ноль — evaluation error (§58)');
162
+ return Dec(_toInt(a as Dec) % y.abs(), 0);
163
+ }
164
+
165
+ static Object min(Object a, Object b) {
166
+ _same(a, b, 'min');
167
+ final p = _pair(a, b);
168
+ return (p[0] as BigInt) <= (p[1] as BigInt) ? a : b;
169
+ }
170
+
171
+ static Object max(Object a, Object b) {
172
+ _same(a, b, 'max');
173
+ final p = _pair(a, b);
174
+ return (p[0] as BigInt) >= (p[1] as BigInt) ? a : b;
175
+ }
176
+
177
+ static Object abs(Object v) {
178
+ if (!_num(v)) throw Refused('§58: abs — не числовой вид');
179
+ return _make(_u(v).abs(), _s(v), v);
180
+ }
181
+
182
+ static final BigInt _powMin = -(BigInt.one << 63);
183
+ static final BigInt _powMax = (BigInt.one << 63) - BigInt.one;
184
+
185
+ // §58 (E-0075): только Integer pow Integer; порядок проверок — знак, предел,
186
+ // вычисление, диапазон — часть контракта.
187
+ static Object pow(Object base, Object exp) {
188
+ if (!(_integral(base) && _integral(exp))) throw Refused('§58: pow определён только для Integer');
189
+ final e = _toInt(exp as Dec);
190
+ if (e.isNegative) throw Refused('§58: pow — показатель отрицателен');
191
+ if (e > BigInt.from(4096)) throw Refused('RESOURCE_LIMIT: pow — показатель превышает 4096');
192
+ final r = _toInt(base as Dec).pow(e.toInt());
193
+ if (r < _powMin || r > _powMax) throw Refused('RESOURCE_LIMIT: pow — результат вне 64-битного диапазона');
194
+ return Dec(r, 0);
195
+ }
196
+
197
+ static Object round(Object m, Object digits, String mode) {
198
+ if (mode != 'HALF_UP') throw Refused('§50: режим округления не реализован: $mode');
199
+ if (!_num(m)) throw Refused('§50: round — не числовой вид');
200
+ final n = digits is int ? digits : _toInt(digits as Dec).toInt();
201
+ if (_s(m) <= n) return _make(_u(m) * _p10(n - _s(m)), n, m);
202
+ final drop = _p10(_s(m) - n);
203
+ final a = _u(m).abs();
204
+ final q = a ~/ drop, r = a.remainder(drop);
205
+ final up = r * BigInt.two >= drop ? q + BigInt.one : q; // HALF_UP — от нуля
206
+ return _make(_u(m).isNegative ? -up : up, n, m);
207
+ }
208
+
209
+ static bool _order(String op, int c) {
210
+ switch (op) {
211
+ case 'lt':
212
+ return c < 0;
213
+ case 'le':
214
+ return c <= 0;
215
+ case 'gt':
216
+ return c > 0;
217
+ case 'ge':
218
+ return c >= 0;
219
+ case 'eq':
220
+ return c == 0;
221
+ case 'ne':
222
+ return c != 0;
223
+ }
224
+ throw Refused('сравнение вне среза рантайма: $op');
225
+ }
226
+
227
+ static bool cmp(Object a, String op, Object b) {
228
+ if (a is Sym || b is Sym) {
229
+ final same = a is Sym && b is Sym && a.id == b.id;
230
+ if (op == 'eq') return same;
231
+ if (op == 'ne') return !same;
232
+ throw Refused('§57: вид enum не упорядочен');
233
+ }
234
+ if (a is LDate || b is LDate) {
235
+ if (a is LDate && b is LDate) return _order(op, a.v.compareTo(b.v));
236
+ if (op == 'eq') return false;
237
+ if (op == 'ne') return true;
238
+ throw Refused('§57: Date сравнивается только с Date');
239
+ }
240
+ if (a is String || b is String) {
241
+ if (op == 'eq') return a == b;
242
+ if (op == 'ne') return a != b;
243
+ throw Refused('порядок на Text вне среза рантайма: $op');
244
+ }
245
+ if ((a is Money) != (b is Money)) throw Refused('§49: сравнение денег с безразмерным');
246
+ if (a is Money && a.cur != (b as Money).cur) throw Refused('§49: разные валюты');
247
+ final p = _pair(a, b);
248
+ return _order(op, (p[0] as BigInt).compareTo(p[1] as BigInt));
249
+ }
250
+
251
+ static bool eq(Object a, Object b) {
252
+ if (a is Sym || b is Sym) return a is Sym && b is Sym && a.id == b.id;
253
+ if (a is LDate || b is LDate) return a is LDate && b is LDate && a.v == b.v;
254
+ if (a is String || b is String) return a == b;
255
+ if ((a is Money) != (b is Money)) return false;
256
+ if (a is Money && a.cur != (b as Money).cur) return false;
257
+ final p = _pair(a, b);
258
+ return p[0] == p[1];
259
+ }
260
+
261
+ static String _norm(Object d) {
262
+ var u = _u(d);
263
+ var s = _s(d);
264
+ while (s > 0 && u.remainder(_ten) == BigInt.zero) {
265
+ u = u ~/ _ten;
266
+ s--;
267
+ }
268
+ return '${u}e-$s';
269
+ }
270
+
271
+ // Разделитель и экран внутри текста экранируются: иначе ["a|e:b","c"] и
272
+ // ["a","b|e:c"] склеивались в один ключ, и второй факт терялся.
273
+ static String _esc(String text) => text.replaceAll('\\', '\\\\').replaceAll('|', '\\|');
274
+
275
+ static String key(List<Object> row) => row.map((v) {
276
+ if (v is String) return 'e:' + _esc(v);
277
+ if (v is LDate) return 't:' + v.v;
278
+ if (v is Sym) return 'c:' + _esc(v.id);
279
+ if (v is Money) return 'm:' + v.cur + ':' + _norm(v);
280
+ return 'd:' + _norm(v);
281
+ }).join('|');
282
+
283
+ static String _legalTime = '';
284
+
285
+ static void setLegalTime(String t) {
286
+ _legalTime = t;
287
+ }
288
+
289
+ static String legalTime() {
290
+ if (_legalTime.isEmpty) throw Refused('legalTime не подан делом');
291
+ return _legalTime;
292
+ }
293
+ }
@@ -0,0 +1,64 @@
1
+ %% Харнесс сертификации рантайма: исполняет runtime-cert.json и печатает,
2
+ %% что получилось. Ожидания сверяют ворота, харнесс их не читает.
3
+ -module(cert).
4
+ -export([main/1]).
5
+
6
+ lit(X) when is_binary(X) -> unicode:characters_to_list(X);
7
+ lit(#{<<"dec">> := D}) -> rt:dec(D);
8
+ lit(#{<<"money">> := M, <<"cur">> := C}) -> rt:money(M, C);
9
+ lit(#{<<"text">> := T}) -> {text, T};
10
+ lit(#{<<"date">> := D}) -> rt:date(D);
11
+ lit(#{<<"sym">> := S}) -> rt:sym(S);
12
+ lit(#{<<"row">> := R}) -> [lit(V) || V <- R].
13
+
14
+ %% Отображение с ЯВНЫМ масштабом: 2.00 и 2 — разные записи одного значения.
15
+ digits(U, S) ->
16
+ Body0 = integer_to_list(erlang:abs(U)),
17
+ Body = lists:duplicate(erlang:max(0, S + 1 - length(Body0)), $0) ++ Body0,
18
+ Text = case S of
19
+ 0 -> Body;
20
+ _ -> {I, F} = lists:split(length(Body) - S, Body), I ++ "." ++ F
21
+ end,
22
+ unicode:characters_to_binary(case U < 0 of true -> "-" ++ Text; false -> Text end).
23
+
24
+ render(null) -> null;
25
+ render(B) when is_boolean(B) -> B;
26
+ render({text, T}) -> T;
27
+ render({money, U, S, C}) -> <<(digits(U, S))/binary, " ", C/binary>>;
28
+ render({date, D}) -> D;
29
+ render({sym, I}) -> I;
30
+ render({dec, U, S}) -> digits(U, S).
31
+
32
+ run(#{<<"op">> := Op, <<"args">> := Raw}) ->
33
+ A = [lit(X) || X <- Raw],
34
+ case Op of
35
+ <<"dec">> -> rt:dec(hd(A));
36
+ <<"money">> -> rt:money(hd(A), lists:nth(2, A));
37
+ <<"date">> -> rt:date(hd(A));
38
+ <<"sym">> -> rt:sym(hd(A));
39
+ <<"add">> -> rt:add(hd(A), lists:nth(2, A));
40
+ <<"sub">> -> rt:sub(hd(A), lists:nth(2, A));
41
+ <<"mul">> -> rt:mul(hd(A), lists:nth(2, A));
42
+ <<"div">> -> rt:'div'(hd(A), lists:nth(2, A));
43
+ <<"mod">> -> rt:'mod'(hd(A), lists:nth(2, A));
44
+ <<"min">> -> rt:min(hd(A), lists:nth(2, A));
45
+ <<"max">> -> rt:max(hd(A), lists:nth(2, A));
46
+ <<"abs">> -> rt:'abs'(hd(A));
47
+ <<"pow">> -> rt:pow(hd(A), lists:nth(2, A));
48
+ <<"round">> -> rt:round(hd(A), lists:nth(2, A), lists:nth(3, A));
49
+ <<"cmp">> -> rt:cmp(hd(A), lists:nth(2, A), lists:nth(3, A));
50
+ <<"eq">> -> rt:eq(hd(A), lists:nth(2, A));
51
+ <<"keyEq">> -> rt:key(hd(A)) =:= rt:key(lists:nth(2, A));
52
+ <<"legalTime">> -> {text, rt:legal_time()};
53
+ <<"setLegalTime">> -> rt:set_legal_time(hd(A)), null
54
+ end.
55
+
56
+ main([Path]) ->
57
+ {ok, Bin} = file:read_file(Path),
58
+ #{<<"cases">> := Cases} = json:decode(Bin),
59
+ Out = [begin
60
+ Got = try render(run(C)) catch throw:{refused, Msg} -> #{<<"refused">> => Msg} end,
61
+ #{<<"id">> => maps:get(<<"id">>, C), <<"got">> => Got}
62
+ end || C <- Cases],
63
+ io:format("~ts~n", [json:encode(Out)]),
64
+ erlang:halt(0).