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,229 @@
1
+ #!/usr/bin/env python3
2
+ """Эмиттер PHP. Тот же план и тот же контракт рантайма; модель — класс `Model`
3
+ со статическими таблицами. Инициализатор статического свойства PHP допускает
4
+ только константное выражение, поэтому константы §54 и собственные факты пакета
5
+ — функции, которые раннер зовёт до фактов дела."""
6
+ import json
7
+ import shutil
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ # Версия плана, которую этот эмиттер умеет печатать. Другая версия — отказ:
12
+ # незнакомое поле плана печаталось бы молча, а это другой ответ, не меньший.
13
+ SUPPORTED_PLAN = "law-backend-plan/0.7"
14
+
15
+
16
+ def check_plan(plan: dict) -> None:
17
+ v = plan.get("planVersion")
18
+ if v != SUPPORTED_PLAN:
19
+ raise ValueError(f"версия плана не поддержана эмиттером: {v!r} (поддержана {SUPPORTED_PLAN})")
20
+
21
+
22
+ def ps(s: str) -> str:
23
+ """Литерал в одинарных кавычках: без интерполяции `$`, экранируются только
24
+ экран и кавычка, остальные байты идут как есть."""
25
+ return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"
26
+
27
+
28
+ def term(t: dict) -> str:
29
+ k = t["k"]
30
+ if k == "var":
31
+ return f'$r{t["row"]}[{t["col"]}]'
32
+ if k == "lit":
33
+ if t["t"] == "money":
34
+ return f'RT::money({ps(t["v"])}, {ps(t["cur"])})'
35
+ if t["t"] == "dec":
36
+ return f'RT::dec({ps(t["v"])})'
37
+ if t["t"] == "date":
38
+ return f'RT::date({ps(t["v"])})'
39
+ if t["t"] == "enum":
40
+ return f'RT::sym({ps(t["v"])})'
41
+ return ps(t["v"])
42
+ if k == "bin":
43
+ return f'RT::{t["op"]}({term(t["l"])}, {term(t["r"])})'
44
+ if k == "call":
45
+ return f'RT::{t["fn"]}(' + ", ".join(term(a) for a in t["args"]) + ')'
46
+ raise AssertionError(k)
47
+
48
+
49
+ def arr(items) -> str:
50
+ return "[" + ", ".join(items) + "]"
51
+
52
+
53
+ def emit(plan: dict) -> str:
54
+ check_plan(plan)
55
+ L = ["<?php", "declare(strict_types=1);", "", "final class Model", "{", " public static array $TABLES = ["]
56
+ for t in plan["tables"]:
57
+ L.append(f' {ps(t["pred"])} => [],')
58
+ L.append(" ];")
59
+ readers: dict[str, set[str]] = {}
60
+ for rule in plan["rules"]:
61
+ for atom in rule.get("atoms") or []:
62
+ readers.setdefault(atom["pred"], set()).add(rule["head"]["pred"])
63
+ for check in rule.get("checks") or []:
64
+ readers.setdefault(check["pred"], set()).add(rule["head"]["pred"])
65
+ L.append(" // E-0210/E-0242: forward readers of a keyed relation.")
66
+ L.append(" public const KEY_READERS = [")
67
+ for source, consumers in sorted(readers.items()):
68
+ L.append(f' {ps(source)} => {arr(ps(value) for value in sorted(consumers))},')
69
+ L.append(" ];")
70
+ L.append(" // Объявленные типы параметров: раннер сверяет с ними аргументы дела (§189).")
71
+ L.append(" public const PARAMS = [")
72
+ for t in plan["tables"]:
73
+ L.append(f' {ps(t["pred"])} => {arr(ps(p) for p in t["params"])},')
74
+ L.append(" ];")
75
+ L.append(" // Наследование сущностей: подтип подходит параметру базового типа.")
76
+ L.append(" public const BASES = [")
77
+ for sub, base in plan["bases"].items():
78
+ L.append(f" {ps(sub)} => {ps(base)},")
79
+ L.append(" ];")
80
+ L.append(" // Ключи отношений §45.1: позиции столбцов каждого ключа.")
81
+ L.append(" public const KEYS = [")
82
+ for t in plan["tables"]:
83
+ if t["keys"]:
84
+ L.append(f' {ps(t["pred"])} => {json.dumps(t["keys"])},')
85
+ L.append(" ];")
86
+ L.append(" private static array $seen = [];")
87
+ L.append("")
88
+ L.append(" // Константы §54: тип для проверки §189 и значение для разыменования аргументов дела.")
89
+ L.append(" public static function consts(): array")
90
+ L.append(" {")
91
+ L.append(" return [")
92
+ for cid, c in plan["consts"].items():
93
+ L.append(f' {ps(cid)} => [{ps(c["type"])}, {term(c["term"])}],')
94
+ L.append(" ];")
95
+ L.append(" }")
96
+ L.append("")
97
+ L.append(" private static function addRow(string $urn, array $row): bool")
98
+ L.append(" {")
99
+ L.append(" $k = RT::key($row);")
100
+ L.append(" if (isset(self::$seen[$urn][$k])) {")
101
+ L.append(" return false;")
102
+ L.append(" }")
103
+ L.append(" self::$seen[$urn][$k] = true;")
104
+ L.append(" self::$TABLES[$urn][] = $row;")
105
+ L.append(" return true;")
106
+ L.append(" }")
107
+ L.append("")
108
+ for r in plan["rules"]:
109
+ L.append(f' // {r["id"]}')
110
+ L.append(f' private static function rule_{r["name"]}(): bool')
111
+ L.append(" {")
112
+ L.append(" $grew = false;")
113
+ if r["window"]:
114
+ if r["window"].get("start"):
115
+ L.append(f' if (strcmp(RT::legalTime(), {ps(r["window"]["start"])}) < 0) {{')
116
+ L.append(" return false;")
117
+ L.append(" }")
118
+ if r["window"].get("end"):
119
+ L.append(f' if (strcmp(RT::legalTime(), {ps(r["window"]["end"])}) >= 0) {{')
120
+ L.append(" return false;")
121
+ L.append(" }")
122
+ ind = " "
123
+ for i, a in enumerate(r["atoms"]):
124
+ # foreach по массиву PHP идёт по копии на входе: строки, добавленные
125
+ # этим же проходом, не перебираются.
126
+ L.append(f'{ind}foreach (self::$TABLES[{ps(a["pred"])}] as $r{i}) {{')
127
+ ind += " "
128
+ for (ai, aj, _at), (bi, bj, _bt) in r["joins"]:
129
+ L.append(f"{ind}if (!RT::cmp($r{ai}[{aj}], 'eq', $r{bi}[{bj}])) {{ continue; }}")
130
+ for f in r["filters"]:
131
+ L.append(f'{ind}if (!RT::eq($r{f["row"]}[{f["col"]}], {term(f["term"])})) {{ continue; }}')
132
+ for g in r["guards"]:
133
+ L.append(f'{ind}if (!RT::cmp({term(g["l"])}, {ps(g["op"])}, {term(g["r"])})) {{ continue; }}')
134
+ args = ", ".join(term(a) for a in r["head"]["args"])
135
+ L.append(f'{ind}if (self::addRow({ps(r["head"]["pred"])}, [{args}])) {{ $grew = true; }}')
136
+ for _ in r["atoms"]:
137
+ ind = ind[:-4]
138
+ L.append(ind + "}")
139
+ L.append(" return $grew;")
140
+ L.append(" }")
141
+ L.append("")
142
+ L.append(" public static function saturate(): void")
143
+ L.append(" {")
144
+ L.append(" $grew = true;")
145
+ L.append(" while ($grew) {")
146
+ L.append(" $grew = false;")
147
+ for r in plan["rules"]:
148
+ L.append(f' if (self::rule_{r["name"]}()) {{ $grew = true; }}')
149
+ L.append(" }")
150
+ L.append(" }")
151
+ L.append("")
152
+ L.append(" public static function register(string $urn, array $row): bool")
153
+ L.append(" {")
154
+ L.append(" return self::addRow($urn, $row);")
155
+ L.append(" }")
156
+ L.append("")
157
+ L.append(" // KEY_CONFLICT §45.1/§196: группы строк одной таблицы с одинаковой проекцией на ключ; строки уникальны по ключу строки, поэтому группа из двух и более строк — различные accepted-кортежи. Считается после насыщения.")
158
+ L.append(" public static function keyConflicts(): int")
159
+ L.append(" {")
160
+ L.append(" $n = 0;")
161
+ L.append(" foreach (self::KEYS as $urn => $keys) {")
162
+ L.append(" foreach ($keys as $key) {")
163
+ L.append(" $groups = [];")
164
+ L.append(" foreach (self::$TABLES[$urn] as $row) {")
165
+ L.append(" $g = RT::key(array_map(fn($i) => $row[$i], $key));")
166
+ L.append(" $groups[$g] = ($groups[$g] ?? 0) + 1;")
167
+ L.append(" }")
168
+ L.append(" $n += count(array_filter($groups, fn($c) => $c > 1));")
169
+ L.append(" }")
170
+ L.append(" }")
171
+ L.append(" return $n;")
172
+ L.append(" }")
173
+ L.append("")
174
+ L.append(" public static function keyConflictPredicates(): array")
175
+ L.append(" {")
176
+ L.append(" $out = [];")
177
+ L.append(" foreach (self::KEYS as $urn => $keys) foreach ($keys as $key) {")
178
+ L.append(" $groups = [];")
179
+ L.append(" foreach (self::$TABLES[$urn] as $row) { $g = RT::key(array_map(fn($i) => $row[$i], $key)); $groups[$g] = ($groups[$g] ?? 0) + 1; }")
180
+ L.append(" if (array_filter($groups, fn($c) => $c > 1)) $out[$urn] = true;")
181
+ L.append(" }")
182
+ L.append(" return array_keys($out);")
183
+ L.append(" }")
184
+ L.append("")
185
+ L.append(" // Собственные факты пакета: раннер кладёт их до фактов дела.")
186
+ L.append(" public static function packageFacts(): void")
187
+ L.append(" {")
188
+ for f in plan["facts"]:
189
+ L.append(f' self::addRow({ps(f["pred"])}, [' + ", ".join(term(a) for a in f["args"]) + "]);")
190
+ L.append(" }")
191
+ L.append("}")
192
+ return "\n".join(L) + "\n"
193
+
194
+
195
+ # --- дескриптор цели ---------------------------------------------------------
196
+ HERE = Path(__file__).resolve().parent
197
+ EXT = "php"
198
+ MODEL = "model.php"
199
+ MAINS = {"runner": "runner.php", "cert": "cert.php"}
200
+
201
+
202
+ def available():
203
+ php = shutil.which("php")
204
+ return (True, php) if php else (False, "нет php в PATH")
205
+
206
+
207
+ def layout(proj, main: str) -> list:
208
+ proj = Path(proj)
209
+ proj.mkdir(parents=True, exist_ok=True)
210
+ out = []
211
+ for name in ("runtime.php", "json.php", MAINS[main]):
212
+ shutil.copyfile(HERE / name, proj / name)
213
+ out.append(proj / name)
214
+ return out
215
+
216
+
217
+ def build(proj):
218
+ return None
219
+
220
+
221
+ def run(proj, main: str):
222
+ return ["php", str(Path(proj) / MAINS[main])], None
223
+
224
+
225
+ if __name__ == "__main__":
226
+ sys.path.insert(0, str(HERE.parents[1]))
227
+ import plan as P
228
+ ir = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
229
+ Path(sys.argv[2]).write_text(emit(P.build_plan(ir)), encoding="utf-8")
@@ -0,0 +1,144 @@
1
+ <?php
2
+ // Разбор JSON с числами ТЕКСТОМ: `json_decode` отдаёт дробную запись как
3
+ // float (2.50 теряет масштаб, длинная дробь — цифры). Число здесь — JNum с
4
+ // исходной записью; остальное — обычные значения PHP, объект — массив.
5
+ declare(strict_types=1);
6
+
7
+ final class JNum
8
+ {
9
+ public function __construct(public readonly string $text) {}
10
+ }
11
+
12
+ final class JsonText
13
+ {
14
+ private int $i = 0;
15
+
16
+ private function __construct(private readonly string $s) {}
17
+
18
+ public static function read(string $path): mixed
19
+ {
20
+ $s = file_get_contents($path);
21
+ if ($s === false) {
22
+ throw new RuntimeException("не прочитать $path");
23
+ }
24
+ $p = new JsonText($s);
25
+ $v = $p->value();
26
+ $p->ws();
27
+ if ($p->i !== strlen($s)) {
28
+ $p->fail('лишнее после значения');
29
+ }
30
+ return $v;
31
+ }
32
+
33
+ /** Текст значения JSON: строка как есть, число — его запись. */
34
+ public static function text(mixed $v): string
35
+ {
36
+ if ($v instanceof JNum) {
37
+ return $v->text;
38
+ }
39
+ if (is_bool($v)) {
40
+ return $v ? 'true' : 'false';
41
+ }
42
+ return $v === null || is_array($v) ? '' : (string) $v;
43
+ }
44
+
45
+ /** Строка JSON: UTF-8 как есть, экранируются только кавычка, экран и управляющие. */
46
+ public static function str(string $s): string
47
+ {
48
+ return json_encode($s, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
49
+ }
50
+
51
+ private function fail(string $what): never
52
+ {
53
+ throw new RuntimeException("JSON: $what в позиции {$this->i}");
54
+ }
55
+
56
+ private function ws(): void
57
+ {
58
+ $this->i += strspn($this->s, " \t\r\n", $this->i);
59
+ }
60
+
61
+ private function value(): mixed
62
+ {
63
+ $this->ws();
64
+ $c = $this->s[$this->i] ?? '';
65
+ if ($c === '{') {
66
+ $this->i++;
67
+ $obj = [];
68
+ $this->ws();
69
+ if (($this->s[$this->i] ?? '') === '}') {
70
+ $this->i++;
71
+ return $obj;
72
+ }
73
+ for (;;) {
74
+ $this->ws();
75
+ if (($this->s[$this->i] ?? '') !== '"') {
76
+ $this->fail('ожидался ключ');
77
+ }
78
+ $k = $this->string();
79
+ $this->ws();
80
+ if (($this->s[$this->i] ?? '') !== ':') {
81
+ $this->fail("ожидалось ':'");
82
+ }
83
+ $this->i++;
84
+ $obj[$k] = $this->value();
85
+ $this->ws();
86
+ $d = $this->s[$this->i] ?? '';
87
+ $this->i++;
88
+ if ($d === '}') {
89
+ return $obj;
90
+ }
91
+ if ($d !== ',') {
92
+ $this->fail("ожидалось ',' или '}'");
93
+ }
94
+ }
95
+ }
96
+ if ($c === '[') {
97
+ $this->i++;
98
+ $arr = [];
99
+ $this->ws();
100
+ if (($this->s[$this->i] ?? '') === ']') {
101
+ $this->i++;
102
+ return $arr;
103
+ }
104
+ for (;;) {
105
+ $arr[] = $this->value();
106
+ $this->ws();
107
+ $d = $this->s[$this->i] ?? '';
108
+ $this->i++;
109
+ if ($d === ']') {
110
+ return $arr;
111
+ }
112
+ if ($d !== ',') {
113
+ $this->fail("ожидалось ',' или ']'");
114
+ }
115
+ }
116
+ }
117
+ if ($c === '"') {
118
+ return $this->string();
119
+ }
120
+ foreach (['true' => true, 'false' => false, 'null' => null] as $word => $val) {
121
+ if (substr_compare($this->s, $word, $this->i, strlen($word)) === 0) {
122
+ $this->i += strlen($word);
123
+ return $val;
124
+ }
125
+ }
126
+ if (!preg_match('/\G-?\d+(\.\d+)?([eE][-+]?\d+)?/', $this->s, $m, 0, $this->i)) {
127
+ $this->fail('неизвестное значение');
128
+ }
129
+ $this->i += strlen($m[0]);
130
+ return new JNum($m[0]);
131
+ }
132
+
133
+ private function string(): string
134
+ {
135
+ // Отрезок от кавычки до закрывающей кавычки разбирает json_decode ядра:
136
+ // у строки JSON нет чисел, и её экраны он знает.
137
+ // Жадные без отката (`++`, `*+`): иначе длинная строка исчерпала бы стек PCRE.
138
+ if (!preg_match('/\G"(?:[^"\\\\]++|\\\\.)*+"/s', $this->s, $m, 0, $this->i)) {
139
+ $this->fail('строка не закрыта');
140
+ }
141
+ $this->i += strlen($m[0]);
142
+ return json_decode($m[0], false, 1, JSON_THROW_ON_ERROR);
143
+ }
144
+ }
@@ -0,0 +1,189 @@
1
+ <?php
2
+ // Раннер PHP-бэкенда: тот же вход и тот же ответ, что у runner.rb.
3
+ //
4
+ // Протокол выхода общий для всех целей: ответ — JSON и код 0; названный отказ
5
+ // среза — JSON {"refused": текст} и код 3, текст побайтово одинаков между
6
+ // целями; любое другое исключение — крах (код 255), который ворота валят.
7
+ declare(strict_types=1);
8
+
9
+ require __DIR__ . '/runtime.php';
10
+ require __DIR__ . '/json.php';
11
+ require __DIR__ . '/model.php';
12
+
13
+ $ISSUES = [];
14
+ $CONSTS = [];
15
+
16
+ function arg(array $a): mixed
17
+ {
18
+ global $CONSTS;
19
+ $kind = JsonText::text($a['kind'] ?? null);
20
+ if ($kind === 'entity_ref') {
21
+ return JsonText::text($a['id'] ?? null);
22
+ }
23
+ if ($kind === 'const_ref') {
24
+ // §54: разыменование предшествует исполнению; член перечисления §38 либо
25
+ // имя без объявления остаётся идентичностью, как в ключе атома оракула.
26
+ $id = JsonText::text($a['id'] ?? null);
27
+ return array_key_exists($id, $CONSTS) ? $CONSTS[$id][1] : RT::sym($id);
28
+ }
29
+ if ($kind !== 'value') {
30
+ throw new Refused("аргумент вне среза раннера: $kind");
31
+ }
32
+ $t = JsonText::text($a['type']['name'] ?? null);
33
+ $v = JsonText::text($a['value'] ?? null);
34
+ return match ($t) {
35
+ 'urn:law:std#Money' => RT::money($v, JsonText::text($a['currency'] ?? null)),
36
+ 'urn:law:std#Decimal', 'urn:law:std#Integer' => RT::dec($v),
37
+ 'urn:law:std#Text' => $v,
38
+ 'urn:law:std#Date' => RT::date($v),
39
+ default => throw new Refused("аргумент вне среза раннера: $t"),
40
+ };
41
+ }
42
+
43
+ // Тип аргумента, если известен: у value — из узла, у const_ref — из объявления
44
+ // константы; entity_ref типа не несёт (как у оракула).
45
+ function arg_type(array $a): ?string
46
+ {
47
+ global $CONSTS;
48
+ $kind = $a['kind'] ?? null;
49
+ if ($kind === 'value') {
50
+ return $a['type']['name'] ?? null;
51
+ }
52
+ if ($kind === 'const_ref') {
53
+ return $CONSTS[JsonText::text($a['id'] ?? null)][0] ?? null;
54
+ }
55
+ return null;
56
+ }
57
+
58
+ // Совпадение типа с наследованием сущностей вверх от фактического.
59
+ function conforms(string $actual, string $expected): bool
60
+ {
61
+ $seen = [];
62
+ $cur = $actual;
63
+ while ($cur !== null && !isset($seen[$cur])) {
64
+ if ($cur === $expected) {
65
+ return true;
66
+ }
67
+ $seen[$cur] = true;
68
+ $cur = Model::BASES[$cur] ?? null;
69
+ }
70
+ return false;
71
+ }
72
+
73
+ // Арность и типы аргументов утверждения против сигнатуры §189 (E-0046).
74
+ function well_typed(array $lit): bool
75
+ {
76
+ global $ISSUES;
77
+ $params = Model::PARAMS[$lit['predicate']] ?? null;
78
+ if ($params === null) {
79
+ return true;
80
+ }
81
+ $args = $lit['args'] ?? [];
82
+ if (count($args) !== count($params)) {
83
+ $ISSUES[] = 'TYPE_ERROR';
84
+ return false;
85
+ }
86
+ foreach ($params as $i => $expected) {
87
+ $actual = arg_type($args[$i]);
88
+ if ($actual !== null && !conforms($actual, $expected)) {
89
+ $ISSUES[] = 'TYPE_ERROR';
90
+ return false;
91
+ }
92
+ }
93
+ return true;
94
+ }
95
+
96
+ // Ключ атома у оракула §208 включает ТИП значения: несогласованный запрос
97
+ // атома не находит — NEITHER без issue.
98
+ function query_typed(array $lit): bool
99
+ {
100
+ $params = Model::PARAMS[$lit['predicate']] ?? null;
101
+ if ($params === null) {
102
+ return false;
103
+ }
104
+ $args = $lit['args'] ?? [];
105
+ if (count($args) !== count($params)) {
106
+ return false;
107
+ }
108
+ foreach ($params as $i => $expected) {
109
+ $actual = arg_type($args[$i]);
110
+ if ($actual !== null && !conforms($actual, $expected)) {
111
+ return false;
112
+ }
113
+ }
114
+ return true;
115
+ }
116
+
117
+ function key_conflict_blocks(string $predicate): bool
118
+ {
119
+ $tainted = [];
120
+ foreach (Model::keyConflictPredicates() as $source) $tainted[$source] = true;
121
+ $waiting = array_keys($tainted);
122
+ while ($waiting) {
123
+ $source = array_pop($waiting);
124
+ foreach (Model::KEY_READERS[$source] ?? [] as $consumer) {
125
+ if (!isset($tainted[$consumer])) { $tainted[$consumer] = true; $waiting[] = $consumer; }
126
+ }
127
+ }
128
+ return isset($tainted[$predicate]);
129
+ }
130
+
131
+ function main(array $argv): void
132
+ {
133
+ global $ISSUES, $CONSTS;
134
+ $case = JsonText::read($argv[1]);
135
+ $query = JsonText::read($argv[2]);
136
+ RT::setLegalTime(JsonText::text($case['context']['legalTime'] ?? null));
137
+ $CONSTS = Model::consts();
138
+ Model::packageFacts();
139
+ foreach ($case['assertions'] as $a) {
140
+ $lit = $a['literal'];
141
+ if (($lit['polarity'] ?? null) !== 'positive') {
142
+ throw new Refused('§113: отрицательный факт вне L0-среза');
143
+ }
144
+ if (!well_typed($lit) || !array_key_exists($lit['predicate'], Model::PARAMS)) {
145
+ continue;
146
+ }
147
+ Model::register($lit['predicate'], array_map('arg', $lit['args']));
148
+ }
149
+ Model::saturate();
150
+ for ($n = Model::keyConflicts(); $n > 0; $n--) {
151
+ $ISSUES[] = 'KEY_CONFLICT';
152
+ }
153
+ $kind = JsonText::text($query['kind'] ?? null);
154
+ if ($kind !== 'truth') {
155
+ throw new Refused("вид запроса вне среза раннера: $kind");
156
+ }
157
+ $hit = false;
158
+ $ql = $query['literal'];
159
+ if (query_typed($ql)) {
160
+ $want = array_map('arg', $ql['args']);
161
+ foreach (Model::$TABLES[$ql['predicate']] ?? [] as $r) {
162
+ if (count($r) !== count($want)) {
163
+ continue;
164
+ }
165
+ $all = true;
166
+ foreach ($r as $i => $v) {
167
+ if (!RT::eq($v, $want[$i])) {
168
+ $all = false;
169
+ break;
170
+ }
171
+ }
172
+ if ($all) {
173
+ $hit = true;
174
+ break;
175
+ }
176
+ }
177
+ }
178
+ // Открытый мир: отсутствие строки — это NEITHER, а не FALSE_ONLY.
179
+ echo '{"queryId":' . JsonText::str(JsonText::text($query['queryId'] ?? null))
180
+ . ',"truthStatus":' . JsonText::str($hit ? 'TRUE_ONLY' : 'NEITHER')
181
+ . ',"evaluationStatus":' . JsonText::str(key_conflict_blocks($ql['predicate']) ? 'NON_EXECUTABLE' : 'COMPUTED') . ',"issues":[' . implode(',', array_map([JsonText::class, 'str'], $ISSUES)) . "]}\n";
182
+ }
183
+
184
+ try {
185
+ main($argv);
186
+ } catch (Refused $e) {
187
+ echo '{"refused":' . JsonText::str($e->getMessage()) . "}\n";
188
+ exit(3);
189
+ }