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,204 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Эмиттер Elixir. Модель — модуль `LawModel` с чистым состоянием таблиц: правила
|
|
3
|
+
принимают db и возвращают {grew, db}; порядок строк — порядок добавления.
|
|
4
|
+
Сборки нет: `elixir runner.exs` компилирует файлы при запуске."""
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
SUPPORTED_PLAN = "law-backend-plan/0.7"
|
|
11
|
+
OPS = {"add": "add", "sub": "sub", "mul": "mul", "div": "divide", "mod": "modulo"}
|
|
12
|
+
FNS = {"round": "round", "min": "min", "max": "max", "abs": "absolute", "pow": "pow"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_plan(plan: dict) -> None:
|
|
16
|
+
v = plan.get("planVersion")
|
|
17
|
+
if v != SUPPORTED_PLAN:
|
|
18
|
+
raise ValueError(f"версия плана не поддержана эмиттером: {v!r} (поддержана {SUPPORTED_PLAN})")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def estr(s: str) -> str:
|
|
22
|
+
return json.dumps(s, ensure_ascii=False).replace("#{", "\\#{")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def term(t: dict) -> str:
|
|
26
|
+
k = t["k"]
|
|
27
|
+
if k == "var":
|
|
28
|
+
return f'elem(r{t["row"]}, {t["col"]})'
|
|
29
|
+
if k == "lit":
|
|
30
|
+
if t["t"] == "money":
|
|
31
|
+
return f'LawRT.money({estr(t["v"])}, {estr(t["cur"])})'
|
|
32
|
+
if t["t"] == "dec":
|
|
33
|
+
return f'LawRT.dec({estr(t["v"])})'
|
|
34
|
+
if t["t"] == "date":
|
|
35
|
+
return f'LawRT.date({estr(t["v"])})'
|
|
36
|
+
if t["t"] == "enum":
|
|
37
|
+
return f'LawRT.sym({estr(t["v"])})'
|
|
38
|
+
return f'{{:text, {estr(t["v"])}}}'
|
|
39
|
+
if k == "bin":
|
|
40
|
+
return f'LawRT.{OPS[t["op"]]}({term(t["l"])}, {term(t["r"])})'
|
|
41
|
+
if k == "call":
|
|
42
|
+
args = t["args"]
|
|
43
|
+
if t["fn"] == "round":
|
|
44
|
+
return f'LawRT.round({term(args[0])}, {term(args[1])}, {estr(args[2]["v"])})'
|
|
45
|
+
return f'LawRT.{FNS[t["fn"]]}(' + ", ".join(term(a) for a in args) + ')'
|
|
46
|
+
raise AssertionError(k)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def emit(plan: dict) -> str:
|
|
50
|
+
check_plan(plan)
|
|
51
|
+
L = ["defmodule LawModel do"]
|
|
52
|
+
L.append(" def new, do: %{tables: %{" + ", ".join(f"{estr(t['pred'])} => []" for t in plan["tables"]) + "}, seen: %{}}")
|
|
53
|
+
L.append(" def rows(db, pred), do: Map.get(db.tables, pred, [])")
|
|
54
|
+
L.append(" # Объявленные типы параметров: раннер сверяет с ними аргументы дела (§189).")
|
|
55
|
+
for t in plan["tables"]:
|
|
56
|
+
L.append(f" def params({estr(t['pred'])}), do: [" + ", ".join(estr(p) for p in t["params"]) + "]")
|
|
57
|
+
L.append(" def params(_), do: nil")
|
|
58
|
+
L.append(" # Константы §54: тип для проверки §189 и значение для разыменования аргументов дела.")
|
|
59
|
+
for cid, c in plan["consts"].items():
|
|
60
|
+
L.append(f" def consts({estr(cid)}), do: {{{estr(c['type'])}, {term(c['term'])}}}")
|
|
61
|
+
L.append(" def consts(_), do: nil")
|
|
62
|
+
L.append(" # Наследование сущностей: подтип подходит параметру базового типа.")
|
|
63
|
+
for sub, base in plan["bases"].items():
|
|
64
|
+
L.append(f" def base({estr(sub)}), do: {estr(base)}")
|
|
65
|
+
L.append(" def base(_), do: nil")
|
|
66
|
+
L.append(" # Ключи отношений §45.1: позиции столбцов каждого ключа.")
|
|
67
|
+
for t in plan["tables"]:
|
|
68
|
+
if t["keys"]:
|
|
69
|
+
L.append(f" def keys({estr(t['pred'])}), do: [" + ", ".join("[" + ", ".join(str(i) for i in k) + "]" for k in t["keys"]) + "]")
|
|
70
|
+
L.append(" def keys(_), do: []")
|
|
71
|
+
L.append("")
|
|
72
|
+
L.append(" # KEY_CONFLICT §45.1/§196: группы строк одной таблицы с одинаковой проекцией на ключ; строки уникальны по ключу строки, поэтому группа из двух и более строк — различные accepted-кортежи. Считается после насыщения.")
|
|
73
|
+
L.append(" def key_conflicts(db) do")
|
|
74
|
+
L.append(" Enum.sum(for pred <- Map.keys(db.tables), key <- keys(pred) do")
|
|
75
|
+
L.append(" rows(db, pred)")
|
|
76
|
+
L.append(" |> Enum.frequencies_by(fn row -> LawRT.key(Enum.map(key, &elem(row, &1))) end)")
|
|
77
|
+
L.append(" |> Map.values()")
|
|
78
|
+
L.append(" |> Enum.count(&(&1 > 1))")
|
|
79
|
+
L.append(" end)")
|
|
80
|
+
L.append(" end")
|
|
81
|
+
L.append("")
|
|
82
|
+
L.append(" def key_conflict_predicates(db), do: for pred <- Map.keys(db.tables), Enum.any?(keys(pred), &(key_conflicts_for(db, pred, &1) > 0)), do: pred")
|
|
83
|
+
L.append(" defp key_conflicts_for(db, pred, key), do: rows(db, pred) |> Enum.frequencies_by(fn row -> LawRT.key(Enum.map(key, &elem(row, &1))) end) |> Map.values() |> Enum.count(&(&1 > 1))")
|
|
84
|
+
readers: dict[str, set[str]] = {}
|
|
85
|
+
for rule in plan["rules"]:
|
|
86
|
+
for atom in rule.get("atoms") or []:
|
|
87
|
+
readers.setdefault(atom["pred"], set()).add(rule["head"]["pred"])
|
|
88
|
+
for check in rule.get("checks") or []:
|
|
89
|
+
readers.setdefault(check["pred"], set()).add(rule["head"]["pred"])
|
|
90
|
+
for pred, heads in sorted(readers.items()):
|
|
91
|
+
L.append(f" def key_readers({estr(pred)}), do: [" + ", ".join(estr(head) for head in sorted(heads)) + "]")
|
|
92
|
+
L.append(" def key_readers(_), do: []")
|
|
93
|
+
L.append("")
|
|
94
|
+
L.append(" def add_row(db, pred, row) do")
|
|
95
|
+
L.append(" k = {pred, LawRT.key(Tuple.to_list(row))}")
|
|
96
|
+
L.append(" if Map.has_key?(db.seen, k) do")
|
|
97
|
+
L.append(" {false, db}")
|
|
98
|
+
L.append(" else")
|
|
99
|
+
L.append(" {true, %{db | tables: Map.update!(db.tables, pred, &(&1 ++ [row])), seen: Map.put(db.seen, k, true)}}")
|
|
100
|
+
L.append(" end")
|
|
101
|
+
L.append(" end")
|
|
102
|
+
L.append("")
|
|
103
|
+
L.append(" def register(db, pred, row), do: elem(add_row(db, pred, row), 1)")
|
|
104
|
+
L.append("")
|
|
105
|
+
for r in plan["rules"]:
|
|
106
|
+
L.append(f" # {r['id']}")
|
|
107
|
+
L.append(f" defp rule_{r['name']}(db0) do")
|
|
108
|
+
pre = []
|
|
109
|
+
if r["window"]:
|
|
110
|
+
if r["window"].get("start"):
|
|
111
|
+
pre.append(f"LawRT.legal_time() < {estr(r['window']['start'])}")
|
|
112
|
+
if r["window"].get("end"):
|
|
113
|
+
pre.append(f"LawRT.legal_time() >= {estr(r['window']['end'])}")
|
|
114
|
+
ind = " "
|
|
115
|
+
if pre:
|
|
116
|
+
L.append(f"{ind}if {' or '.join(pre)} do")
|
|
117
|
+
L.append(f"{ind} {{false, db0}}")
|
|
118
|
+
L.append(f"{ind}else")
|
|
119
|
+
ind += " "
|
|
120
|
+
n = len(r["atoms"])
|
|
121
|
+
for i, a in enumerate(r["atoms"]):
|
|
122
|
+
gprev = f"g{i - 1}" if i > 0 else "false"
|
|
123
|
+
L.append(f"{ind}Enum.reduce(rows(db{i}, {estr(a['pred'])}), {{{gprev}, db{i}}}, fn r{i}, {{g{i}, db{i + 1}}} ->")
|
|
124
|
+
ind += " "
|
|
125
|
+
checks = [f'LawRT.cmp(elem(r{ai}, {aj}), "eq", elem(r{bi}, {bj}))'
|
|
126
|
+
for (ai, aj, _at), (bi, bj, _bt) in r["joins"]]
|
|
127
|
+
checks += [f'LawRT.eq(elem(r{f["row"]}, {f["col"]}), {term(f["term"])})' for f in r["filters"]]
|
|
128
|
+
checks += [f'LawRT.cmp({term(g["l"])}, {estr(g["op"])}, {term(g["r"])})' for g in r["guards"]]
|
|
129
|
+
gname = f"g{n - 1}" if n else "false"
|
|
130
|
+
dbname = f"db{n}"
|
|
131
|
+
args = ", ".join(term(a) for a in r["head"]["args"])
|
|
132
|
+
add = f"case add_row({dbname}, {estr(r['head']['pred'])}, {{{args}}}) do {{true, d}} -> {{true, d}}; {{false, d}} -> {{{gname}, d}} end"
|
|
133
|
+
if checks:
|
|
134
|
+
L.append(f"{ind}cond do")
|
|
135
|
+
for c in checks:
|
|
136
|
+
L.append(f"{ind} not {c} -> {{{gname}, {dbname}}}")
|
|
137
|
+
L.append(f"{ind} true -> {add}")
|
|
138
|
+
L.append(f"{ind}end")
|
|
139
|
+
else:
|
|
140
|
+
L.append(f"{ind}{add}")
|
|
141
|
+
for _ in range(n):
|
|
142
|
+
ind = ind[:-2]
|
|
143
|
+
L.append(f"{ind}end)")
|
|
144
|
+
if pre:
|
|
145
|
+
ind = ind[:-2]
|
|
146
|
+
L.append(f"{ind}end")
|
|
147
|
+
L.append(" end")
|
|
148
|
+
L.append("")
|
|
149
|
+
names = [r["name"] for r in plan["rules"]]
|
|
150
|
+
L.append(" def saturate(db0) do")
|
|
151
|
+
if names:
|
|
152
|
+
L.append(" {grew, db} = Enum.reduce([" + ", ".join(f"&rule_{n}/1" for n in names) + "], {false, db0}, fn rule, {g, d} -> {g2, d2} = rule.(d); {g or g2, d2} end)")
|
|
153
|
+
L.append(" if grew, do: saturate(db), else: db")
|
|
154
|
+
else:
|
|
155
|
+
L.append(" db0")
|
|
156
|
+
L.append(" end")
|
|
157
|
+
L.append("")
|
|
158
|
+
L.append(" def package_facts(db0) do")
|
|
159
|
+
if plan["facts"]:
|
|
160
|
+
L.append(" Enum.reduce([")
|
|
161
|
+
L.append(",\n".join(f" {{{estr(f['pred'])}, {{" + ", ".join(term(a) for a in f["args"]) + "}}" for f in plan["facts"]))
|
|
162
|
+
L.append(" ], db0, fn {p, row}, db -> register(db, p, row) end)")
|
|
163
|
+
else:
|
|
164
|
+
L.append(" db0")
|
|
165
|
+
L.append(" end")
|
|
166
|
+
L.append("end")
|
|
167
|
+
return "\n".join(L) + "\n"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# --- дескриптор цели ---------------------------------------------------------
|
|
171
|
+
HERE = Path(__file__).resolve().parent
|
|
172
|
+
EXT = "ex"
|
|
173
|
+
MODEL = "model.ex"
|
|
174
|
+
MAINS = {"runner": "runner.exs", "cert": "cert.exs"}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def available():
|
|
178
|
+
ex = shutil.which("elixir")
|
|
179
|
+
return (True, ex) if ex else (False, "нет elixir в PATH")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def layout(proj, main: str) -> list:
|
|
183
|
+
proj = Path(proj)
|
|
184
|
+
proj.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
out = []
|
|
186
|
+
for name in ("runtime.ex", MAINS[main]):
|
|
187
|
+
shutil.copyfile(HERE / name, proj / name)
|
|
188
|
+
out.append(proj / name)
|
|
189
|
+
return out
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def build(proj):
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def run(proj, main: str):
|
|
197
|
+
return ["elixir", str(Path(proj) / MAINS[main])], None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
sys.path.insert(0, str(HERE.parents[1]))
|
|
202
|
+
import plan as P
|
|
203
|
+
ir = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
204
|
+
Path(sys.argv[2]).write_text(emit(P.build_plan(ir)), encoding="utf-8")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Раннер Elixir-бэкенда: тот же вход и тот же ответ, что у runner.py.
|
|
2
|
+
#
|
|
3
|
+
# Протокол выхода общий для всех целей: ответ — JSON и код 0; названный отказ
|
|
4
|
+
# среза — JSON {"refused": текст} и код 3; любой другой код — крах.
|
|
5
|
+
Code.require_file("runtime.ex", __DIR__)
|
|
6
|
+
Code.require_file("model.ex", __DIR__)
|
|
7
|
+
|
|
8
|
+
defmodule LawRunner do
|
|
9
|
+
defp text(v) when is_binary(v), do: v
|
|
10
|
+
defp text(v) when is_integer(v), do: Integer.to_string(v)
|
|
11
|
+
defp text(nil), do: ""
|
|
12
|
+
defp text(v), do: JSON.encode!(v)
|
|
13
|
+
|
|
14
|
+
def arg(a) do
|
|
15
|
+
case a["kind"] do
|
|
16
|
+
"entity_ref" -> {:text, a["id"]}
|
|
17
|
+
"const_ref" ->
|
|
18
|
+
# §54: разыменование предшествует исполнению; член перечисления §38 либо
|
|
19
|
+
# имя без объявления остаётся идентичностью, как в ключе атома оракула.
|
|
20
|
+
case LawModel.consts(a["id"]) do
|
|
21
|
+
nil -> LawRT.sym(a["id"])
|
|
22
|
+
{_, v} -> v
|
|
23
|
+
end
|
|
24
|
+
"value" ->
|
|
25
|
+
t = get_in(a, ["type", "name"])
|
|
26
|
+
v = text(a["value"])
|
|
27
|
+
case t do
|
|
28
|
+
"urn:law:std#Money" -> LawRT.money(v, a["currency"])
|
|
29
|
+
"urn:law:std#Decimal" -> LawRT.dec(v)
|
|
30
|
+
"urn:law:std#Integer" -> LawRT.dec(v)
|
|
31
|
+
"urn:law:std#Text" -> {:text, v}
|
|
32
|
+
"urn:law:std#Date" -> LawRT.date(v)
|
|
33
|
+
_ -> LawRT.refuse(["аргумент вне среза раннера: ", text(t)])
|
|
34
|
+
end
|
|
35
|
+
k -> LawRT.refuse(["аргумент вне среза раннера: ", text(k)])
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Тип аргумента, если известен: у value — из узла, у const_ref — из объявления
|
|
40
|
+
# константы; entity_ref типа не несёт (как у оракула).
|
|
41
|
+
def arg_type(a) do
|
|
42
|
+
case a["kind"] do
|
|
43
|
+
"value" -> get_in(a, ["type", "name"])
|
|
44
|
+
"const_ref" -> (case LawModel.consts(a["id"]) do nil -> nil; {t, _} -> t end)
|
|
45
|
+
_ -> nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Совпадение типа с наследованием сущностей вверх от фактического.
|
|
50
|
+
def conforms(actual, expected, seen \\ %{})
|
|
51
|
+
def conforms(nil, _, _), do: false
|
|
52
|
+
def conforms(expected, expected, _), do: true
|
|
53
|
+
def conforms(cur, expected, seen) do
|
|
54
|
+
if Map.has_key?(seen, cur), do: false, else: conforms(LawModel.base(cur), expected, Map.put(seen, cur, true))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def typed(params, args) do
|
|
58
|
+
length(params) == length(args) and
|
|
59
|
+
Enum.all?(Enum.zip(params, args), fn {e, a} -> (case arg_type(a) do nil -> true; t -> conforms(t, e) end) end)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def key_conflict_blocks(db, predicate), do: key_closure(LawModel.key_conflict_predicates(db), predicate, MapSet.new())
|
|
63
|
+
defp key_closure([], predicate, seen), do: MapSet.member?(seen, predicate)
|
|
64
|
+
defp key_closure([source | waiting], predicate, seen) do
|
|
65
|
+
if MapSet.member?(seen, source), do: key_closure(waiting, predicate, seen),
|
|
66
|
+
else: key_closure(waiting ++ LawModel.key_readers(source), predicate, MapSet.put(seen, source))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def run(case_path, query_path) do
|
|
70
|
+
kase = JSON.decode!(File.read!(case_path))
|
|
71
|
+
query = JSON.decode!(File.read!(query_path))
|
|
72
|
+
LawRT.set_legal_time(text(get_in(kase, ["context", "legalTime"])))
|
|
73
|
+
db0 = LawModel.package_facts(LawModel.new())
|
|
74
|
+
{db1, issues} =
|
|
75
|
+
Enum.reduce(kase["assertions"] || [], {db0, []}, fn a, {db, iss} ->
|
|
76
|
+
lit = a["literal"]
|
|
77
|
+
if lit["polarity"] != "positive", do: LawRT.refuse("§113: отрицательный факт вне L0-среза")
|
|
78
|
+
pred = lit["predicate"]
|
|
79
|
+
args = lit["args"] || []
|
|
80
|
+
case LawModel.params(pred) do
|
|
81
|
+
nil -> {db, iss}
|
|
82
|
+
params ->
|
|
83
|
+
# Арность и типы аргументов утверждения против сигнатуры §189 (E-0046).
|
|
84
|
+
if typed(params, args),
|
|
85
|
+
do: {LawModel.register(db, pred, List.to_tuple(Enum.map(args, &arg/1))), iss},
|
|
86
|
+
else: {db, iss ++ ["TYPE_ERROR"]}
|
|
87
|
+
end
|
|
88
|
+
end)
|
|
89
|
+
db2 = LawModel.saturate(db1)
|
|
90
|
+
issues = issues ++ List.duplicate("KEY_CONFLICT", LawModel.key_conflicts(db2))
|
|
91
|
+
if query["kind"] != "truth", do: LawRT.refuse(["вид запроса вне среза раннера: ", text(query["kind"])])
|
|
92
|
+
qlit = query["literal"]
|
|
93
|
+
qargs = qlit["args"] || []
|
|
94
|
+
# Ключ атома у оракула §208 включает ТИП значения: несогласованный запрос
|
|
95
|
+
# атома не находит — NEITHER без issue.
|
|
96
|
+
hit =
|
|
97
|
+
case LawModel.params(qlit["predicate"]) do
|
|
98
|
+
nil -> false
|
|
99
|
+
qparams ->
|
|
100
|
+
if typed(qparams, qargs) do
|
|
101
|
+
want = Enum.map(qargs, &arg/1)
|
|
102
|
+
Enum.any?(LawModel.rows(db2, qlit["predicate"]), fn row ->
|
|
103
|
+
r = Tuple.to_list(row)
|
|
104
|
+
length(r) == length(want) and Enum.all?(Enum.zip(r, want), fn {v, w} -> LawRT.eq(v, w) end)
|
|
105
|
+
end)
|
|
106
|
+
else
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
# Открытый мир: отсутствие строки — это NEITHER, а не FALSE_ONLY.
|
|
111
|
+
evaluation = if key_conflict_blocks(db2, qlit["predicate"]), do: "NON_EXECUTABLE", else: "COMPUTED"
|
|
112
|
+
IO.puts(JSON.encode!(%{"queryId" => query["queryId"], "truthStatus" => (if hit, do: "TRUE_ONLY", else: "NEITHER"),
|
|
113
|
+
"evaluationStatus" => evaluation, "issues" => issues}))
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
[case_path, query_path] = System.argv()
|
|
118
|
+
try do
|
|
119
|
+
LawRunner.run(case_path, query_path)
|
|
120
|
+
catch
|
|
121
|
+
{:refused, msg} ->
|
|
122
|
+
IO.puts(JSON.encode!(%{"refused" => msg}))
|
|
123
|
+
System.halt(3)
|
|
124
|
+
end
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Рантайм L0 для Elixir. Тот же контракт, что у runtime.py: точная десятичная
|
|
2
|
+
# арифметика §58 на встроенных целых, деньги с валютой, названный режим
|
|
3
|
+
# округления, календарный день, член перечисления. Отказ — throw({:refused, msg}),
|
|
4
|
+
# любая другая ошибка — крах.
|
|
5
|
+
defmodule LawRT do
|
|
6
|
+
import Kernel, except: [max: 2, min: 2]
|
|
7
|
+
# Значения: {:dec, u, s} | {:money, u, s, cur} | {:text, t} | {:date, d} | {:sym, id}.
|
|
8
|
+
def refuse(msg), do: throw({:refused, IO.iodata_to_binary(msg)})
|
|
9
|
+
|
|
10
|
+
defp p10(n), do: Integer.pow(10, n)
|
|
11
|
+
|
|
12
|
+
def dec(lit) when is_binary(lit) do
|
|
13
|
+
{i, f} = case String.split(lit, ".", parts: 2) do [a, b] -> {a, b}; [a] -> {a, ""} end
|
|
14
|
+
neg = String.starts_with?(i, "-")
|
|
15
|
+
digits = (if neg, do: String.slice(i, 1..-1//1), else: i) <> f
|
|
16
|
+
u = String.to_integer(digits)
|
|
17
|
+
{:dec, (if neg, do: -u, else: u), String.length(f)}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def money(lit, cur), do: (({:dec, u, s} = dec(lit)); {:money, u, s, cur})
|
|
21
|
+
def date(lit), do: {:date, lit}
|
|
22
|
+
def sym(id), do: {:sym, id}
|
|
23
|
+
|
|
24
|
+
# --- виды -------------------------------------------------------------------
|
|
25
|
+
defp num({:dec, u, s}), do: {u, s, nil}
|
|
26
|
+
defp num({:money, u, s, c}), do: {u, s, c}
|
|
27
|
+
defp num(_), do: nil
|
|
28
|
+
|
|
29
|
+
defp money?({:money, _, _, _}), do: true
|
|
30
|
+
defp money?(_), do: false
|
|
31
|
+
|
|
32
|
+
# §48/§58: сложение, вычитание, min/max требуют совпадения видов; у денег — и валюты.
|
|
33
|
+
defp same!(a, b, op) do
|
|
34
|
+
case {num(a), num(b)} do
|
|
35
|
+
{{_, _, ca}, {_, _, cb}} when is_nil(ca) == is_nil(cb) ->
|
|
36
|
+
if ca != nil and ca != cb, do: refuse(["§58: разные валюты: ", op])
|
|
37
|
+
_ -> refuse(["§58: виды операндов не совпадают: ", op])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
defp pair(a, b) do
|
|
42
|
+
{au, as, _} = num(a); {bu, bs, _} = num(b)
|
|
43
|
+
s = Kernel.max(as, bs)
|
|
44
|
+
{au * p10(s - as), bu * p10(s - bs), s}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
defp mk(u, s, {:money, _, _, c}), do: {:money, u, s, c}
|
|
48
|
+
defp mk(u, s, _), do: {:dec, u, s}
|
|
49
|
+
|
|
50
|
+
# Целое значение: дробная часть нулевая (защита в глубину под статикой плана).
|
|
51
|
+
defp integral({:dec, u, s}), do: (if rem(u, p10(s)) == 0, do: {:ok, div(u, p10(s))}, else: nil)
|
|
52
|
+
defp integral(_), do: nil
|
|
53
|
+
|
|
54
|
+
# --- арифметика §58 ---------------------------------------------------------
|
|
55
|
+
def add(a, b), do: (same!(a, b, "add"); ({x, y, s} = pair(a, b)); mk(x + y, s, a))
|
|
56
|
+
def sub(a, b), do: (same!(a, b, "sub"); ({x, y, s} = pair(a, b)); mk(x - y, s, a))
|
|
57
|
+
|
|
58
|
+
# Правило скаляра §49: безразмерное масштабирует размерное; масштаб результата —
|
|
59
|
+
# сумма масштабов. Два размерных не перемножаются.
|
|
60
|
+
def mul(a, b) do
|
|
61
|
+
case {num(a), num(b)} do
|
|
62
|
+
{{au, as, ca}, {bu, bs, cb}} ->
|
|
63
|
+
if ca != nil and cb != nil, do: refuse("§58: произведение двух размерных величин не определено")
|
|
64
|
+
mk(au * bu, as + bs, (if ca != nil, do: a, else: b))
|
|
65
|
+
_ -> refuse("§58: виды операндов не совпадают: mul")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
defp strip(d, p, n), do: (if rem(d, p) == 0, do: strip(div(d, p), p, n + 1), else: {d, n})
|
|
70
|
+
|
|
71
|
+
# Точное десятичное частное num/den при 10-гладком знаменателе (§18), иначе nil.
|
|
72
|
+
defp exact(num0, den0) do
|
|
73
|
+
g = case Integer.gcd(num0, den0) do 0 -> 1; x -> x end
|
|
74
|
+
{num1, den1} = {div(num0, g), div(den0, g)}
|
|
75
|
+
{num, den} = if den1 < 0, do: {-num1, -den1}, else: {num1, den1}
|
|
76
|
+
{d2, twos} = strip(den, 2, 0)
|
|
77
|
+
{d5, fives} = strip(d2, 5, 0)
|
|
78
|
+
if d5 == 1 do
|
|
79
|
+
k = Kernel.max(twos, fives)
|
|
80
|
+
{num * Integer.pow(2, k - twos) * Integer.pow(5, k - fives), k}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Таблица делимых §58 в срезе: Decimal/Decimal, Money/Decimal, Money/Money одной
|
|
85
|
+
# валюты. Integer/Integer даёт Rational — вне среза, отвергается планом статически.
|
|
86
|
+
def divide(a, b) do
|
|
87
|
+
case {num(a), num(b)} do
|
|
88
|
+
{{au, as, ca}, {bu, bs, cb}} ->
|
|
89
|
+
if bu == 0, do: refuse("деление на ноль — evaluation error (§58)")
|
|
90
|
+
like = case {ca, cb} do
|
|
91
|
+
{nil, nil} -> nil
|
|
92
|
+
{_, nil} -> a
|
|
93
|
+
{nil, _} -> refuse("§58: деление вне таблицы делимых")
|
|
94
|
+
{c, c} -> nil
|
|
95
|
+
_ -> refuse("CURRENCY_MISMATCH: §58 делит Money на Money только при тождественной валюте")
|
|
96
|
+
end
|
|
97
|
+
case exact(au * p10(bs), bu * p10(as)) do
|
|
98
|
+
nil when like != nil -> refuse("INEXACT_DIVISION: округление только явной политикой (§50/§18)")
|
|
99
|
+
nil -> refuse("§58: частное не 10-гладкое — Rational вне среза")
|
|
100
|
+
{u, s} when is_nil(like) -> {:dec, u, s}
|
|
101
|
+
{u, s} -> mk(u, s, like)
|
|
102
|
+
end
|
|
103
|
+
_ -> refuse("§58: деление вне таблицы делимых")
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Евклидов остаток §58: 0 <= r < |b| при любых знаках операндов.
|
|
108
|
+
def modulo(a, b) do
|
|
109
|
+
case {integral(a), integral(b)} do
|
|
110
|
+
{{:ok, _}, {:ok, 0}} -> refuse("остаток от деления на ноль — evaluation error (§58)")
|
|
111
|
+
{{:ok, x}, {:ok, y}} -> {:dec, Integer.mod(x, abs(y)), 0}
|
|
112
|
+
_ -> refuse("§58: mod определён только для Integer")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def min(a, b), do: (same!(a, b, "min"); ({x, y, _} = pair(a, b)); (if x <= y, do: a, else: b))
|
|
117
|
+
def max(a, b), do: (same!(a, b, "max"); ({x, y, _} = pair(a, b)); (if x >= y, do: a, else: b))
|
|
118
|
+
|
|
119
|
+
def absolute(v) do
|
|
120
|
+
case num(v) do
|
|
121
|
+
{u, s, _} -> mk(abs(u), s, v)
|
|
122
|
+
nil -> refuse("§58: abs — не числовой вид")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# §58 (E-0075): только Integer pow Integer; порядок проверок — знак, предел,
|
|
127
|
+
# вычисление, диапазон — часть контракта.
|
|
128
|
+
def pow(base, exp) do
|
|
129
|
+
case {integral(base), integral(exp)} do
|
|
130
|
+
{{:ok, x}, {:ok, e}} ->
|
|
131
|
+
if e < 0, do: refuse("§58: pow — показатель отрицателен")
|
|
132
|
+
if e > 4096, do: refuse("RESOURCE_LIMIT: pow — показатель превышает 4096")
|
|
133
|
+
r = Integer.pow(x, e)
|
|
134
|
+
if r < -Integer.pow(2, 63) or r > Integer.pow(2, 63) - 1, do: refuse("RESOURCE_LIMIT: pow — результат вне 64-битного диапазона")
|
|
135
|
+
{:dec, r, 0}
|
|
136
|
+
_ -> refuse("§58: pow определён только для Integer")
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def round(m, digits, mode) do
|
|
141
|
+
if mode != "HALF_UP", do: refuse(["§50: режим округления не реализован: ", mode])
|
|
142
|
+
case num(m) do
|
|
143
|
+
nil -> refuse("§50: round — не числовой вид")
|
|
144
|
+
{mu, ms, _} ->
|
|
145
|
+
n = case num(digits) do {du, ds, _} -> div(du, p10(ds)); nil -> 0 end
|
|
146
|
+
if ms <= n do
|
|
147
|
+
mk(mu * p10(n - ms), n, m)
|
|
148
|
+
else
|
|
149
|
+
drop = p10(ms - n)
|
|
150
|
+
a = abs(mu); q = div(a, drop); r = rem(a, drop)
|
|
151
|
+
up = if r * 2 >= drop, do: q + 1, else: q
|
|
152
|
+
mk((if mu < 0, do: -up, else: up), n, m)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# --- сравнение и равенство ---------------------------------------------------
|
|
158
|
+
defp ordered("lt", x, y), do: x < y
|
|
159
|
+
defp ordered("le", x, y), do: x <= y
|
|
160
|
+
defp ordered("gt", x, y), do: x > y
|
|
161
|
+
defp ordered("ge", x, y), do: x >= y
|
|
162
|
+
defp ordered("eq", x, y), do: x == y
|
|
163
|
+
defp ordered("ne", x, y), do: x != y
|
|
164
|
+
defp ordered(op, _, _), do: refuse(["сравнение вне среза рантайма: ", op])
|
|
165
|
+
|
|
166
|
+
def cmp(a, op, b) do
|
|
167
|
+
case {a, b} do
|
|
168
|
+
{{:sym, x}, {:sym, y}} -> (case op do "eq" -> x == y; "ne" -> x != y; _ -> refuse("§57: вид enum не упорядочен") end)
|
|
169
|
+
{{:sym, _}, _} -> (case op do "eq" -> false; "ne" -> true; _ -> refuse("§57: вид enum не упорядочен") end)
|
|
170
|
+
{_, {:sym, _}} -> (case op do "eq" -> false; "ne" -> true; _ -> refuse("§57: вид enum не упорядочен") end)
|
|
171
|
+
{{:date, x}, {:date, y}} -> ordered(op, x, y)
|
|
172
|
+
{{:date, _}, _} -> (case op do "eq" -> false; "ne" -> true; _ -> refuse("§57: Date сравнивается только с Date") end)
|
|
173
|
+
{_, {:date, _}} -> (case op do "eq" -> false; "ne" -> true; _ -> refuse("§57: Date сравнивается только с Date") end)
|
|
174
|
+
{{:text, x}, {:text, y}} -> (case op do "eq" -> x == y; "ne" -> x != y; _ -> refuse(["порядок на Text вне среза рантайма: ", op]) end)
|
|
175
|
+
{{:text, _}, _} -> (case op do "eq" -> false; "ne" -> true; _ -> refuse(["порядок на Text вне среза рантайма: ", op]) end)
|
|
176
|
+
{_, {:text, _}} -> (case op do "eq" -> false; "ne" -> true; _ -> refuse(["порядок на Text вне среза рантайма: ", op]) end)
|
|
177
|
+
_ ->
|
|
178
|
+
if money?(a) != money?(b), do: refuse("§49: сравнение денег с безразмерным")
|
|
179
|
+
if money?(a) and elem(a, 3) != elem(b, 3), do: refuse("§49: разные валюты")
|
|
180
|
+
{x, y, _} = pair(a, b)
|
|
181
|
+
ordered(op, x, y)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def eq({:sym, x}, {:sym, y}), do: x == y
|
|
186
|
+
def eq({:sym, _}, _), do: false
|
|
187
|
+
def eq(_, {:sym, _}), do: false
|
|
188
|
+
def eq({:date, x}, {:date, y}), do: x == y
|
|
189
|
+
def eq({:date, _}, _), do: false
|
|
190
|
+
def eq(_, {:date, _}), do: false
|
|
191
|
+
def eq({:text, x}, {:text, y}), do: x == y
|
|
192
|
+
def eq({:text, _}, _), do: false
|
|
193
|
+
def eq(_, {:text, _}), do: false
|
|
194
|
+
def eq(a, b) do
|
|
195
|
+
if money?(a) != money?(b) or (money?(a) and elem(a, 3) != elem(b, 3)) do
|
|
196
|
+
false
|
|
197
|
+
else
|
|
198
|
+
{x, y, _} = pair(a, b)
|
|
199
|
+
x == y
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
defp norm(u, s) when s > 0 and rem(u, 10) == 0, do: norm(div(u, 10), s - 1)
|
|
204
|
+
defp norm(u, s), do: "#{u}e-#{s}"
|
|
205
|
+
|
|
206
|
+
# Разделитель и экран внутри текста экранируются: иначе ["a|e:b","c"] и
|
|
207
|
+
# ["a","b|e:c"] склеивались в один ключ, и второй факт терялся.
|
|
208
|
+
defp esc(t), do: t |> String.replace("\\", "\\\\") |> String.replace("|", "\\|")
|
|
209
|
+
|
|
210
|
+
defp cell({:text, t}), do: "e:" <> esc(t)
|
|
211
|
+
defp cell({:date, d}), do: "t:" <> d
|
|
212
|
+
defp cell({:sym, i}), do: "c:" <> esc(i)
|
|
213
|
+
defp cell({:money, u, s, c}), do: "m:" <> c <> ":" <> norm(u, s)
|
|
214
|
+
defp cell({:dec, u, s}), do: "d:" <> norm(u, s)
|
|
215
|
+
|
|
216
|
+
def key(row), do: Enum.map_join(row, "|", &cell/1)
|
|
217
|
+
|
|
218
|
+
def set_legal_time(t), do: Process.put(:law_legal_time, t)
|
|
219
|
+
|
|
220
|
+
def legal_time do
|
|
221
|
+
case Process.get(:law_legal_time) do
|
|
222
|
+
nil -> refuse("legalTime не подан делом")
|
|
223
|
+
"" -> refuse("legalTime не подан делом")
|
|
224
|
+
t -> t
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|