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,216 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Эмиттер Erlang. Модель — модуль `model` с чистым состоянием таблиц: правила
|
|
3
|
+
принимают Db и возвращают {Grew, Db}; порядок строк — порядок добавления."""
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
SUPPORTED_PLAN = "law-backend-plan/0.7"
|
|
10
|
+
OPS = {"add": "add", "sub": "sub", "mul": "mul", "div": "'div'", "mod": "'mod'"}
|
|
11
|
+
FNS = {"round": "round", "min": "min", "max": "max", "abs": "'abs'", "pow": "pow"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check_plan(plan: dict) -> None:
|
|
15
|
+
v = plan.get("planVersion")
|
|
16
|
+
if v != SUPPORTED_PLAN:
|
|
17
|
+
raise ValueError(f"версия плана не поддержана эмиттером: {v!r} (поддержана {SUPPORTED_PLAN})")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def ebin(s: str) -> str:
|
|
21
|
+
"""Бинарная строка Erlang с UTF-8: json.dumps даёт экранирование, совместимое
|
|
22
|
+
с синтаксисом <<"...">> при ensure_ascii=False, кроме escape-последовательностей
|
|
23
|
+
JSON, которых в идентификаторах и литералах плана не встречается."""
|
|
24
|
+
return "<<" + json.dumps(s, ensure_ascii=False) + "/utf8>>"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def term(t: dict) -> str:
|
|
28
|
+
k = t["k"]
|
|
29
|
+
if k == "var":
|
|
30
|
+
return f'element({t["col"] + 1}, R{t["row"]})'
|
|
31
|
+
if k == "lit":
|
|
32
|
+
if t["t"] == "money":
|
|
33
|
+
return f'rt:money({ebin(t["v"])}, {ebin(t["cur"])})'
|
|
34
|
+
if t["t"] == "dec":
|
|
35
|
+
return f'rt:dec({ebin(t["v"])})'
|
|
36
|
+
if t["t"] == "date":
|
|
37
|
+
return f'rt:date({ebin(t["v"])})'
|
|
38
|
+
if t["t"] == "enum":
|
|
39
|
+
return f'rt:sym({ebin(t["v"])})'
|
|
40
|
+
return f'{{text, {ebin(t["v"])}}}'
|
|
41
|
+
if k == "bin":
|
|
42
|
+
return f'rt:{OPS[t["op"]]}({term(t["l"])}, {term(t["r"])})'
|
|
43
|
+
if k == "call":
|
|
44
|
+
args = t["args"]
|
|
45
|
+
if t["fn"] == "round":
|
|
46
|
+
return f'rt:round({term(args[0])}, {term(args[1])}, {ebin(args[2]["v"])})'
|
|
47
|
+
return f'rt:{FNS[t["fn"]]}(' + ", ".join(term(a) for a in args) + ')'
|
|
48
|
+
raise AssertionError(k)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def emit(plan: dict) -> str:
|
|
52
|
+
check_plan(plan)
|
|
53
|
+
L = ["-module(model).",
|
|
54
|
+
"-export([new/0, rows/2, params/1, consts/1, base/1, keys/1, key_conflicts/1, key_conflict_predicates/1, key_readers/1, register/3, saturate/1, package_facts/1]).", ""]
|
|
55
|
+
L.append("new() -> #{tables => #{" + ", ".join(f"{ebin(t['pred'])} => []" for t in plan["tables"]) + "}, seen => #{}}.")
|
|
56
|
+
L.append("")
|
|
57
|
+
L.append("rows(#{tables := T}, Pred) -> maps:get(Pred, T, []).")
|
|
58
|
+
L.append("")
|
|
59
|
+
L.append("%% Объявленные типы параметров: раннер сверяет с ними аргументы дела (§189).")
|
|
60
|
+
for t in plan["tables"]:
|
|
61
|
+
L.append(f"params({ebin(t['pred'])}) -> [" + ", ".join(ebin(p) for p in t["params"]) + "];")
|
|
62
|
+
L.append("params(_) -> undefined.")
|
|
63
|
+
L.append("")
|
|
64
|
+
L.append("%% Константы §54: тип для проверки §189 и значение для разыменования аргументов дела.")
|
|
65
|
+
for cid, c in plan["consts"].items():
|
|
66
|
+
L.append(f"consts({ebin(cid)}) -> {{{ebin(c['type'])}, {term(c['term'])}}};")
|
|
67
|
+
L.append("consts(_) -> undefined.")
|
|
68
|
+
L.append("")
|
|
69
|
+
L.append("%% Наследование сущностей: подтип подходит параметру базового типа.")
|
|
70
|
+
for sub, base in plan["bases"].items():
|
|
71
|
+
L.append(f"base({ebin(sub)}) -> {ebin(base)};")
|
|
72
|
+
L.append("base(_) -> undefined.")
|
|
73
|
+
L.append("")
|
|
74
|
+
L.append("%% Ключи отношений §45.1: позиции столбцов каждого ключа (с единицы).")
|
|
75
|
+
for t in plan["tables"]:
|
|
76
|
+
if t["keys"]:
|
|
77
|
+
L.append(f"keys({ebin(t['pred'])}) -> [" + ", ".join("[" + ", ".join(str(i + 1) for i in k) + "]" for k in t["keys"]) + "];")
|
|
78
|
+
L.append("keys(_) -> [].")
|
|
79
|
+
L.append("")
|
|
80
|
+
L.append("%% KEY_CONFLICT §45.1/§196: группы строк одной таблицы с одинаковой проекцией на ключ; строки уникальны по ключу строки, поэтому группа из двух и более строк — различные accepted-кортежи. Считается после насыщения.")
|
|
81
|
+
L.append("key_conflicts(#{tables := T} = Db) ->")
|
|
82
|
+
L.append(" lists:sum([key_conflicts(Db, Pred, Key) || Pred <- maps:keys(T), Key <- keys(Pred)]).")
|
|
83
|
+
L.append("")
|
|
84
|
+
L.append("key_conflicts(Db, Pred, Key) ->")
|
|
85
|
+
L.append(" Groups = lists:foldl(fun(Row, G) ->")
|
|
86
|
+
L.append(" P = rt:key([element(I, Row) || I <- Key]),")
|
|
87
|
+
L.append(" maps:update_with(P, fun(C) -> C + 1 end, 1, G)")
|
|
88
|
+
L.append(" end, #{}, rows(Db, Pred)),")
|
|
89
|
+
L.append(" length([C || C <- maps:values(Groups), C > 1]).")
|
|
90
|
+
L.append("")
|
|
91
|
+
L.append("key_conflict_predicates(#{tables := T} = Db) ->")
|
|
92
|
+
L.append(" [Pred || Pred <- maps:keys(T), lists:any(fun(Key) -> key_conflicts(Db, Pred, Key) > 0 end, keys(Pred))].")
|
|
93
|
+
L.append("")
|
|
94
|
+
readers: dict[str, set[str]] = {}
|
|
95
|
+
for rule in plan["rules"]:
|
|
96
|
+
head = rule["head"]["pred"]
|
|
97
|
+
for atom in rule.get("atoms") or []:
|
|
98
|
+
readers.setdefault(atom["pred"], set()).add(head)
|
|
99
|
+
for check in rule.get("checks") or []:
|
|
100
|
+
readers.setdefault(check["pred"], set()).add(head)
|
|
101
|
+
for pred, heads in sorted(readers.items()):
|
|
102
|
+
L.append(f"key_readers({ebin(pred)}) -> [" + ", ".join(ebin(head) for head in sorted(heads)) + "];" )
|
|
103
|
+
L.append("key_readers(_) -> [].")
|
|
104
|
+
L.append("")
|
|
105
|
+
L.append("add_row(#{tables := T, seen := Seen} = Db, Pred, Row) ->")
|
|
106
|
+
L.append(" K = {Pred, rt:key(tuple_to_list(Row))},")
|
|
107
|
+
L.append(" case maps:is_key(K, Seen) of")
|
|
108
|
+
L.append(" true -> {false, Db};")
|
|
109
|
+
L.append(" false -> {true, Db#{tables := T#{Pred := maps:get(Pred, T) ++ [Row]}, seen := Seen#{K => true}}}")
|
|
110
|
+
L.append(" end.")
|
|
111
|
+
L.append("")
|
|
112
|
+
L.append("register(Db, Pred, Row) -> {_, Db2} = add_row(Db, Pred, Row), Db2.")
|
|
113
|
+
L.append("")
|
|
114
|
+
for r in plan["rules"]:
|
|
115
|
+
L.append(f"%% {r['id']}")
|
|
116
|
+
L.append(f"rule_{r['name']}(Db0) ->")
|
|
117
|
+
pre = []
|
|
118
|
+
if r["window"]:
|
|
119
|
+
if r["window"].get("start"):
|
|
120
|
+
pre.append(f"rt:legal_time() < {ebin(r['window']['start'])}")
|
|
121
|
+
if r["window"].get("end"):
|
|
122
|
+
pre.append(f"rt:legal_time() >= {ebin(r['window']['end'])}")
|
|
123
|
+
if pre:
|
|
124
|
+
L.append(" case " + " orelse ".join(pre) + " of true -> {false, Db0}; false ->")
|
|
125
|
+
n = len(r["atoms"])
|
|
126
|
+
# вложенные свёртки: снимок строк каждой таблицы берётся из Db на входе в цикл
|
|
127
|
+
body = []
|
|
128
|
+
ind = " "
|
|
129
|
+
for i, a in enumerate(r["atoms"]):
|
|
130
|
+
body.append(f"{ind}lists:foldl(fun(R{i}, {{G{i}, Db{i + 1}}}) ->")
|
|
131
|
+
ind += " "
|
|
132
|
+
checks = [f'rt:cmp(element({aj + 1}, R{ai}), <<"eq">>, element({bj + 1}, R{bi}))'
|
|
133
|
+
for (ai, aj, _at), (bi, bj, _bt) in r["joins"]]
|
|
134
|
+
checks += [f'rt:eq(element({f["col"] + 1}, R{f["row"]}), {term(f["term"])})' for f in r["filters"]]
|
|
135
|
+
checks += [f'rt:cmp({term(g["l"])}, {ebin(g["op"])}, {term(g["r"])})' for g in r["guards"]]
|
|
136
|
+
gname = f"G{n - 1}" if n else "false"
|
|
137
|
+
dbname = f"Db{n}"
|
|
138
|
+
for c in checks:
|
|
139
|
+
body.append(f"{ind}case {c} of false -> {{{gname}, {dbname}}}; true ->")
|
|
140
|
+
ind += " "
|
|
141
|
+
args = ", ".join(term(a) for a in r["head"]["args"])
|
|
142
|
+
body.append(f"{ind}case add_row({dbname}, {ebin(r['head']['pred'])}, list_to_tuple([{args}])) of")
|
|
143
|
+
body.append(f"{ind} {{true, DbN}} -> {{true, DbN}};")
|
|
144
|
+
body.append(f"{ind} {{false, DbN}} -> {{{gname}, DbN}}")
|
|
145
|
+
body.append(f"{ind}end")
|
|
146
|
+
for _ in checks:
|
|
147
|
+
ind = ind[:-4]
|
|
148
|
+
body.append(f"{ind}end")
|
|
149
|
+
for i in reversed(range(n)):
|
|
150
|
+
ind = ind[:-4]
|
|
151
|
+
gprev = f"G{i - 1}" if i > 0 else "false"
|
|
152
|
+
dbprev = f"Db{i}"
|
|
153
|
+
body.append(f"{ind}end, {{{gprev}, {dbprev}}}, rows({dbprev}, {ebin(r['atoms'][i]['pred'])}))")
|
|
154
|
+
L += body
|
|
155
|
+
if pre:
|
|
156
|
+
L.append(" end.")
|
|
157
|
+
else:
|
|
158
|
+
L[-1] = L[-1] + "."
|
|
159
|
+
L.append("")
|
|
160
|
+
names = [r["name"] for r in plan["rules"]]
|
|
161
|
+
L.append("saturate(Db0) ->")
|
|
162
|
+
if names:
|
|
163
|
+
L.append(" {Grew, Db} = lists:foldl(fun(Rule, {G, D}) -> {G2, D2} = Rule(D), {G orelse G2, D2} end, {false, Db0},")
|
|
164
|
+
L.append(" [" + ", ".join(f"fun rule_{n}/1" for n in names) + "]),")
|
|
165
|
+
L.append(" case Grew of true -> saturate(Db); false -> Db end.")
|
|
166
|
+
else:
|
|
167
|
+
L.append(" Db0.")
|
|
168
|
+
L.append("")
|
|
169
|
+
L.append("package_facts(Db0) ->")
|
|
170
|
+
if plan["facts"]:
|
|
171
|
+
L.append(" lists:foldl(fun({P, Row}, Db) -> register(Db, P, Row) end, Db0, [")
|
|
172
|
+
L.append(",\n".join(f" {{{ebin(f['pred'])}, list_to_tuple([" + ", ".join(term(a) for a in f["args"]) + "])}" for f in plan["facts"]))
|
|
173
|
+
L.append(" ]).")
|
|
174
|
+
else:
|
|
175
|
+
L.append(" Db0.")
|
|
176
|
+
return "\n".join(L) + "\n"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# --- дескриптор цели ---------------------------------------------------------
|
|
180
|
+
HERE = Path(__file__).resolve().parent
|
|
181
|
+
EXT = "erl"
|
|
182
|
+
MODEL = "model.erl"
|
|
183
|
+
MAINS = {"runner": "runner.erl", "cert": "cert.erl"}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def available():
|
|
187
|
+
erlc, erl = shutil.which("erlc"), shutil.which("erl")
|
|
188
|
+
return (True, erl) if erlc and erl else (False, "нет erlc/erl в PATH")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def layout(proj, main: str) -> list:
|
|
192
|
+
proj = Path(proj)
|
|
193
|
+
proj.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
out = []
|
|
195
|
+
for name in ("rt.erl", MAINS[main]):
|
|
196
|
+
shutil.copyfile(HERE / name, proj / name)
|
|
197
|
+
out.append(proj / name)
|
|
198
|
+
return out
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def build(proj):
|
|
202
|
+
proj = Path(proj)
|
|
203
|
+
(proj / "out").mkdir(exist_ok=True)
|
|
204
|
+
return ["erlc", "-o", "out"] + sorted(p.name for p in proj.glob("*.erl")), str(proj)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def run(proj, main: str):
|
|
208
|
+
module = MAINS[main][:-4]
|
|
209
|
+
return ["erl", "-noshell", "-pa", str(Path(proj) / "out"), "-run", module, "main"], None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
sys.path.insert(0, str(HERE.parents[1]))
|
|
214
|
+
import plan as P
|
|
215
|
+
ir = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
216
|
+
Path(sys.argv[2]).write_text(emit(P.build_plan(ir)), encoding="utf-8")
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
%% Рантайм L0 для Erlang. Тот же контракт, что у runtime.py: точная десятичная
|
|
2
|
+
%% арифметика §58 на встроенных целых, деньги с валютой, названный режим
|
|
3
|
+
%% округления, календарный день, член перечисления. Отказ — throw({refused, Msg}),
|
|
4
|
+
%% любая другая ошибка — крах.
|
|
5
|
+
-module(rt).
|
|
6
|
+
-compile({no_auto_import, [abs/1, max/2, min/2]}).
|
|
7
|
+
-export([dec/1, money/2, date/1, sym/1, add/2, sub/2, mul/2, 'div'/2, 'mod'/2,
|
|
8
|
+
min/2, max/2, 'abs'/1, pow/2, round/3, cmp/3, eq/2, key/1,
|
|
9
|
+
set_legal_time/1, legal_time/0, refuse/1]).
|
|
10
|
+
|
|
11
|
+
%% Значения: {dec, U, S} | {money, U, S, Cur} | {text, T} | {date, D} | {sym, Id}.
|
|
12
|
+
refuse(Msg) -> throw({refused, unicode:characters_to_binary(Msg)}).
|
|
13
|
+
|
|
14
|
+
p10(N) -> pow10(N, 1).
|
|
15
|
+
pow10(0, Acc) -> Acc;
|
|
16
|
+
pow10(N, Acc) -> pow10(N - 1, Acc * 10).
|
|
17
|
+
|
|
18
|
+
dec(Lit) when is_binary(Lit) -> dec(unicode:characters_to_list(Lit));
|
|
19
|
+
dec(Lit) ->
|
|
20
|
+
{I, F} = case string:split(Lit, ".") of [A, B] -> {A, B}; [A] -> {A, ""} end,
|
|
21
|
+
Neg = lists:prefix("-", I),
|
|
22
|
+
Digits = (case Neg of true -> tl(I); false -> I end) ++ F,
|
|
23
|
+
U = list_to_integer(Digits),
|
|
24
|
+
{dec, case Neg of true -> -U; false -> U end, length(F)}.
|
|
25
|
+
|
|
26
|
+
money(Lit, Cur) -> {dec, U, S} = dec(Lit), {money, U, S, unicode:characters_to_binary(Cur)}.
|
|
27
|
+
date(Lit) -> {date, unicode:characters_to_binary(Lit)}.
|
|
28
|
+
sym(Id) -> {sym, unicode:characters_to_binary(Id)}.
|
|
29
|
+
|
|
30
|
+
%% --- виды -------------------------------------------------------------------
|
|
31
|
+
num({dec, U, S}) -> {U, S, none};
|
|
32
|
+
num({money, U, S, C}) -> {U, S, C};
|
|
33
|
+
num(_) -> false.
|
|
34
|
+
|
|
35
|
+
is_money({money, _, _, _}) -> true;
|
|
36
|
+
is_money(_) -> false.
|
|
37
|
+
|
|
38
|
+
%% §48/§58: сложение, вычитание, min/max требуют совпадения видов; у денег — и валюты.
|
|
39
|
+
same(A, B, Op) ->
|
|
40
|
+
case {num(A), num(B)} of
|
|
41
|
+
{{_, _, CA}, {_, _, CB}} when (CA =:= none) =:= (CB =:= none) ->
|
|
42
|
+
case CA =/= none andalso CA =/= CB of
|
|
43
|
+
true -> refuse(["§58: разные валюты: ", Op]);
|
|
44
|
+
false -> ok
|
|
45
|
+
end;
|
|
46
|
+
_ -> refuse(["§58: виды операндов не совпадают: ", Op])
|
|
47
|
+
end.
|
|
48
|
+
|
|
49
|
+
pair(A, B) ->
|
|
50
|
+
{AU, AS, _} = num(A), {BU, BS, _} = num(B),
|
|
51
|
+
S = erlang:max(AS, BS),
|
|
52
|
+
{AU * p10(S - AS), BU * p10(S - BS), S}.
|
|
53
|
+
|
|
54
|
+
mk(U, S, {money, _, _, C}) -> {money, U, S, C};
|
|
55
|
+
mk(U, S, _) -> {dec, U, S}.
|
|
56
|
+
|
|
57
|
+
%% Целое значение: дробная часть нулевая (защита в глубину под статикой плана).
|
|
58
|
+
integral({dec, U, S}) ->
|
|
59
|
+
case U rem p10(S) of 0 -> {ok, U div p10(S)}; _ -> false end;
|
|
60
|
+
integral(_) -> false.
|
|
61
|
+
|
|
62
|
+
%% --- арифметика §58 ---------------------------------------------------------
|
|
63
|
+
add(A, B) -> same(A, B, "add"), {X, Y, S} = pair(A, B), mk(X + Y, S, A).
|
|
64
|
+
sub(A, B) -> same(A, B, "sub"), {X, Y, S} = pair(A, B), mk(X - Y, S, A).
|
|
65
|
+
|
|
66
|
+
%% Правило скаляра §49: безразмерное масштабирует размерное; масштаб результата —
|
|
67
|
+
%% сумма масштабов. Два размерных не перемножаются.
|
|
68
|
+
mul(A, B) ->
|
|
69
|
+
case {num(A), num(B)} of
|
|
70
|
+
{{AU, AS, CA}, {BU, BS, CB}} ->
|
|
71
|
+
case CA =/= none andalso CB =/= none of
|
|
72
|
+
true -> refuse("§58: произведение двух размерных величин не определено");
|
|
73
|
+
false -> ok
|
|
74
|
+
end,
|
|
75
|
+
Like = case CA of none -> B; _ -> A end,
|
|
76
|
+
mk(AU * BU, AS + BS, Like);
|
|
77
|
+
_ -> refuse("§58: виды операндов не совпадают: mul")
|
|
78
|
+
end.
|
|
79
|
+
|
|
80
|
+
gcd(A, 0) -> erlang:abs(A);
|
|
81
|
+
gcd(A, B) -> gcd(B, A rem B).
|
|
82
|
+
|
|
83
|
+
strip(D, P, N) -> case D rem P of 0 -> strip(D div P, P, N + 1); _ -> {D, N} end.
|
|
84
|
+
|
|
85
|
+
%% Точное десятичное частное Num/Den при 10-гладком знаменателе (§18), иначе none.
|
|
86
|
+
exact(Num0, Den0) ->
|
|
87
|
+
G = case gcd(Num0, Den0) of 0 -> 1; X -> X end,
|
|
88
|
+
{Num1, Den1} = {Num0 div G, Den0 div G},
|
|
89
|
+
{Num, Den} = case Den1 < 0 of true -> {-Num1, -Den1}; false -> {Num1, Den1} end,
|
|
90
|
+
{D2, Twos} = strip(Den, 2, 0),
|
|
91
|
+
{D5, Fives} = strip(D2, 5, 0),
|
|
92
|
+
case D5 of
|
|
93
|
+
1 -> K = erlang:max(Twos, Fives),
|
|
94
|
+
{Num * pow_int(2, K - Twos) * pow_int(5, K - Fives), K};
|
|
95
|
+
_ -> none
|
|
96
|
+
end.
|
|
97
|
+
|
|
98
|
+
pow_int(_, 0) -> 1;
|
|
99
|
+
pow_int(B, E) -> B * pow_int(B, E - 1).
|
|
100
|
+
|
|
101
|
+
%% Таблица делимых §58 в срезе: Decimal/Decimal, Money/Decimal, Money/Money одной
|
|
102
|
+
%% валюты. Integer/Integer даёт Rational — вне среза, отвергается планом статически.
|
|
103
|
+
'div'(A, B) ->
|
|
104
|
+
case {num(A), num(B)} of
|
|
105
|
+
{{AU, AS, CA}, {BU, BS, CB}} ->
|
|
106
|
+
case BU of 0 -> refuse("деление на ноль — evaluation error (§58)"); _ -> ok end,
|
|
107
|
+
Like = case {CA, CB} of
|
|
108
|
+
{none, none} -> none;
|
|
109
|
+
{_, none} -> A;
|
|
110
|
+
{none, _} -> refuse("§58: деление вне таблицы делимых");
|
|
111
|
+
{C, C} -> none;
|
|
112
|
+
{_, _} -> refuse("CURRENCY_MISMATCH: §58 делит Money на Money только при тождественной валюте")
|
|
113
|
+
end,
|
|
114
|
+
case exact(AU * p10(BS), BU * p10(AS)) of
|
|
115
|
+
none when Like =/= none -> refuse("INEXACT_DIVISION: округление только явной политикой (§50/§18)");
|
|
116
|
+
none -> refuse("§58: частное не 10-гладкое — Rational вне среза");
|
|
117
|
+
{U, S} when Like =:= none -> {dec, U, S};
|
|
118
|
+
{U, S} -> mk(U, S, Like)
|
|
119
|
+
end;
|
|
120
|
+
_ -> refuse("§58: деление вне таблицы делимых")
|
|
121
|
+
end.
|
|
122
|
+
|
|
123
|
+
%% Евклидов остаток §58: 0 <= r < |b| при любых знаках операндов.
|
|
124
|
+
'mod'(A, B) ->
|
|
125
|
+
case {integral(A), integral(B)} of
|
|
126
|
+
{{ok, _}, {ok, 0}} -> refuse("остаток от деления на ноль — evaluation error (§58)");
|
|
127
|
+
{{ok, X}, {ok, Y}} -> M = erlang:abs(Y), {dec, ((X rem M) + M) rem M, 0};
|
|
128
|
+
_ -> refuse("§58: mod определён только для Integer")
|
|
129
|
+
end.
|
|
130
|
+
|
|
131
|
+
min(A, B) -> same(A, B, "min"), {X, Y, _} = pair(A, B), case X =< Y of true -> A; false -> B end.
|
|
132
|
+
max(A, B) -> same(A, B, "max"), {X, Y, _} = pair(A, B), case X >= Y of true -> A; false -> B end.
|
|
133
|
+
|
|
134
|
+
'abs'(V) ->
|
|
135
|
+
case num(V) of
|
|
136
|
+
{U, S, _} -> mk(erlang:abs(U), S, V);
|
|
137
|
+
false -> refuse("§58: abs — не числовой вид")
|
|
138
|
+
end.
|
|
139
|
+
|
|
140
|
+
%% §58 (E-0075): только Integer pow Integer; порядок проверок — знак, предел,
|
|
141
|
+
%% вычисление, диапазон — часть контракта.
|
|
142
|
+
pow(Base, Exp) ->
|
|
143
|
+
case {integral(Base), integral(Exp)} of
|
|
144
|
+
{{ok, X}, {ok, E}} ->
|
|
145
|
+
case E < 0 of true -> refuse("§58: pow — показатель отрицателен"); false -> ok end,
|
|
146
|
+
case E > 4096 of true -> refuse("RESOURCE_LIMIT: pow — показатель превышает 4096"); false -> ok end,
|
|
147
|
+
R = pow_int(X, E),
|
|
148
|
+
case R < -(1 bsl 63) orelse R > (1 bsl 63) - 1 of
|
|
149
|
+
true -> refuse("RESOURCE_LIMIT: pow — результат вне 64-битного диапазона");
|
|
150
|
+
false -> {dec, R, 0}
|
|
151
|
+
end;
|
|
152
|
+
_ -> refuse("§58: pow определён только для Integer")
|
|
153
|
+
end.
|
|
154
|
+
|
|
155
|
+
round(M, Digits, Mode) ->
|
|
156
|
+
case Mode of <<"HALF_UP">> -> ok; "HALF_UP" -> ok; _ -> refuse(["§50: режим округления не реализован: ", Mode]) end,
|
|
157
|
+
case num(M) of
|
|
158
|
+
false -> refuse("§50: round — не числовой вид");
|
|
159
|
+
{MU, MS, _} ->
|
|
160
|
+
N = case num(Digits) of {DU, DS, _} -> DU div p10(DS); false -> 0 end,
|
|
161
|
+
case MS =< N of
|
|
162
|
+
true -> mk(MU * p10(N - MS), N, M);
|
|
163
|
+
false ->
|
|
164
|
+
Drop = p10(MS - N),
|
|
165
|
+
A = erlang:abs(MU), Q = A div Drop, R = A rem Drop,
|
|
166
|
+
Up = case R * 2 >= Drop of true -> Q + 1; false -> Q end,
|
|
167
|
+
mk(case MU < 0 of true -> -Up; false -> Up end, N, M)
|
|
168
|
+
end
|
|
169
|
+
end.
|
|
170
|
+
|
|
171
|
+
%% --- сравнение и равенство ---------------------------------------------------
|
|
172
|
+
ordered(<<"lt">>, X, Y) -> X < Y;
|
|
173
|
+
ordered(<<"le">>, X, Y) -> X =< Y;
|
|
174
|
+
ordered(<<"gt">>, X, Y) -> X > Y;
|
|
175
|
+
ordered(<<"ge">>, X, Y) -> X >= Y;
|
|
176
|
+
ordered(<<"eq">>, X, Y) -> X =:= Y;
|
|
177
|
+
ordered(<<"ne">>, X, Y) -> X =/= Y;
|
|
178
|
+
ordered(Op, _, _) -> refuse(["сравнение вне среза рантайма: ", Op]).
|
|
179
|
+
|
|
180
|
+
op_bin(Op) when is_list(Op) -> unicode:characters_to_binary(Op);
|
|
181
|
+
op_bin(Op) -> Op.
|
|
182
|
+
|
|
183
|
+
cmp(A, Op0, B) ->
|
|
184
|
+
Op = op_bin(Op0),
|
|
185
|
+
case {A, B} of
|
|
186
|
+
{{sym, X}, {sym, Y}} -> case Op of <<"eq">> -> X =:= Y; <<"ne">> -> X =/= Y; _ -> refuse("§57: вид enum не упорядочен") end;
|
|
187
|
+
{{sym, _}, _} -> case Op of <<"eq">> -> false; <<"ne">> -> true; _ -> refuse("§57: вид enum не упорядочен") end;
|
|
188
|
+
{_, {sym, _}} -> case Op of <<"eq">> -> false; <<"ne">> -> true; _ -> refuse("§57: вид enum не упорядочен") end;
|
|
189
|
+
{{date, X}, {date, Y}} -> ordered(Op, X, Y);
|
|
190
|
+
{{date, _}, _} -> case Op of <<"eq">> -> false; <<"ne">> -> true; _ -> refuse("§57: Date сравнивается только с Date") end;
|
|
191
|
+
{_, {date, _}} -> case Op of <<"eq">> -> false; <<"ne">> -> true; _ -> refuse("§57: Date сравнивается только с Date") end;
|
|
192
|
+
{{text, X}, {text, Y}} -> case Op of <<"eq">> -> X =:= Y; <<"ne">> -> X =/= Y; _ -> refuse(["порядок на Text вне среза рантайма: ", Op]) end;
|
|
193
|
+
{{text, _}, _} -> case Op of <<"eq">> -> false; <<"ne">> -> true; _ -> refuse(["порядок на Text вне среза рантайма: ", Op]) end;
|
|
194
|
+
{_, {text, _}} -> case Op of <<"eq">> -> false; <<"ne">> -> true; _ -> refuse(["порядок на Text вне среза рантайма: ", Op]) end;
|
|
195
|
+
_ ->
|
|
196
|
+
case is_money(A) =/= is_money(B) of true -> refuse("§49: сравнение денег с безразмерным"); false -> ok end,
|
|
197
|
+
case is_money(A) andalso element(4, A) =/= element(4, B) of true -> refuse("§49: разные валюты"); false -> ok end,
|
|
198
|
+
{X, Y, _} = pair(A, B),
|
|
199
|
+
ordered(Op, X, Y)
|
|
200
|
+
end.
|
|
201
|
+
|
|
202
|
+
eq({sym, X}, {sym, Y}) -> X =:= Y;
|
|
203
|
+
eq({sym, _}, _) -> false;
|
|
204
|
+
eq(_, {sym, _}) -> false;
|
|
205
|
+
eq({date, X}, {date, Y}) -> X =:= Y;
|
|
206
|
+
eq({date, _}, _) -> false;
|
|
207
|
+
eq(_, {date, _}) -> false;
|
|
208
|
+
eq({text, X}, {text, Y}) -> X =:= Y;
|
|
209
|
+
eq({text, _}, _) -> false;
|
|
210
|
+
eq(_, {text, _}) -> false;
|
|
211
|
+
eq(A, B) ->
|
|
212
|
+
case is_money(A) =/= is_money(B) orelse (is_money(A) andalso element(4, A) =/= element(4, B)) of
|
|
213
|
+
true -> false;
|
|
214
|
+
false -> {X, Y, _} = pair(A, B), X =:= Y
|
|
215
|
+
end.
|
|
216
|
+
|
|
217
|
+
norm(U, S) when S > 0, U rem 10 =:= 0 -> norm(U div 10, S - 1);
|
|
218
|
+
norm(U, S) -> [integer_to_binary(U), <<"e-">>, integer_to_binary(S)].
|
|
219
|
+
|
|
220
|
+
%% Разделитель и экран внутри текста экранируются: иначе ["a|e:b","c"] и
|
|
221
|
+
%% ["a","b|e:c"] склеивались в один ключ, и второй факт терялся.
|
|
222
|
+
esc(T) -> binary:replace(binary:replace(T, <<"\\">>, <<"\\\\">>, [global]), <<"|">>, <<"\\|">>, [global]).
|
|
223
|
+
|
|
224
|
+
cell({text, T}) -> [<<"e:">>, esc(T)];
|
|
225
|
+
cell({date, D}) -> [<<"t:">>, D];
|
|
226
|
+
cell({sym, I}) -> [<<"c:">>, esc(I)];
|
|
227
|
+
cell({money, U, S, C}) -> [<<"m:">>, C, <<":">>, norm(U, S)];
|
|
228
|
+
cell({dec, U, S}) -> [<<"d:">>, norm(U, S)].
|
|
229
|
+
|
|
230
|
+
key(Row) -> iolist_to_binary(lists:join(<<"|">>, [cell(V) || V <- Row])).
|
|
231
|
+
|
|
232
|
+
set_legal_time(T) -> put(law_legal_time, unicode:characters_to_binary(T)).
|
|
233
|
+
legal_time() ->
|
|
234
|
+
case get(law_legal_time) of
|
|
235
|
+
undefined -> refuse("legalTime не подан делом");
|
|
236
|
+
<<>> -> refuse("legalTime не подан делом");
|
|
237
|
+
T -> T
|
|
238
|
+
end.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
%% Раннер Erlang-бэкенда: тот же вход и тот же ответ, что у runner.py.
|
|
2
|
+
%%
|
|
3
|
+
%% Протокол выхода общий для всех целей: ответ — JSON и код 0; названный отказ
|
|
4
|
+
%% среза — JSON {"refused": текст} и код 3; любой другой код — крах.
|
|
5
|
+
-module(runner).
|
|
6
|
+
-export([main/1]).
|
|
7
|
+
|
|
8
|
+
text(V) when is_binary(V) -> V;
|
|
9
|
+
text(V) when is_integer(V) -> integer_to_binary(V);
|
|
10
|
+
text(V) when is_atom(V) -> atom_to_binary(V, utf8);
|
|
11
|
+
text(V) -> iolist_to_binary(json:encode(V)).
|
|
12
|
+
|
|
13
|
+
get(M, K) -> maps:get(K, M, undefined).
|
|
14
|
+
get(M, K, D) -> maps:get(K, M, D).
|
|
15
|
+
|
|
16
|
+
arg(A) ->
|
|
17
|
+
case get(A, <<"kind">>) of
|
|
18
|
+
<<"entity_ref">> -> {text, get(A, <<"id">>)};
|
|
19
|
+
<<"const_ref">> ->
|
|
20
|
+
%% §54: разыменование предшествует исполнению; член перечисления §38 либо
|
|
21
|
+
%% имя без объявления остаётся идентичностью, как в ключе атома оракула.
|
|
22
|
+
case model:consts(get(A, <<"id">>)) of
|
|
23
|
+
undefined -> rt:sym(get(A, <<"id">>));
|
|
24
|
+
{_, V} -> V
|
|
25
|
+
end;
|
|
26
|
+
<<"value">> ->
|
|
27
|
+
T = get(get(A, <<"type">>, #{}), <<"name">>),
|
|
28
|
+
V = text(get(A, <<"value">>)),
|
|
29
|
+
case T of
|
|
30
|
+
<<"urn:law:std#Money">> -> rt:money(V, get(A, <<"currency">>));
|
|
31
|
+
<<"urn:law:std#Decimal">> -> rt:dec(V);
|
|
32
|
+
<<"urn:law:std#Integer">> -> rt:dec(V);
|
|
33
|
+
<<"urn:law:std#Text">> -> {text, V};
|
|
34
|
+
<<"urn:law:std#Date">> -> rt:date(V);
|
|
35
|
+
_ -> rt:refuse(["аргумент вне среза раннера: ", text(T)])
|
|
36
|
+
end;
|
|
37
|
+
K -> rt:refuse(["аргумент вне среза раннера: ", text(K)])
|
|
38
|
+
end.
|
|
39
|
+
|
|
40
|
+
%% Тип аргумента, если известен: у value — из узла, у const_ref — из объявления
|
|
41
|
+
%% константы; entity_ref типа не несёт (как у оракула).
|
|
42
|
+
arg_type(A) ->
|
|
43
|
+
case get(A, <<"kind">>) of
|
|
44
|
+
<<"value">> -> get(get(A, <<"type">>, #{}), <<"name">>);
|
|
45
|
+
<<"const_ref">> -> case model:consts(get(A, <<"id">>)) of undefined -> undefined; {T, _} -> T end;
|
|
46
|
+
_ -> undefined
|
|
47
|
+
end.
|
|
48
|
+
|
|
49
|
+
%% Совпадение типа с наследованием сущностей вверх от фактического.
|
|
50
|
+
conforms(Actual, Expected) -> conforms(Actual, Expected, #{}).
|
|
51
|
+
conforms(undefined, _, _) -> false;
|
|
52
|
+
conforms(Expected, Expected, _) -> true;
|
|
53
|
+
conforms(Cur, Expected, Seen) ->
|
|
54
|
+
case maps:is_key(Cur, Seen) of
|
|
55
|
+
true -> false;
|
|
56
|
+
false -> conforms(model:base(Cur), Expected, Seen#{Cur => true})
|
|
57
|
+
end.
|
|
58
|
+
|
|
59
|
+
typed(Params, Args) ->
|
|
60
|
+
length(Params) =:= length(Args) andalso
|
|
61
|
+
lists:all(fun({E, A}) -> case arg_type(A) of undefined -> true; T -> conforms(T, E) end end,
|
|
62
|
+
lists:zip(Params, Args)).
|
|
63
|
+
|
|
64
|
+
%% E-0210/E-0242: a conflict blocks its relation and transitive rule readers,
|
|
65
|
+
%% not independent queries in the same program.
|
|
66
|
+
key_conflict_blocks(Db, Predicate) ->
|
|
67
|
+
closure(model:key_conflict_predicates(Db), Predicate, #{}).
|
|
68
|
+
closure([], Predicate, Seen) -> maps:is_key(Predicate, Seen);
|
|
69
|
+
closure([Source | Rest], Predicate, Seen) ->
|
|
70
|
+
case maps:is_key(Source, Seen) of
|
|
71
|
+
true -> closure(Rest, Predicate, Seen);
|
|
72
|
+
false -> closure(Rest ++ model:key_readers(Source), Predicate, Seen#{Source => true})
|
|
73
|
+
end.
|
|
74
|
+
|
|
75
|
+
main([CasePath, QueryPath]) ->
|
|
76
|
+
try
|
|
77
|
+
{ok, CaseBin} = file:read_file(CasePath),
|
|
78
|
+
{ok, QueryBin} = file:read_file(QueryPath),
|
|
79
|
+
Case = json:decode(CaseBin),
|
|
80
|
+
Query = json:decode(QueryBin),
|
|
81
|
+
rt:set_legal_time(text(get(get(Case, <<"context">>, #{}), <<"legalTime">>, <<>>))),
|
|
82
|
+
Db0 = model:package_facts(model:new()),
|
|
83
|
+
{Db1, Issues} = lists:foldl(fun(A, {Db, Iss}) ->
|
|
84
|
+
Lit = get(A, <<"literal">>),
|
|
85
|
+
case get(Lit, <<"polarity">>) of <<"positive">> -> ok; _ -> rt:refuse("§113: отрицательный факт вне L0-среза") end,
|
|
86
|
+
Pred = get(Lit, <<"predicate">>),
|
|
87
|
+
Args = get(Lit, <<"args">>, []),
|
|
88
|
+
case model:params(Pred) of
|
|
89
|
+
undefined -> {Db, Iss};
|
|
90
|
+
Params ->
|
|
91
|
+
%% Арность и типы аргументов утверждения против сигнатуры §189 (E-0046).
|
|
92
|
+
case typed(Params, Args) of
|
|
93
|
+
false -> {Db, Iss ++ [<<"TYPE_ERROR">>]};
|
|
94
|
+
true -> {model:register(Db, Pred, list_to_tuple([arg(X) || X <- Args])), Iss}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end, {Db0, []}, get(Case, <<"assertions">>, [])),
|
|
98
|
+
Db2 = model:saturate(Db1),
|
|
99
|
+
Issues2 = Issues ++ lists:duplicate(model:key_conflicts(Db2), <<"KEY_CONFLICT">>),
|
|
100
|
+
case get(Query, <<"kind">>) of <<"truth">> -> ok; K -> rt:refuse(["вид запроса вне среза раннера: ", text(K)]) end,
|
|
101
|
+
QLit = get(Query, <<"literal">>),
|
|
102
|
+
QPred = get(QLit, <<"predicate">>),
|
|
103
|
+
QArgs = get(QLit, <<"args">>, []),
|
|
104
|
+
%% Ключ атома у оракула §208 включает ТИП значения: несогласованный запрос
|
|
105
|
+
%% атома не находит — NEITHER без issue.
|
|
106
|
+
Hit = case model:params(QPred) of
|
|
107
|
+
undefined -> false;
|
|
108
|
+
QParams ->
|
|
109
|
+
case typed(QParams, QArgs) of
|
|
110
|
+
false -> false;
|
|
111
|
+
true ->
|
|
112
|
+
Want = [arg(X) || X <- QArgs],
|
|
113
|
+
lists:any(fun(Row) ->
|
|
114
|
+
R = tuple_to_list(Row),
|
|
115
|
+
length(R) =:= length(Want) andalso lists:all(fun({V, W}) -> rt:eq(V, W) end, lists:zip(R, Want))
|
|
116
|
+
end, model:rows(Db2, QPred))
|
|
117
|
+
end
|
|
118
|
+
end,
|
|
119
|
+
%% Открытый мир: отсутствие строки — это NEITHER, а не FALSE_ONLY.
|
|
120
|
+
Out = #{<<"queryId">> => get(Query, <<"queryId">>),
|
|
121
|
+
<<"truthStatus">> => case Hit of true -> <<"TRUE_ONLY">>; false -> <<"NEITHER">> end,
|
|
122
|
+
<<"evaluationStatus">> => case key_conflict_blocks(Db2, QPred) of true -> <<"NON_EXECUTABLE">>; false -> <<"COMPUTED">> end,
|
|
123
|
+
<<"issues">> => Issues2},
|
|
124
|
+
io:format("~ts~n", [json:encode(Out)]),
|
|
125
|
+
erlang:halt(0)
|
|
126
|
+
catch
|
|
127
|
+
throw:{refused, Msg} ->
|
|
128
|
+
io:format("~ts~n", [json:encode(#{<<"refused">> => Msg})]),
|
|
129
|
+
erlang:halt(3)
|
|
130
|
+
end.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Харнесс сертификации рантайма: исполняет runtime-cert.json и печатает,
|
|
2
|
+
# что получилось. Ожидания сверяют ворота, харнесс их не читает.
|
|
3
|
+
Code.require_file("runtime.ex", __DIR__)
|
|
4
|
+
|
|
5
|
+
defmodule LawCert do
|
|
6
|
+
def lit(x) when is_binary(x), do: x
|
|
7
|
+
def lit(%{"dec" => d}), do: LawRT.dec(d)
|
|
8
|
+
def lit(%{"money" => m, "cur" => c}), do: LawRT.money(m, c)
|
|
9
|
+
def lit(%{"text" => t}), do: {:text, t}
|
|
10
|
+
def lit(%{"date" => d}), do: LawRT.date(d)
|
|
11
|
+
def lit(%{"sym" => s}), do: LawRT.sym(s)
|
|
12
|
+
def lit(%{"row" => r}), do: Enum.map(r, &lit/1)
|
|
13
|
+
|
|
14
|
+
# Отображение с ЯВНЫМ масштабом: 2.00 и 2 — разные записи одного значения.
|
|
15
|
+
def digits(u, s) do
|
|
16
|
+
body = Integer.to_string(abs(u)) |> String.pad_leading(s + 1, "0")
|
|
17
|
+
text = if s == 0, do: body, else: String.slice(body, 0, String.length(body) - s) <> "." <> String.slice(body, -s, s)
|
|
18
|
+
if u < 0, do: "-" <> text, else: text
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def render(nil), do: nil
|
|
22
|
+
def render(b) when is_boolean(b), do: b
|
|
23
|
+
def render({:text, t}), do: t
|
|
24
|
+
def render({:money, u, s, c}), do: digits(u, s) <> " " <> c
|
|
25
|
+
def render({:date, d}), do: d
|
|
26
|
+
def render({:sym, i}), do: i
|
|
27
|
+
def render({:dec, u, s}), do: digits(u, s)
|
|
28
|
+
|
|
29
|
+
def run(%{"op" => op, "args" => raw}) do
|
|
30
|
+
a = Enum.map(raw, &lit/1)
|
|
31
|
+
case op do
|
|
32
|
+
"dec" -> LawRT.dec(Enum.at(a, 0))
|
|
33
|
+
"money" -> LawRT.money(Enum.at(a, 0), Enum.at(a, 1))
|
|
34
|
+
"date" -> LawRT.date(Enum.at(a, 0))
|
|
35
|
+
"sym" -> LawRT.sym(Enum.at(a, 0))
|
|
36
|
+
"add" -> LawRT.add(Enum.at(a, 0), Enum.at(a, 1))
|
|
37
|
+
"sub" -> LawRT.sub(Enum.at(a, 0), Enum.at(a, 1))
|
|
38
|
+
"mul" -> LawRT.mul(Enum.at(a, 0), Enum.at(a, 1))
|
|
39
|
+
"div" -> LawRT.divide(Enum.at(a, 0), Enum.at(a, 1))
|
|
40
|
+
"mod" -> LawRT.modulo(Enum.at(a, 0), Enum.at(a, 1))
|
|
41
|
+
"min" -> LawRT.min(Enum.at(a, 0), Enum.at(a, 1))
|
|
42
|
+
"max" -> LawRT.max(Enum.at(a, 0), Enum.at(a, 1))
|
|
43
|
+
"abs" -> LawRT.absolute(Enum.at(a, 0))
|
|
44
|
+
"pow" -> LawRT.pow(Enum.at(a, 0), Enum.at(a, 1))
|
|
45
|
+
"round" -> LawRT.round(Enum.at(a, 0), Enum.at(a, 1), Enum.at(a, 2))
|
|
46
|
+
"cmp" -> LawRT.cmp(Enum.at(a, 0), Enum.at(a, 1), Enum.at(a, 2))
|
|
47
|
+
"eq" -> LawRT.eq(Enum.at(a, 0), Enum.at(a, 1))
|
|
48
|
+
"keyEq" -> LawRT.key(Enum.at(a, 0)) == LawRT.key(Enum.at(a, 1))
|
|
49
|
+
"legalTime" -> {:text, LawRT.legal_time()}
|
|
50
|
+
"setLegalTime" -> (LawRT.set_legal_time(Enum.at(a, 0)); nil)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
[path] = System.argv()
|
|
56
|
+
fixture = JSON.decode!(File.read!(path))
|
|
57
|
+
out =
|
|
58
|
+
Enum.map(fixture["cases"], fn c ->
|
|
59
|
+
got = try do LawCert.render(LawCert.run(c)) catch {:refused, msg} -> %{"refused" => msg} end
|
|
60
|
+
%{"id" => c["id"], "got" => got}
|
|
61
|
+
end)
|
|
62
|
+
IO.puts(JSON.encode!(out))
|