arxo-codegen 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. arxo_codegen/__init__.py +5 -0
  2. arxo_codegen/__main__.py +7 -0
  3. arxo_codegen/active-outcomes.json +6837 -0
  4. arxo_codegen/check.py +158 -0
  5. arxo_codegen/check_active_canaries.py +262 -0
  6. arxo_codegen/check_api.py +76 -0
  7. arxo_codegen/check_conformance.py +414 -0
  8. arxo_codegen/check_matrix.py +131 -0
  9. arxo_codegen/check_packages.py +281 -0
  10. arxo_codegen/check_semantic_probes.py +293 -0
  11. arxo_codegen/check_xlsx.py +320 -0
  12. arxo_codegen/consumer_runner.py +316 -0
  13. arxo_codegen/core_support.py +203 -0
  14. arxo_codegen/coverage.py +341 -0
  15. arxo_codegen/deliver.py +180 -0
  16. arxo_codegen/examples/xlsx/pricing.lawir.json +409 -0
  17. arxo_codegen/examples/xlsx/pricing.xlsx.json +52 -0
  18. arxo_codegen/excel_plan.py +602 -0
  19. arxo_codegen/harness/verify/ci/gates/backends/check_backends.py +1248 -0
  20. arxo_codegen/harness/verify/ci/gates/backends/l1_fixtures.py +463 -0
  21. arxo_codegen/inert-nodes.json +62 -0
  22. arxo_codegen/inert.py +91 -0
  23. arxo_codegen/inputs.py +59 -0
  24. arxo_codegen/package.py +565 -0
  25. arxo_codegen/plan.py +4053 -0
  26. arxo_codegen/recertify_registry.py +100 -0
  27. arxo_codegen/runtime-cert.json +5259 -0
  28. arxo_codegen/runtime_cert.py +233 -0
  29. arxo_codegen/survey.py +272 -0
  30. arxo_codegen/target_runner.py +101 -0
  31. arxo_codegen/targets/bsl/cert.bsl +113 -0
  32. arxo_codegen/targets/bsl/emit.py +356 -0
  33. arxo_codegen/targets/bsl/json.bsl +243 -0
  34. arxo_codegen/targets/bsl/runner.bsl +210 -0
  35. arxo_codegen/targets/bsl/runtime.bsl +858 -0
  36. arxo_codegen/targets/cs/Cert.cs +90 -0
  37. arxo_codegen/targets/cs/Runner.cs +154 -0
  38. arxo_codegen/targets/cs/Runtime.cs +243 -0
  39. arxo_codegen/targets/cs/emit.py +223 -0
  40. arxo_codegen/targets/dart/cert.dart +99 -0
  41. arxo_codegen/targets/dart/emit.py +206 -0
  42. arxo_codegen/targets/dart/json.dart +166 -0
  43. arxo_codegen/targets/dart/runner.dart +154 -0
  44. arxo_codegen/targets/dart/runtime.dart +293 -0
  45. arxo_codegen/targets/erl/cert.erl +64 -0
  46. arxo_codegen/targets/erl/emit.py +216 -0
  47. arxo_codegen/targets/erl/rt.erl +238 -0
  48. arxo_codegen/targets/erl/runner.erl +130 -0
  49. arxo_codegen/targets/ex/cert.exs +62 -0
  50. arxo_codegen/targets/ex/emit.py +204 -0
  51. arxo_codegen/targets/ex/runner.exs +124 -0
  52. arxo_codegen/targets/ex/runtime.ex +227 -0
  53. arxo_codegen/targets/go/apiBase.go +483 -0
  54. arxo_codegen/targets/go/apicheck.go +97 -0
  55. arxo_codegen/targets/go/cert.go +218 -0
  56. arxo_codegen/targets/go/emit.py +1806 -0
  57. arxo_codegen/targets/go/evaluate.go +1826 -0
  58. arxo_codegen/targets/go/runner.go +38 -0
  59. arxo_codegen/targets/go/runtime.go +2612 -0
  60. arxo_codegen/targets/java/Cert.java +91 -0
  61. arxo_codegen/targets/java/Json.java +148 -0
  62. arxo_codegen/targets/java/Runner.java +151 -0
  63. arxo_codegen/targets/java/Runtime.java +243 -0
  64. arxo_codegen/targets/java/emit.py +198 -0
  65. arxo_codegen/targets/kt/Cert.kt +67 -0
  66. arxo_codegen/targets/kt/Json.kt +103 -0
  67. arxo_codegen/targets/kt/Runner.kt +126 -0
  68. arxo_codegen/targets/kt/Runtime.kt +196 -0
  69. arxo_codegen/targets/kt/emit.py +195 -0
  70. arxo_codegen/targets/lua/cert.lua +82 -0
  71. arxo_codegen/targets/lua/emit.py +250 -0
  72. arxo_codegen/targets/lua/json.lua +133 -0
  73. arxo_codegen/targets/lua/runner.lua +167 -0
  74. arxo_codegen/targets/lua/runtime.lua +541 -0
  75. arxo_codegen/targets/php/cert.php +84 -0
  76. arxo_codegen/targets/php/emit.py +229 -0
  77. arxo_codegen/targets/php/json.php +144 -0
  78. arxo_codegen/targets/php/runner.php +189 -0
  79. arxo_codegen/targets/php/runtime.php +660 -0
  80. arxo_codegen/targets/py/api_base.py +334 -0
  81. arxo_codegen/targets/py/apicheck.py +51 -0
  82. arxo_codegen/targets/py/cert.py +140 -0
  83. arxo_codegen/targets/py/emit.py +1140 -0
  84. arxo_codegen/targets/py/evaluate.py +980 -0
  85. arxo_codegen/targets/py/runner.py +38 -0
  86. arxo_codegen/targets/py/runtime.py +1634 -0
  87. arxo_codegen/targets/rb/cert.rb +73 -0
  88. arxo_codegen/targets/rb/emit.py +203 -0
  89. arxo_codegen/targets/rb/runner.rb +130 -0
  90. arxo_codegen/targets/rb/runtime.rb +255 -0
  91. arxo_codegen/targets/rs/Cargo.lock +142 -0
  92. arxo_codegen/targets/rs/Cargo.toml +16 -0
  93. arxo_codegen/targets/rs/cert.rs +106 -0
  94. arxo_codegen/targets/rs/emit.py +233 -0
  95. arxo_codegen/targets/rs/prefetch.rs +1 -0
  96. arxo_codegen/targets/rs/runner.rs +181 -0
  97. arxo_codegen/targets/rs/runtime.rs +448 -0
  98. arxo_codegen/targets/scala/Cert.scala +77 -0
  99. arxo_codegen/targets/scala/Json.scala +140 -0
  100. arxo_codegen/targets/scala/Runner.scala +121 -0
  101. arxo_codegen/targets/scala/Runtime.scala +269 -0
  102. arxo_codegen/targets/scala/emit.py +193 -0
  103. arxo_codegen/targets/swift/cert.swift +105 -0
  104. arxo_codegen/targets/swift/emit.py +231 -0
  105. arxo_codegen/targets/swift/json.swift +208 -0
  106. arxo_codegen/targets/swift/runner.swift +133 -0
  107. arxo_codegen/targets/swift/runtime.swift +528 -0
  108. arxo_codegen/targets/ts/apiBase.ts +250 -0
  109. arxo_codegen/targets/ts/apicheck.ts +37 -0
  110. arxo_codegen/targets/ts/cert.ts +96 -0
  111. arxo_codegen/targets/ts/emit.py +1214 -0
  112. arxo_codegen/targets/ts/evaluate.ts +894 -0
  113. arxo_codegen/targets/ts/package-lock.json +45 -0
  114. arxo_codegen/targets/ts/package.json +9 -0
  115. arxo_codegen/targets/ts/runner.ts +31 -0
  116. arxo_codegen/targets/ts/runtime.ts +1369 -0
  117. arxo_codegen/targets/xlsx/README.ru.md +176 -0
  118. arxo_codegen/targets/xlsx/emit.py +198 -0
  119. arxo_codegen/temporal.py +59 -0
  120. arxo_codegen/test_alias_types.py +171 -0
  121. arxo_codegen/test_api_contract.py +89 -0
  122. arxo_codegen/test_calendar_policy.py +48 -0
  123. arxo_codegen/test_check.py +49 -0
  124. arxo_codegen/test_check_active_canaries.py +96 -0
  125. arxo_codegen/test_check_conformance.py +132 -0
  126. arxo_codegen/test_check_semantic_probes.py +85 -0
  127. arxo_codegen/test_check_xlsx.py +52 -0
  128. arxo_codegen/test_consumer_runner.py +175 -0
  129. arxo_codegen/test_core_support.py +80 -0
  130. arxo_codegen/test_e0212_decision_time.py +63 -0
  131. arxo_codegen/test_head_aggregate.py +71 -0
  132. arxo_codegen/test_inert.py +143 -0
  133. arxo_codegen/test_instant_types.py +113 -0
  134. arxo_codegen/test_key_conflict_scope.py +92 -0
  135. arxo_codegen/test_key_conflict_targets.py +17 -0
  136. arxo_codegen/test_package_layout.py +163 -0
  137. arxo_codegen/test_plan_language_version.py +44 -0
  138. arxo_codegen/test_positions_refusal.py +43 -0
  139. arxo_codegen/test_recertify_registry.py +77 -0
  140. arxo_codegen/test_ruby_target.py +41 -0
  141. arxo_codegen/test_runtime_cert.py +83 -0
  142. arxo_codegen/test_secondary_key_conflict_scope.py +17 -0
  143. arxo_codegen/test_stage_r.py +108 -0
  144. arxo_codegen/test_standalone_inputs.py +71 -0
  145. arxo_codegen/test_support.py +150 -0
  146. arxo_codegen/test_survey_inputs.py +115 -0
  147. arxo_codegen-0.4.0.dist-info/LICENSE +202 -0
  148. arxo_codegen-0.4.0.dist-info/METADATA +1673 -0
  149. arxo_codegen-0.4.0.dist-info/RECORD +152 -0
  150. arxo_codegen-0.4.0.dist-info/WHEEL +5 -0
  151. arxo_codegen-0.4.0.dist-info/entry_points.txt +13 -0
  152. arxo_codegen-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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