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,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
|
+
}
|