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,1140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Эмиттер Python. Тот же план, другой синтаксис и тот же контракт рантайма."""
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
# Версия плана, которую этот эмиттер умеет печатать. Другая версия — отказ:
|
|
6
|
+
# незнакомое поле плана печаталось бы молча, а это другой ответ, не меньший.
|
|
7
|
+
SUPPORTED_PLAN = "law-backend-plan/0.7"
|
|
8
|
+
# План поражаемого слоя (DECISION-0237) — отдельная версия со своей печатью.
|
|
9
|
+
SUPPORTED_PLAN_L1 = "law-backend-plan/0.8"
|
|
10
|
+
# План с агрегатами §59 в правилах и замыканиями §70 (DECISION-0239).
|
|
11
|
+
SUPPORTED_PLAN_09 = "law-backend-plan/0.9"
|
|
12
|
+
# План с нормативными позициями L2 (DECISION-0242).
|
|
13
|
+
SUPPORTED_PLAN_10 = "law-backend-plan/0.10"
|
|
14
|
+
# План с календарными функциями §84–§86 (DECISION-0245).
|
|
15
|
+
SUPPORTED_PLAN_11 = "law-backend-plan/0.11"
|
|
16
|
+
# План с Rational §35/§58 (DECISION-0252).
|
|
17
|
+
SUPPORTED_PLAN_12 = "law-backend-plan/0.12"
|
|
18
|
+
# План с Quantity §49 v1 и счётчиком-связывателем §86.1 (DECISION-0256).
|
|
19
|
+
SUPPORTED_PLAN_13 = "law-backend-plan/0.13"
|
|
20
|
+
# План с политикой округления §50 (DECISION-0259).
|
|
21
|
+
SUPPORTED_PLAN_14 = "law-backend-plan/0.14"
|
|
22
|
+
# План с Boolean/Natural, границами месяца §84.2 и `_` §98 (DECISION-0266–0268).
|
|
23
|
+
SUPPORTED_PLAN_15 = "law-backend-plan/0.15"
|
|
24
|
+
# План с датированными приоритетами §92.3 (DECISION-0269).
|
|
25
|
+
SUPPORTED_PLAN_16 = "law-backend-plan/0.16"
|
|
26
|
+
# План с вычисляемым аргументом атома тела и условным термом §56 (DECISION-0270).
|
|
27
|
+
SUPPORTED_PLAN_17 = "law-backend-plan/0.17"
|
|
28
|
+
# План 0.18 (DECISION-0273): функции §46, параметры A3, `let`, `match`.
|
|
29
|
+
SUPPORTED_PLAN_18 = "law-backend-plan/0.18"
|
|
30
|
+
# План 0.19 (DECISION-0274): канал суждения §47.3.
|
|
31
|
+
SUPPORTED_PLAN_19 = "law-backend-plan/0.19"
|
|
32
|
+
# Возможности плана, которые цель печатает; поставка сверяет их до эмиссии.
|
|
33
|
+
CAPABILITIES = {"admission:current", "defeasible", "negation", "priority", "status:not_known", "status:unknown",
|
|
34
|
+
"function", "param:a3", "term:let", "term:match", "judgment",
|
|
35
|
+
"status:refuted", "status:monotone", "status:supported", "aggregate", "closure",
|
|
36
|
+
"calendar:days_between", "calendar:period", "calendar:business", "value:rational",
|
|
37
|
+
"value:quantity", "value:round", "value:div_round",
|
|
38
|
+
"value:boolean", "value:natural", "calendar:month_bound", "wildcard", "priority:dated", "term:body", "term:if",
|
|
39
|
+
"value:instant"}
|
|
40
|
+
PHASES = {"strict", "closure", "candidates", "defeat", "surviving", "status", "norms", "effects"}
|
|
41
|
+
# Поля фаз расписания: незнакомое поле печаталось бы молча. `waiting` —
|
|
42
|
+
# правила, ждущие замыкания §70 (errata E-0174).
|
|
43
|
+
PHASE_FIELDS = {"strict": {"phase", "waiting"}, "closure": {"phase", "policies"},
|
|
44
|
+
"candidates": {"phase", "stratum", "rules"}, "defeat": {"phase", "stratum"},
|
|
45
|
+
"surviving": {"phase", "stratum"}, "status": {"phase", "stratum"},
|
|
46
|
+
"norms": {"phase"}, "effects": {"phase"}}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_plan(plan: dict) -> None:
|
|
50
|
+
if "norms" in plan or "norm" in (plan.get("capabilities") or []):
|
|
51
|
+
raise ValueError("NORM_RANKS_UNAVAILABLE: norm runtime requires current ranks (W6)")
|
|
52
|
+
v = plan.get("basePlanVersion") if plan.get("planVersion") == "law-backend-plan/0.20" else plan.get("planVersion")
|
|
53
|
+
if v not in (SUPPORTED_PLAN, SUPPORTED_PLAN_L1, SUPPORTED_PLAN_09, SUPPORTED_PLAN_10, SUPPORTED_PLAN_11,
|
|
54
|
+
SUPPORTED_PLAN_12, SUPPORTED_PLAN_13, SUPPORTED_PLAN_14, SUPPORTED_PLAN_15, SUPPORTED_PLAN_16, SUPPORTED_PLAN_17, SUPPORTED_PLAN_18, SUPPORTED_PLAN_19):
|
|
55
|
+
raise ValueError(f"версия плана не поддержана эмиттером: {v!r} (поддержаны {SUPPORTED_PLAN}, "
|
|
56
|
+
f"{SUPPORTED_PLAN_L1}, {SUPPORTED_PLAN_09}, {SUPPORTED_PLAN_10}, {SUPPORTED_PLAN_11}, "
|
|
57
|
+
f"{SUPPORTED_PLAN_12}, {SUPPORTED_PLAN_13}, {SUPPORTED_PLAN_14}, {SUPPORTED_PLAN_15}, {SUPPORTED_PLAN_16}, {SUPPORTED_PLAN_17}, {SUPPORTED_PLAN_18}, {SUPPORTED_PLAN_19})")
|
|
58
|
+
missing = sorted(set(plan.get("capabilities") or ()) - CAPABILITIES)
|
|
59
|
+
if missing:
|
|
60
|
+
raise ValueError(f"возможности плана не поддержаны эмиттером: {', '.join(missing)}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _judgments(L: list, plan: dict) -> None:
|
|
64
|
+
"""Отношения суждения §47.3 (DECISION-0274): орган и форма запроса по
|
|
65
|
+
отношению — для обхода запроса судье в рантайме и проверки ответа §47.3.1."""
|
|
66
|
+
L.append("")
|
|
67
|
+
L.append("# Отношения суждения §47.3 (DECISION-0274): орган и форма запроса.")
|
|
68
|
+
L.append("JUDGMENTS = " + json.dumps(plan.get("judgments") or {}, ensure_ascii=False))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _functions(L: list, plan: dict) -> None:
|
|
72
|
+
"""Функции §46 и параметры A3 (DECISION-0273). Функции печатаются
|
|
73
|
+
функциями цели по позиционным параметрам и экспортируются раннеру для
|
|
74
|
+
вопросов `term`; параметр A3 — значение на legal_time из `prepare`."""
|
|
75
|
+
L.append("")
|
|
76
|
+
L.append("# Функции §46 (DECISION-0273): чистые тела по позиционным параметрам.")
|
|
77
|
+
L.append("def _match_fail():")
|
|
78
|
+
L.append(' raise RT.TermError("NON_EXHAUSTIVE_MATCH")')
|
|
79
|
+
L.append("")
|
|
80
|
+
for f in plan.get("functions") or []:
|
|
81
|
+
params = ", ".join(f"a{i}" for i in range(len(f["params"])))
|
|
82
|
+
L.append(f'def f_{f["name"]}({params}):')
|
|
83
|
+
L.append(f' return {term(f["body"])}')
|
|
84
|
+
L.append("")
|
|
85
|
+
L.append("FUNCTIONS = {")
|
|
86
|
+
for f in plan.get("functions") or []:
|
|
87
|
+
L.append(f' {json.dumps(f["id"])}: (f_{f["name"]}, {json.dumps(f["paramTypes"])}, {json.dumps(f["returnType"])}),')
|
|
88
|
+
L.append("}")
|
|
89
|
+
L.append("# Параметры A3 §A3.2: окна [start, end) оси права со значением; активное значение")
|
|
90
|
+
L.append("# готовит `prepare` по legal_time, разрыв датасета — MISSING_PARAMETER_VALUE правила.")
|
|
91
|
+
L.append("A3_TYPES = " + json.dumps({p["id"]: p["type"] for p in plan.get("a3Params") or []}, ensure_ascii=False))
|
|
92
|
+
if plan.get("a3Params"):
|
|
93
|
+
L.append("A3_WINDOWS = [")
|
|
94
|
+
for p in plan["a3Params"]:
|
|
95
|
+
ws = ", ".join(f"({s!r}, {e!r}, {term(v)})" for s, e, v in p["windows"])
|
|
96
|
+
L.append(f' ({json.dumps(p["id"])}, [{ws}]),')
|
|
97
|
+
L.append("]")
|
|
98
|
+
L.append("A3 = {}")
|
|
99
|
+
L.append("")
|
|
100
|
+
L.append("def _a3(id):")
|
|
101
|
+
L.append(" if id not in A3:")
|
|
102
|
+
L.append(' raise RT.TermError("MISSING_PARAMETER_VALUE")')
|
|
103
|
+
L.append(" return A3[id]")
|
|
104
|
+
L.append("")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _calendar_env(L: list, plan: dict) -> None:
|
|
108
|
+
"""Окружение календаря (DECISION-0245 п. 3–4): раннер выбирает снимок и
|
|
109
|
+
политику дела до вывода."""
|
|
110
|
+
if "calendars" in plan:
|
|
111
|
+
L.append("# §85/§86 (DECISION-0245): версия семантики, политики и снимки программы.")
|
|
112
|
+
L.append(f'CAL_SEMVER = {json.dumps(plan["semanticVersion"])}')
|
|
113
|
+
L.append(f'CAL_POLICIES = {plan["deadlinePolicies"]!r}')
|
|
114
|
+
L.append(f'CAL_SNAPSHOTS = {plan["calendars"]!r}')
|
|
115
|
+
if "backchain" in plan:
|
|
116
|
+
L.append("# E-0105: правила для обхода ошибок терма от цели вопроса.")
|
|
117
|
+
L.append(f'BACKCHAIN = {plan["backchain"]!r}')
|
|
118
|
+
L.append(f'BACKCHAIN_CONSTS = {plan["backchainConsts"]!r}')
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _is_l1(plan: dict) -> bool:
|
|
122
|
+
"""Модель L1: у плана 0.11 — по расписанию фаз (DECISION-0245 п. 1)."""
|
|
123
|
+
return plan["planVersion"] in (SUPPORTED_PLAN_L1, SUPPORTED_PLAN_09, SUPPORTED_PLAN_10) or \
|
|
124
|
+
(plan["planVersion"] in (SUPPORTED_PLAN_11, SUPPORTED_PLAN_12, SUPPORTED_PLAN_13, SUPPORTED_PLAN_14, SUPPORTED_PLAN_15, SUPPORTED_PLAN_16, SUPPORTED_PLAN_17, SUPPORTED_PLAN_18, SUPPORTED_PLAN_19) and "schedule" in plan)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _is_09(plan: dict) -> bool:
|
|
128
|
+
return plan["planVersion"] in (SUPPORTED_PLAN_09, SUPPORTED_PLAN_10) or \
|
|
129
|
+
(plan["planVersion"] in (SUPPORTED_PLAN_11, SUPPORTED_PLAN_12, SUPPORTED_PLAN_13, SUPPORTED_PLAN_14, SUPPORTED_PLAN_15, SUPPORTED_PLAN_16, SUPPORTED_PLAN_17, SUPPORTED_PLAN_18, SUPPORTED_PLAN_19) and "aggregates" in plan)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def term(t: dict) -> str:
|
|
133
|
+
k = t["k"]
|
|
134
|
+
if k == "qty":
|
|
135
|
+
# Счётчик календарной функции (DECISION-0245 п. 2): число и единица.
|
|
136
|
+
return f'{t["v"]}, {json.dumps(t["unit"])}'
|
|
137
|
+
if k == "qtyvar":
|
|
138
|
+
# Счётчик-связыватель §86.1 (DECISION-0256 п. 6): число и единица проверяются на деле.
|
|
139
|
+
return f'*RT.counter({term(t["term"])}, {json.dumps(t["fn"])})'
|
|
140
|
+
|
|
141
|
+
if k == "var":
|
|
142
|
+
return f'r{t["row"]}[{t["col"]}]'
|
|
143
|
+
if k == "lit":
|
|
144
|
+
if t["t"] == "money":
|
|
145
|
+
return f'RT.money("{t["v"]}", "{t["cur"]}")'
|
|
146
|
+
if t["t"] == "dec":
|
|
147
|
+
return f'RT.dec("{t["v"]}")'
|
|
148
|
+
if t["t"] == "date":
|
|
149
|
+
return f'RT.date("{t["v"]}")'
|
|
150
|
+
if t["t"] == "inst":
|
|
151
|
+
return f'RT.instant("{t["v"]}")'
|
|
152
|
+
if t["t"] == "enum":
|
|
153
|
+
return f'RT.sym("{t["v"]}")'
|
|
154
|
+
if t["t"] == "rat":
|
|
155
|
+
return f'RT.rat("{t["v"]}")'
|
|
156
|
+
if t["t"] == "qty":
|
|
157
|
+
return f'RT.qty("{t["v"]}", {json.dumps(t["unit"], ensure_ascii=False)})'
|
|
158
|
+
if t["t"] == "bool":
|
|
159
|
+
# DECISION-0266: булево — литерал языка, не JSON.
|
|
160
|
+
return "True" if t["v"] else "False"
|
|
161
|
+
return json.dumps(t["v"], ensure_ascii=False)
|
|
162
|
+
if k == "bin":
|
|
163
|
+
if "lk" in t:
|
|
164
|
+
# DECISION-0252 п. 3: пара §58 решается статическими видами операндов.
|
|
165
|
+
return f'RT.arith("{t["op"]}", {term(t["l"])}, {term(t["r"])}, "{t["lk"]}", "{t["rk"]}")'
|
|
166
|
+
return f'RT.{t["op"]}({term(t["l"])}, {term(t["r"])})'
|
|
167
|
+
if k == "call":
|
|
168
|
+
if "ak" in t:
|
|
169
|
+
# DECISION-0259 п. 2: политика §50 проверяется рантаймом; виды
|
|
170
|
+
# значения и точности — из плана (последний вид — Text режима).
|
|
171
|
+
name = "round_mode" if t["fn"] == "round" else t["fn"]
|
|
172
|
+
return (f'RT.{name}(' + ", ".join(term(a) for a in t["args"]) + ", "
|
|
173
|
+
+ ", ".join(json.dumps(c) for c in t["ak"][:-1]) + ')')
|
|
174
|
+
return f'RT.{t["fn"]}(' + ", ".join(term(a) for a in t["args"]) + ')'
|
|
175
|
+
if k == "param":
|
|
176
|
+
return f'p{t["i"]}'
|
|
177
|
+
if k == "fvar":
|
|
178
|
+
# DECISION-0273: параметр функции §46 — позиционный аргумент.
|
|
179
|
+
return f'a{t["i"]}'
|
|
180
|
+
if k == "lvar":
|
|
181
|
+
return f'l{t["i"]}'
|
|
182
|
+
if k == "fcall":
|
|
183
|
+
return f'f_{t["fn"]}(' + ", ".join(term(a) for a in t["args"]) + ')'
|
|
184
|
+
if k == "a3":
|
|
185
|
+
# Параметр A3: значение на legal_time готовит `prepare`; разрыв — TermError.
|
|
186
|
+
return f'_a3({json.dumps(t["id"])})'
|
|
187
|
+
if k == "let":
|
|
188
|
+
# §55: значение считается один раз, тело — с локальным связыванием.
|
|
189
|
+
return f'(lambda l{t["i"]}: {term(t["b"])})({term(t["v"])})'
|
|
190
|
+
if k == "match":
|
|
191
|
+
# §55.1: первая совпавшая ветвь; ни одна — NON_EXHAUSTIVE_MATCH.
|
|
192
|
+
out = "_match_fail()"
|
|
193
|
+
for arm in reversed(t["arms"]):
|
|
194
|
+
if arm["m"] is None:
|
|
195
|
+
out = term(arm["v"])
|
|
196
|
+
else:
|
|
197
|
+
out = f'({term(arm["v"])} if RT.eq(s, RT.sym({json.dumps(arm["m"])})) else {out})'
|
|
198
|
+
return f'(lambda s: {out})({term(t["s"])})'
|
|
199
|
+
if k == "agg":
|
|
200
|
+
return f'{t["fn"]}(' + ", ".join(term(a) for a in t["args"]) + ')'
|
|
201
|
+
if k == "if":
|
|
202
|
+
# §56 (DECISION-0270): ленивая ветвь — условное выражение.
|
|
203
|
+
return f'({term(t["t"])} if {cond(t["c"])} else {term(t["e"])})'
|
|
204
|
+
raise AssertionError(k)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def cond(c: dict) -> str:
|
|
208
|
+
"""Data-условие §56: boolean, сравнение, and, or."""
|
|
209
|
+
k = c["k"]
|
|
210
|
+
if k == "bool":
|
|
211
|
+
return "True" if c["v"] else "False"
|
|
212
|
+
if k == "cmp":
|
|
213
|
+
return f'RT.cmp({term(c["l"])}, "{c["op"]}", {term(c["r"])})'
|
|
214
|
+
if k in ("and", "or"):
|
|
215
|
+
return "(" + f" {k} ".join(cond(x) for x in c["items"]) + ")"
|
|
216
|
+
raise AssertionError(k)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# Виды ячеек, на которых `eq` рантайма не отказывает: проверку соединения
|
|
220
|
+
# можно поставить на уровень атома, который её связывает, — ответ и отказы
|
|
221
|
+
# прежние, а декартово произведение не строится (у `algo.*` шесть атомов
|
|
222
|
+
# тела давали минуты на пустом деле). Соединение над деньгами отказывает на
|
|
223
|
+
# разных валютах, поэтому при нём правило печатается прежним порядком.
|
|
224
|
+
_EARLY_KINDS = {"text", "enum", "date", "dec"}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _py_wild_open(L: list, ind: str, i: int, a: dict) -> None:
|
|
228
|
+
"""§98 (DECISION-0268): множество проекций строк атома с `_` — на уровне
|
|
229
|
+
цикла, то есть заново на каждой подстановке внешних атомов."""
|
|
230
|
+
if a.get("wild"):
|
|
231
|
+
L.append(f'{ind}w{i} = set()')
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _py_wild(L: list, ind: str, i: int, a: dict) -> None:
|
|
235
|
+
"""Строки, различающиеся только в столбцах `_`, дают одну подстановку."""
|
|
236
|
+
if a.get("wild"):
|
|
237
|
+
cells = ", ".join(f"r{i}[{c}]" for c in a["keep"])
|
|
238
|
+
L.append(f'{ind}wk{i} = RT.key([{cells}])')
|
|
239
|
+
L.append(f'{ind}if wk{i} in w{i}: continue')
|
|
240
|
+
L.append(f'{ind}w{i}.add(wk{i})')
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _py_checks(L: list, ind: str, checks: list) -> None:
|
|
244
|
+
for kind, c in checks:
|
|
245
|
+
if kind == "join":
|
|
246
|
+
(ai, aj, _at), (bi, bj, _bt) = c
|
|
247
|
+
L.append(f'{ind}if not RT.cmp(r{ai}[{aj}], "eq", r{bi}[{bj}]): continue')
|
|
248
|
+
else:
|
|
249
|
+
L.append(f'{ind}if not RT.eq(r{c["row"]}[{c["col"]}], {term(c["term"])}): continue')
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _early(r: dict) -> tuple[dict, list, list]:
|
|
253
|
+
"""Проверки по уровням атомов `{i: [строки-проверки]}` и оставшиеся во
|
|
254
|
+
внутреннем цикле соединения и фильтры (порядок печати прежний)."""
|
|
255
|
+
joins, filters = r["joins"], r["filters"]
|
|
256
|
+
if any(at not in _EARLY_KINDS or bt not in _EARLY_KINDS for (_ai, _aj, at), (_bi, _bj, bt) in joins):
|
|
257
|
+
return {}, joins, filters
|
|
258
|
+
levels: dict = {}
|
|
259
|
+
for j in joins:
|
|
260
|
+
levels.setdefault(j[1][0], []).append(("join", j))
|
|
261
|
+
for f in filters:
|
|
262
|
+
levels.setdefault(f["row"], []).append(("filter", f))
|
|
263
|
+
return levels, [], []
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _key_scope(L: list, plan: dict) -> None:
|
|
267
|
+
"""Emit the §181.2 forward predicate graph for KEY_CONFLICT locality."""
|
|
268
|
+
readers: dict[str, set[str]] = {}
|
|
269
|
+
for rule in plan["rules"]:
|
|
270
|
+
head = rule["head"]["pred"]
|
|
271
|
+
for atom in rule.get("atoms") or []:
|
|
272
|
+
readers.setdefault(atom["pred"], set()).add(head)
|
|
273
|
+
for check in rule.get("checks") or []:
|
|
274
|
+
readers.setdefault(check["pred"], set()).add(head)
|
|
275
|
+
L.append("# E-0210/E-0242: конфликт ключа блокирует только читающий его запрос.")
|
|
276
|
+
L.append("KEY_READERS = " + repr({key: sorted(value) for key, value in sorted(readers.items())}))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def emit(plan: dict) -> str:
|
|
280
|
+
check_plan(plan)
|
|
281
|
+
if plan.get("planVersion") == "law-backend-plan/0.20":
|
|
282
|
+
plan = dict(plan, planVersion=plan["basePlanVersion"],
|
|
283
|
+
capabilities=[c for c in plan["capabilities"] if c != "admission:current"])
|
|
284
|
+
if _is_l1(plan):
|
|
285
|
+
return emit_l1(plan)
|
|
286
|
+
L = ["import runtime as RT", "", "TABLES = {"]
|
|
287
|
+
for t in plan["tables"]:
|
|
288
|
+
L.append(f' {json.dumps(t["pred"])}: [],')
|
|
289
|
+
L.append("}")
|
|
290
|
+
_key_scope(L, plan)
|
|
291
|
+
L.append("")
|
|
292
|
+
L.append("# Объявленные типы параметров: раннер сверяет с ними аргументы дела (§189).")
|
|
293
|
+
L.append("PARAMS = {")
|
|
294
|
+
for t in plan["tables"]:
|
|
295
|
+
L.append(f' {json.dumps(t["pred"])}: {json.dumps(t["params"], ensure_ascii=False)},')
|
|
296
|
+
L.append("}")
|
|
297
|
+
L.append("")
|
|
298
|
+
L.append("# Константы §54: тип для проверки §189 и значение для разыменования аргументов дела.")
|
|
299
|
+
L.append("CONSTS = {")
|
|
300
|
+
for cid, c in plan["consts"].items():
|
|
301
|
+
L.append(f' {json.dumps(cid)}: ({json.dumps(c["type"])}, {term(c["term"])}),')
|
|
302
|
+
L.append("}")
|
|
303
|
+
L.append("")
|
|
304
|
+
L.append("# Наследование сущностей: подтип подходит параметру базового типа.")
|
|
305
|
+
L.append(f'BASES = {json.dumps(plan["bases"], ensure_ascii=False)}')
|
|
306
|
+
_functions(L, plan)
|
|
307
|
+
_judgments(L, plan)
|
|
308
|
+
_calendar_env(L, plan)
|
|
309
|
+
L.append("")
|
|
310
|
+
L.append("# Ключи отношений §45.1: позиции столбцов каждого ключа.")
|
|
311
|
+
L.append("KEYS = {")
|
|
312
|
+
for t in plan["tables"]:
|
|
313
|
+
if t["keys"]:
|
|
314
|
+
L.append(f' {json.dumps(t["pred"])}: {json.dumps(t["keys"])},')
|
|
315
|
+
L.append("}")
|
|
316
|
+
L.append("")
|
|
317
|
+
L.append("# E-0243 §181.2: forward predicate dependencies for carrier issues.")
|
|
318
|
+
L.append("CARRIER_EDGES = {")
|
|
319
|
+
edges = {}
|
|
320
|
+
for r in plan["rules"]:
|
|
321
|
+
edges.setdefault(r["head"]["pred"], []).extend(atom["pred"] for atom in r.get("atoms", []))
|
|
322
|
+
for head, deps in sorted(edges.items()):
|
|
323
|
+
L.append(f' {json.dumps(head)}: {json.dumps(list(dict.fromkeys(deps)))},')
|
|
324
|
+
L.append("}")
|
|
325
|
+
L.append("")
|
|
326
|
+
L.append("_seen = set()")
|
|
327
|
+
L.append("")
|
|
328
|
+
L.append("def add(urn, row):")
|
|
329
|
+
L.append(' k = (urn, RT.key(row))')
|
|
330
|
+
L.append(" if k in _seen: return False")
|
|
331
|
+
L.append(" _seen.add(k); TABLES[urn].append(row); return True")
|
|
332
|
+
L.append("")
|
|
333
|
+
for r in plan["rules"]:
|
|
334
|
+
L.append(f'# {r["id"]}')
|
|
335
|
+
L.append(f'def rule_{r["name"]}():')
|
|
336
|
+
L.append(" grew = False")
|
|
337
|
+
if r["window"]:
|
|
338
|
+
if r["window"].get("start"):
|
|
339
|
+
L.append(f' if RT.legal_time() < "{r["window"]["start"]}": return False')
|
|
340
|
+
if r["window"].get("end"):
|
|
341
|
+
L.append(f' if RT.legal_time() >= "{r["window"]["end"]}": return False')
|
|
342
|
+
ind = " "
|
|
343
|
+
levels, late_joins, late_filters = _early(r)
|
|
344
|
+
for i, a in enumerate(r["atoms"]):
|
|
345
|
+
_py_wild_open(L, ind, i, a)
|
|
346
|
+
L.append(f'{ind}for r{i} in TABLES[{json.dumps(a["pred"])}]:')
|
|
347
|
+
ind += " "
|
|
348
|
+
_py_checks(L, ind, levels.get(i, []))
|
|
349
|
+
_py_wild(L, ind, i, a)
|
|
350
|
+
for (ai, aj, _at), (bi, bj, _bt) in late_joins:
|
|
351
|
+
L.append(f'{ind}if not RT.cmp(r{ai}[{aj}], "eq", r{bi}[{bj}]): continue')
|
|
352
|
+
for f in late_filters:
|
|
353
|
+
L.append(f'{ind}if not RT.eq(r{f["row"]}[{f["col"]}], {term(f["term"])}): continue')
|
|
354
|
+
for g in r["guards"]:
|
|
355
|
+
L.append(f'{ind}if not RT.cmp({term(g["l"])}, "{g["op"]}", {term(g["r"])}): continue')
|
|
356
|
+
args = ", ".join(term(a) for a in r["head"]["args"])
|
|
357
|
+
L.append(f'{ind}if add({json.dumps(r["head"]["pred"])}, [{args}]): grew = True')
|
|
358
|
+
L.append(" return grew")
|
|
359
|
+
L.append("")
|
|
360
|
+
names = [r["name"] for r in plan["rules"]]
|
|
361
|
+
L.append("def saturate():")
|
|
362
|
+
L.append(" grew = True")
|
|
363
|
+
L.append(" while grew:")
|
|
364
|
+
if names:
|
|
365
|
+
L.append(" grew = any([" + ", ".join(f"rule_{n}()" for n in names) + "])")
|
|
366
|
+
else:
|
|
367
|
+
L.append(" grew = False")
|
|
368
|
+
L.append("")
|
|
369
|
+
L.append("def register(urn, row):")
|
|
370
|
+
L.append(" add(urn, row)")
|
|
371
|
+
L.append("")
|
|
372
|
+
L.append("# Статус строки §62 у модели L0 — для обхода запроса судье §47.3 (DECISION-0274).")
|
|
373
|
+
L.append("def status(urn, row):")
|
|
374
|
+
L.append(' return "TRUE_ONLY" if any(len(r) == len(row) and all(RT.eq(v, row[i]) for i, v in enumerate(r)) for r in TABLES.get(urn, [])) else "NEITHER"')
|
|
375
|
+
L.append("")
|
|
376
|
+
L.append("# KEY_CONFLICT §45.1/§196: группы строк одной таблицы с одинаковой проекцией")
|
|
377
|
+
L.append("# на ключ; строки уникальны по ключу строки, поэтому группа из двух и более")
|
|
378
|
+
L.append("# строк — различные accepted-кортежи. Считается после насыщения.")
|
|
379
|
+
L.append("def key_conflicts():")
|
|
380
|
+
L.append(" n = 0")
|
|
381
|
+
L.append(" for urn, keys in KEYS.items():")
|
|
382
|
+
L.append(" for key in keys:")
|
|
383
|
+
L.append(" groups = {}")
|
|
384
|
+
L.append(" for row in TABLES[urn]:")
|
|
385
|
+
L.append(" p = RT.key([row[i] for i in key])")
|
|
386
|
+
L.append(" groups[p] = groups.get(p, 0) + 1")
|
|
387
|
+
L.append(" n += sum(1 for c in groups.values() if c > 1)")
|
|
388
|
+
L.append(" return n")
|
|
389
|
+
L.append("def key_conflict_predicates():")
|
|
390
|
+
L.append(" out = set()")
|
|
391
|
+
L.append(" for urn, keys in KEYS.items():")
|
|
392
|
+
L.append(" for key in keys:")
|
|
393
|
+
L.append(" groups = {}")
|
|
394
|
+
L.append(" for row in TABLES[urn]:")
|
|
395
|
+
L.append(" p = RT.key([row[i] for i in key]); groups[p] = groups.get(p, 0) + 1")
|
|
396
|
+
L.append(" if any(c > 1 for c in groups.values()): out.add(urn)")
|
|
397
|
+
L.append(" return out")
|
|
398
|
+
L.append("")
|
|
399
|
+
L.append("# Собственные факты пакета; `reset` возвращает модель к ним (чистое `evaluate` API).")
|
|
400
|
+
L.append("def package_facts():")
|
|
401
|
+
for f in plan["facts"]:
|
|
402
|
+
L.append(f' add({json.dumps(f["pred"])}, [' + ", ".join(term(a) for a in f["args"]) + '])')
|
|
403
|
+
L.append(" pass")
|
|
404
|
+
L.append("")
|
|
405
|
+
L.append("def reset():")
|
|
406
|
+
L.append(" for rows in TABLES.values(): rows.clear()")
|
|
407
|
+
L.append(" _seen.clear()")
|
|
408
|
+
L.append(" package_facts()")
|
|
409
|
+
L.append("")
|
|
410
|
+
L.append("package_facts()")
|
|
411
|
+
return _factory("\n".join(L) + "\n", plan)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _factory(src: str, plan: dict) -> str:
|
|
415
|
+
"""Модель — фабрика состояния на вызов (DECISION-0423 §2.3). Тело модуля
|
|
416
|
+
модели целиком становится телом `new_model()`: таблицы, опоры, кандидаты
|
|
417
|
+
и issues — локальные переменные вызова, правила — замыкания над ними;
|
|
418
|
+
`global` модуля становится `nonlocal`. Возвращается пространство имён тех
|
|
419
|
+
же имён, что раньше были атрибутами модуля, поэтому вход `evaluate` читает
|
|
420
|
+
модель как прежде, но каждый вызов получает своё состояние."""
|
|
421
|
+
src += "NODE_IDS = " + repr((plan.get("admission") or {}).get("reservedAssertionIds", [])) + "\n"
|
|
422
|
+
lines = src.rstrip("\n").split("\n")
|
|
423
|
+
if lines[0] != "import runtime as RT":
|
|
424
|
+
raise ValueError("модель без заголовка импорта рантайма")
|
|
425
|
+
out = ["import types as _types", "import runtime as RT", "", "",
|
|
426
|
+
"def new_model():",
|
|
427
|
+
' """Новое состояние модели на одно вычисление (DECISION-0423 §2.3)."""']
|
|
428
|
+
for ln in lines[1:]:
|
|
429
|
+
if not ln.strip():
|
|
430
|
+
out.append("")
|
|
431
|
+
continue
|
|
432
|
+
if ln.lstrip().startswith("global "):
|
|
433
|
+
ln = ln.replace("global ", "nonlocal ", 1)
|
|
434
|
+
out.append(" " + ln)
|
|
435
|
+
out.append(" return _types.SimpleNamespace(**{k: v for k, v in locals().items() if not k.startswith('__')})")
|
|
436
|
+
return "\n".join(out) + "\n"
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# --- поражаемый слой L1 (DECISION-0237) ---------------------------------------
|
|
440
|
+
# Опоры хранятся битами происхождения на ключ строки: strict/asserted и
|
|
441
|
+
# surviving-defeasible по обеим полярностям (F3); TruthStatus §62 выводится
|
|
442
|
+
# при чтении. Кандидаты — отдельная таблица с идентичностью «правило +
|
|
443
|
+
# подстановка», defeat — локальный helper над полным снимком кандидатов.
|
|
444
|
+
# Страты и порядок фаз §111 решил план; эмиттер печатает их циклами.
|
|
445
|
+
|
|
446
|
+
def _steps(L: list, body: dict, ind: str) -> str:
|
|
447
|
+
"""Шаги тела плана 0.9 в порядке `solve`: генератор открывает цикл, его
|
|
448
|
+
соединения и фильтры стоят сразу за ним; проверка и гард — там, где их
|
|
449
|
+
переменные связаны. Возвращает отступ самого внутреннего уровня."""
|
|
450
|
+
L.append(f"{ind}for _once in (0,):")
|
|
451
|
+
ind += " "
|
|
452
|
+
for step in body["steps"]:
|
|
453
|
+
if "gen" in step:
|
|
454
|
+
i = step["gen"]
|
|
455
|
+
a = body["atoms"][i]
|
|
456
|
+
p = json.dumps(a["pred"])
|
|
457
|
+
for f in body["filters"]:
|
|
458
|
+
if f["row"] == i and f.get("pre"):
|
|
459
|
+
# DECISION-0270: вычисляемый аргумент считается ДО перебора
|
|
460
|
+
# таблицы (`solver.solve`); ошибка значения — код по подстановке.
|
|
461
|
+
name = f'pre{i}_{f["col"]}'
|
|
462
|
+
if f.get("err"):
|
|
463
|
+
L.append(f'{ind}try:')
|
|
464
|
+
L.append(f'{ind} {name} = {term(f["term"])}')
|
|
465
|
+
L.append(f'{ind}except RT.TermError as _e:')
|
|
466
|
+
L.append(f'{ind} _codes.add(_e.code); continue')
|
|
467
|
+
else:
|
|
468
|
+
L.append(f'{ind}{name} = {term(f["term"])}')
|
|
469
|
+
_py_wild_open(L, ind, i, a)
|
|
470
|
+
L.append(f'{ind}for i{i}, r{i} in enumerate(TABLES[{p}]):')
|
|
471
|
+
ind += " "
|
|
472
|
+
L.append(f'{ind}if mask_status(SUP[{p}][RK[{p}][i{i}]]) not in {tuple(a["accept"])!r}: continue')
|
|
473
|
+
for (ai, aj, _at), (bi, bj, _bt) in body["joins"]:
|
|
474
|
+
if bi == i:
|
|
475
|
+
L.append(f'{ind}if not RT.cmp(r{ai}[{aj}], "eq", r{bi}[{bj}]): continue')
|
|
476
|
+
for f in body["filters"]:
|
|
477
|
+
if f["row"] == i:
|
|
478
|
+
value = f'pre{i}_{f["col"]}' if f.get("pre") else term(f["term"])
|
|
479
|
+
L.append(f'{ind}if not RT.eq(r{f["row"]}[{f["col"]}], {value}): continue')
|
|
480
|
+
_py_wild(L, ind, i, a)
|
|
481
|
+
elif "check" in step:
|
|
482
|
+
c = body["checks"][step["check"]]
|
|
483
|
+
args = ", ".join(term(a) for a in c["args"])
|
|
484
|
+
if c.get("err"):
|
|
485
|
+
# DECISION-0270: ошибка значения аргумента проверки — код по подстановке.
|
|
486
|
+
L.append(f'{ind}try:')
|
|
487
|
+
L.append(f'{ind} _a = [{args}]')
|
|
488
|
+
L.append(f'{ind}except RT.TermError as _e:')
|
|
489
|
+
L.append(f'{ind} _codes.add(_e.code); continue')
|
|
490
|
+
L.append(f'{ind}if status({json.dumps(c["pred"])}, _a) not in {tuple(c["accept"])!r}: continue')
|
|
491
|
+
else:
|
|
492
|
+
L.append(f'{ind}if status({json.dumps(c["pred"])}, [{args}]) not in {tuple(c["accept"])!r}: continue')
|
|
493
|
+
else:
|
|
494
|
+
g = body["guards"][step["guard"]]
|
|
495
|
+
if g.get("cal") or g.get("err"):
|
|
496
|
+
# DECISION-0245 п. 6, DECISION-0252 п. 4: код ошибки значения — по подстановкам.
|
|
497
|
+
L.append(f'{ind}try:')
|
|
498
|
+
L.append(f'{ind} _ok = RT.cmp({term(g["l"])}, "{g["op"]}", {term(g["r"])})')
|
|
499
|
+
L.append(f'{ind}except RT.TermError as _e:')
|
|
500
|
+
L.append(f'{ind} _codes.add(_e.code); continue')
|
|
501
|
+
L.append(f'{ind}if not _ok: continue')
|
|
502
|
+
else:
|
|
503
|
+
L.append(f'{ind}if not RT.cmp({term(g["l"])}, "{g["op"]}", {term(g["r"])}): continue')
|
|
504
|
+
return ind
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _aggregates(L: list, plan: dict) -> None:
|
|
508
|
+
"""Подпланы агрегатов §59: материализация comprehension §52.1 по
|
|
509
|
+
установленным строкам и свёртка; ошибка значения — `AggError` с кодом."""
|
|
510
|
+
for a in plan.get("aggregates") or []:
|
|
511
|
+
params = ", ".join(f"p{i}" for i in range(len(a["params"])))
|
|
512
|
+
L.append(f'def items_{a["name"]}({params}):')
|
|
513
|
+
L.append(" items, seen = [], set()")
|
|
514
|
+
ind = _steps(L, a, " ")
|
|
515
|
+
L.append(f'{ind}v = {term(a["element"])}')
|
|
516
|
+
if a["distinct"]:
|
|
517
|
+
L.append(f"{ind}k = RT.key([v])")
|
|
518
|
+
L.append(f"{ind}if k in seen: continue")
|
|
519
|
+
L.append(f"{ind}seen.add(k)")
|
|
520
|
+
L.append(f"{ind}items.append(v)")
|
|
521
|
+
L.append(" return items")
|
|
522
|
+
L.append("")
|
|
523
|
+
L.append(f'def {a["name"]}({params}):')
|
|
524
|
+
L.append(f' items = items_{a["name"]}({params})')
|
|
525
|
+
if a["op"] == "count":
|
|
526
|
+
L.append(" return RT.dec(str(len(items)))")
|
|
527
|
+
else:
|
|
528
|
+
L.append(" if not items:")
|
|
529
|
+
if a["empty"] is not None:
|
|
530
|
+
L.append(f' return {term(a["empty"])}')
|
|
531
|
+
else:
|
|
532
|
+
L.append(' raise AggError("EMPTY_AGGREGATE")')
|
|
533
|
+
if a["op"] == "only":
|
|
534
|
+
# §51 (errata E-0256): единственное РАЗЛИЧНОЕ значение — по
|
|
535
|
+
# каноническому ключу, тем же, что у distinct; кратность безразлична.
|
|
536
|
+
L.append(" if len({RT.key([x]) for x in items}) > 1:")
|
|
537
|
+
L.append(' raise AggError("CARDINALITY_CONFLICT")')
|
|
538
|
+
L.append(" return items[0]")
|
|
539
|
+
else:
|
|
540
|
+
L.append(" acc = items[0]")
|
|
541
|
+
fn = {"sum": "add", "min": "min", "max": "max"}[a["op"]]
|
|
542
|
+
L.append(f" for x in items[1:]: acc = RT.{fn}(acc, x)")
|
|
543
|
+
L.append(" return acc")
|
|
544
|
+
L.append("")
|
|
545
|
+
L.append(f'def neg_{a["name"]}({params}):')
|
|
546
|
+
L.append(" # E-0116: отрицательное слагаемое под монотонным гардом sum")
|
|
547
|
+
L.append(f' return any(RT.cmp(x, "lt", RT.sub(x, x)) for x in items_{a["name"]}({params}))')
|
|
548
|
+
L.append("")
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _rule09(L: list, r: dict, value: bool = False) -> None:
|
|
552
|
+
"""Правило плана 0.9. Фоллибельное (агрегат с ошибкой значения) копит
|
|
553
|
+
выводы и применяет их только без ошибки — как `list(solve(...))` оракула.
|
|
554
|
+
`value` — план 0.12 (DECISION-0252): ошибки значения §58 и сверка вида."""
|
|
555
|
+
L.append(f'# {r["id"]} ({r["strength"]})')
|
|
556
|
+
L.append(f'def rule_{r["name"]}():')
|
|
557
|
+
L.append(" grew = False")
|
|
558
|
+
if r["window"]:
|
|
559
|
+
if r["window"].get("start"):
|
|
560
|
+
L.append(f' if RT.legal_time() < "{r["window"]["start"]}": return False')
|
|
561
|
+
if r["window"].get("end"):
|
|
562
|
+
L.append(f' if RT.legal_time() >= "{r["window"]["end"]}": return False')
|
|
563
|
+
L.append(" out = []")
|
|
564
|
+
cal = r.get("headFallible") or any(g.get("cal") or g.get("err") for g in r["guards"]) \
|
|
565
|
+
or any(f.get("err") for f in r["filters"]) or any(c.get("err") for c in r["checks"])
|
|
566
|
+
if cal:
|
|
567
|
+
L.append(" _codes, _hcodes = set(), set()")
|
|
568
|
+
L.append(" try:")
|
|
569
|
+
ind = _steps(L, r, " ")
|
|
570
|
+
for a in r.get("addends") or []:
|
|
571
|
+
args = ", ".join(term(x) for x in a["args"])
|
|
572
|
+
L.append(f'{ind}if neg_{a["fn"]}({args}):')
|
|
573
|
+
L.append(f'{ind} issue({json.dumps(r["name"])}, "MONOTONE_GUARD_NEGATIVE_ADDEND"); continue')
|
|
574
|
+
args = ", ".join(term(a) for a in r["head"]["args"])
|
|
575
|
+
pos = r["head"]["polarity"] == "positive"
|
|
576
|
+
if r.get("headFallible"):
|
|
577
|
+
L.append(f"{ind}try:")
|
|
578
|
+
L.append(f"{ind} _row = [{args}]")
|
|
579
|
+
L.append(f"{ind}except (RT.TermError, AggError) as _e:" if r.get("headAgg") else f"{ind}except RT.TermError as _e:")
|
|
580
|
+
L.append(f"{ind} _hcodes.add(_e.code); continue")
|
|
581
|
+
args = "*_row"
|
|
582
|
+
elif r.get("kindChecks"):
|
|
583
|
+
L.append(f"{ind}_row = [{args}]")
|
|
584
|
+
args = "*_row"
|
|
585
|
+
for pos, want in r.get("kindChecks") or []:
|
|
586
|
+
# E-0077 (DECISION-0252 п. 5): вид значения против слота — вывод не принят.
|
|
587
|
+
L.append(f'{ind}if RT.kind_of(_row[{pos}]) != "{want}":')
|
|
588
|
+
L.append(f'{ind} kind_issue({json.dumps(r["name"])}, {json.dumps(r["head"]["pred"])}, {pos}, _row[{pos}]); continue')
|
|
589
|
+
L.append(f"{ind}out.append([{args}])")
|
|
590
|
+
if r["strength"] == "strict":
|
|
591
|
+
pass
|
|
592
|
+
else:
|
|
593
|
+
subst = ", ".join(term(a) for a in r["subst"])
|
|
594
|
+
L[-1] = f"{ind}out.append(([{args}], [{subst}]))"
|
|
595
|
+
L.append(" except AggError as exc:")
|
|
596
|
+
L.append(f' issue({json.dumps(r["name"])}, exc.code)')
|
|
597
|
+
L.append(" return False")
|
|
598
|
+
if cal:
|
|
599
|
+
name = json.dumps(r["name"])
|
|
600
|
+
L.append(" # §58: ошибка гарда снимает правило на проход; ошибка головы снимает только")
|
|
601
|
+
L.append(" # свою подстановку, issue — по каждому коду однажды (errata E-0265).")
|
|
602
|
+
what, why = ("значения", "DECISION-0252 п. 4") if value else ("календаря", "DECISION-0245 п. 6")
|
|
603
|
+
L.append(" if _codes:")
|
|
604
|
+
L.append(" if len(_codes) > 1:")
|
|
605
|
+
L.append(f' raise RT.Refused("коды ошибок {what} в гарде зависят от порядка подстановок ({why}): " + {name})')
|
|
606
|
+
L.append(f" issue({name}, next(iter(_codes))); return False")
|
|
607
|
+
L.append(" for _c in sorted(_hcodes):")
|
|
608
|
+
L.append(f" issue({name}, _c)")
|
|
609
|
+
if r["strength"] == "strict":
|
|
610
|
+
L.append(f' for row in out:')
|
|
611
|
+
L.append(f' if add({json.dumps(r["head"]["pred"])}, row, {"SP" if pos else "SN"}): grew = True')
|
|
612
|
+
else:
|
|
613
|
+
L.append(f' for row, subst in out:')
|
|
614
|
+
L.append(f' cand({json.dumps(r["name"])}, {r["strength"] == "defeater"}, '
|
|
615
|
+
f'{json.dumps(r["head"]["pred"])}, {pos}, row, subst)')
|
|
616
|
+
L.append(" return grew")
|
|
617
|
+
L.append("")
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _waiting_strata(L: list, plan: dict, line, comment: str) -> dict:
|
|
621
|
+
"""Страты strict-прохода без ждущих правил (errata E-0174): у каждого
|
|
622
|
+
набора `waiting` расписания — своя константа. Кто ждёт, решил план;
|
|
623
|
+
эмиттер лишь вычёркивает имена. Возвращает набор → имя константы."""
|
|
624
|
+
out: dict = {}
|
|
625
|
+
for ph in plan["schedule"]:
|
|
626
|
+
if ph["phase"] in PHASE_FIELDS and set(ph) - PHASE_FIELDS[ph["phase"]]:
|
|
627
|
+
raise ValueError(f"поле фазы расписания не поддержано эмиттером: "
|
|
628
|
+
f"{sorted(set(ph) - PHASE_FIELDS[ph['phase']])}")
|
|
629
|
+
key = tuple(ph.get("waiting") or ())
|
|
630
|
+
if not key or key in out:
|
|
631
|
+
continue
|
|
632
|
+
if not set(key) <= {n for s in plan["strata"]["strict"] for n in s}:
|
|
633
|
+
raise ValueError(f"ждущее правило вне strict-страт плана: {', '.join(key)}")
|
|
634
|
+
out[key] = f"STRICT_WAIT_{len(out)}"
|
|
635
|
+
L.append(f"{comment} §70/§231.1 (E-0174): проход до замыкания — без {', '.join(key)}")
|
|
636
|
+
L.append(line(out[key], [[n for n in s if n not in key] for s in plan["strata"]["strict"]]))
|
|
637
|
+
return out
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def emit_l1(plan: dict) -> str:
|
|
641
|
+
L = ["import runtime as RT", "",
|
|
642
|
+
"# Модель поражаемого слоя L1 (DECISION-0237): опоры с происхождением,",
|
|
643
|
+
"# кандидаты и defeat по стратам §111; статус §62 выводится при чтении.",
|
|
644
|
+
'LAYER = "L1"', "", "TABLES = {"]
|
|
645
|
+
for t in plan["tables"]:
|
|
646
|
+
L.append(f' {json.dumps(t["pred"])}: [],')
|
|
647
|
+
L.append("}")
|
|
648
|
+
_key_scope(L, plan)
|
|
649
|
+
L.append("# Ключи строк таблиц — параллельно строкам; опоры — биты на ключ строки.")
|
|
650
|
+
L.append("RK = {urn: [] for urn in TABLES}")
|
|
651
|
+
L.append("SUP = {urn: {} for urn in TABLES}")
|
|
652
|
+
L.append("SP, SN, DP, DN = 1, 2, 4, 8 # strict/asserted +/−, surviving defeasible +/−")
|
|
653
|
+
L.append("")
|
|
654
|
+
L.append("# Объявленные типы параметров: раннер сверяет с ними аргументы дела (§189).")
|
|
655
|
+
L.append("PARAMS = {")
|
|
656
|
+
for t in plan["tables"]:
|
|
657
|
+
L.append(f' {json.dumps(t["pred"])}: {json.dumps(t["params"], ensure_ascii=False)},')
|
|
658
|
+
L.append("}")
|
|
659
|
+
L.append("")
|
|
660
|
+
L.append("# Константы §54: тип для проверки §189 и значение для разыменования аргументов дела.")
|
|
661
|
+
L.append("CONSTS = {")
|
|
662
|
+
for cid, c in plan["consts"].items():
|
|
663
|
+
L.append(f' {json.dumps(cid)}: ({json.dumps(c["type"])}, {term(c["term"])}),')
|
|
664
|
+
L.append("}")
|
|
665
|
+
L.append("")
|
|
666
|
+
L.append("# Наследование сущностей: подтип подходит параметру базового типа.")
|
|
667
|
+
L.append(f'BASES = {json.dumps(plan["bases"], ensure_ascii=False)}')
|
|
668
|
+
_functions(L, plan)
|
|
669
|
+
_judgments(L, plan)
|
|
670
|
+
_calendar_env(L, plan)
|
|
671
|
+
L.append("")
|
|
672
|
+
L.append("# Ключи отношений §45.1: позиции столбцов каждого ключа.")
|
|
673
|
+
L.append("KEYS = {")
|
|
674
|
+
for t in plan["tables"]:
|
|
675
|
+
if t["keys"]:
|
|
676
|
+
L.append(f' {json.dumps(t["pred"])}: {json.dumps(t["keys"])},')
|
|
677
|
+
L.append("}")
|
|
678
|
+
L.append("")
|
|
679
|
+
L.append("# E-0243 §181.2: forward predicate dependencies for carrier issues.")
|
|
680
|
+
L.append("CARRIER_EDGES = {")
|
|
681
|
+
edges = {}
|
|
682
|
+
for r in plan["rules"]:
|
|
683
|
+
edges.setdefault(r["head"]["pred"], []).extend(atom["pred"] for atom in r.get("atoms", []))
|
|
684
|
+
for head, deps in sorted(edges.items()):
|
|
685
|
+
L.append(f' {json.dumps(head)}: {json.dumps(list(dict.fromkeys(deps)))},')
|
|
686
|
+
L.append("}")
|
|
687
|
+
L.append("")
|
|
688
|
+
L.append("def add(urn, row, bit):")
|
|
689
|
+
L.append(" k = RT.key(row)")
|
|
690
|
+
L.append(" sup = SUP[urn]")
|
|
691
|
+
L.append(" m = sup.get(k)")
|
|
692
|
+
L.append(" if m is None:")
|
|
693
|
+
L.append(" sup[k] = bit; TABLES[urn].append(row); RK[urn].append(k); return True")
|
|
694
|
+
L.append(" if m & bit: return False")
|
|
695
|
+
L.append(" sup[k] = m | bit; return True")
|
|
696
|
+
L.append("")
|
|
697
|
+
L.append("# §62: пара опор (t, f) → статус; опоры — объединение strict и surviving.")
|
|
698
|
+
L.append("def mask_status(m):")
|
|
699
|
+
L.append(" pos, neg = m & (SP | DP), m & (SN | DN)")
|
|
700
|
+
L.append(' return "BOTH" if pos and neg else "TRUE_ONLY" if pos else "FALSE_ONLY" if neg else "NEITHER"')
|
|
701
|
+
L.append("")
|
|
702
|
+
L.append("def status(urn, row):")
|
|
703
|
+
L.append(" sup = SUP.get(urn)")
|
|
704
|
+
L.append(' return "NEITHER" if sup is None else mask_status(sup.get(RT.key(row), 0))')
|
|
705
|
+
L.append("")
|
|
706
|
+
L.append("# Установленные строки (TRUE_ONLY) — перечисление collect и API, без BOTH.")
|
|
707
|
+
L.append("def established_rows(urn):")
|
|
708
|
+
L.append(" sup, keys = SUP.get(urn, {}), RK.get(urn, [])")
|
|
709
|
+
L.append(' return [r for i, r in enumerate(TABLES.get(urn, [])) if mask_status(sup[keys[i]]) == "TRUE_ONLY"]')
|
|
710
|
+
L.append("")
|
|
711
|
+
if _is_09(plan):
|
|
712
|
+
L.append("# Ошибка значения агрегата в правиле (DECISION-0239 G3): issue по правилу")
|
|
713
|
+
L.append("# и коду однажды, правило на проходе не срабатывает.")
|
|
714
|
+
L.append("class AggError(Exception):")
|
|
715
|
+
L.append(" def __init__(self, code):")
|
|
716
|
+
L.append(" super().__init__(code); self.code = code")
|
|
717
|
+
L.append("")
|
|
718
|
+
L.append("RULE_ISSUES, _ISSUE_SEEN = [], set()")
|
|
719
|
+
L.append("")
|
|
720
|
+
L.append("def issue(rule, code):")
|
|
721
|
+
L.append(" if (rule, code) not in _ISSUE_SEEN:")
|
|
722
|
+
L.append(" _ISSUE_SEEN.add((rule, code)); RULE_ISSUES.append(code)")
|
|
723
|
+
L.append("")
|
|
724
|
+
if plan["planVersion"] in (SUPPORTED_PLAN_12, SUPPORTED_PLAN_13, SUPPORTED_PLAN_14, SUPPORTED_PLAN_15, SUPPORTED_PLAN_16, SUPPORTED_PLAN_17, SUPPORTED_PLAN_18, SUPPORTED_PLAN_19):
|
|
725
|
+
L.append("# E-0077 (DECISION-0252 п. 5): TYPE_ERROR по правилу, предикату, позиции и")
|
|
726
|
+
L.append("# фактическому виду однажды; связывается с правилом для статуса вопроса §175.")
|
|
727
|
+
L.append("_KIND_SEEN = set()")
|
|
728
|
+
L.append("")
|
|
729
|
+
L.append("def kind_issue(rule, pred, pos, value):")
|
|
730
|
+
L.append(" k = (rule, pred, pos, RT.kind_of(value))")
|
|
731
|
+
L.append(" if k not in _KIND_SEEN:")
|
|
732
|
+
L.append(' _KIND_SEEN.add(k); issue(rule, "TYPE_ERROR")')
|
|
733
|
+
L.append("")
|
|
734
|
+
_aggregates(L, plan)
|
|
735
|
+
L.append("# Кандидаты §104 страты: идентичность — правило и типизированная подстановка.")
|
|
736
|
+
L.append("CANDS = {}")
|
|
737
|
+
L.append('STATS = {"candidates": 0, "groups": 0, "maxGroup": 0}')
|
|
738
|
+
L.append("")
|
|
739
|
+
L.append("def cand(rule, defeater, urn, positive, row, subst):")
|
|
740
|
+
L.append(" k = (rule, RT.key(subst))")
|
|
741
|
+
L.append(" if k not in CANDS: CANDS[k] = (rule, defeater, urn, positive, row, RT.key(row))")
|
|
742
|
+
L.append("")
|
|
743
|
+
L.append("# Приоритет §106: транзитивное замыкание безусловных рёбер (выше, ниже).")
|
|
744
|
+
L.append("PRIO = {" + ", ".join(f"({json.dumps(a)}, {json.dumps(b)})" for a, b in plan["priorities"]) + "}")
|
|
745
|
+
dated = "datedPriorities" in plan
|
|
746
|
+
prep = dated or bool(plan.get("a3Params"))
|
|
747
|
+
if dated:
|
|
748
|
+
L.append("# DECISION-0269: датированные рёбра §92.3 — окна [start, end) оси права; активное")
|
|
749
|
+
L.append("# множество готовится один раз на дело по legal_time (`prepare`, подготовленный контекст).")
|
|
750
|
+
L.append("DATED_PRIO = " + repr([((a, b), [tuple(w) for w in ws]) for a, b, ws in plan["datedPriorities"]]))
|
|
751
|
+
L.append("PRIO_ACTIVE = set()")
|
|
752
|
+
if prep:
|
|
753
|
+
L.append("")
|
|
754
|
+
L.append("def _in_window(w, t):")
|
|
755
|
+
L.append(" return (w[0] is None or t >= w[0]) and (w[1] is None or t < w[1])")
|
|
756
|
+
L.append("")
|
|
757
|
+
L.append("def prepare():")
|
|
758
|
+
L.append(" t = RT.legal_time()")
|
|
759
|
+
if dated:
|
|
760
|
+
L.append(" PRIO_ACTIVE.clear(); PRIO_ACTIVE.update(PRIO)")
|
|
761
|
+
L.append(" for pair, ws in DATED_PRIO:")
|
|
762
|
+
L.append(" if any(_in_window(w, t) for w in ws): PRIO_ACTIVE.add(pair)")
|
|
763
|
+
if "datedNormPriorities" in plan:
|
|
764
|
+
L.append(" NORM_PRIO_ACTIVE.clear(); NORM_PRIO_ACTIVE.update(NORM_PRIO)")
|
|
765
|
+
L.append(" for pair, ws in DATED_NORM_PRIO:")
|
|
766
|
+
L.append(" if any(_in_window(w, t) for w in ws): NORM_PRIO_ACTIVE.add(pair)")
|
|
767
|
+
if plan.get("a3Params"):
|
|
768
|
+
L.append(" # DECISION-0273: значение параметра A3 на legal_time (подготовленный контекст).")
|
|
769
|
+
L.append(" A3.clear()")
|
|
770
|
+
L.append(" for id, ws in A3_WINDOWS:")
|
|
771
|
+
L.append(" for s, e, v in ws:")
|
|
772
|
+
L.append(" if _in_window((s, e), t): A3[id] = v")
|
|
773
|
+
prio = "PRIO_ACTIVE" if dated else "PRIO"
|
|
774
|
+
L.append("")
|
|
775
|
+
L.append("# DECISION-0274: применения страт для пути запроса судье — дефитеры, давшие")
|
|
776
|
+
L.append("# применение (E-0103), и поражённые/уцелевшие поражаемые кандидаты (E-0100).")
|
|
777
|
+
L.append("APPLIED = []")
|
|
778
|
+
L.append("")
|
|
779
|
+
L.append("def support_bits(urn, row):")
|
|
780
|
+
L.append(" sup = SUP.get(urn)")
|
|
781
|
+
L.append(" return 0 if sup is None else sup.get(RT.key(row), 0)")
|
|
782
|
+
L.append("")
|
|
783
|
+
L.append("# Defeat §107 по ПОЛНОМУ снимку кандидатов страты; группа — ground-кортеж,")
|
|
784
|
+
L.append("# побеждённые не удаляются по ходу обхода. Возвращает выживших §108.")
|
|
785
|
+
L.append("def defeat():")
|
|
786
|
+
L.append(" groups, survived = {}, set()")
|
|
787
|
+
L.append(" for c in CANDS.values():")
|
|
788
|
+
L.append(" groups.setdefault((c[2], c[5]), []).append(c)")
|
|
789
|
+
L.append(' STATS["candidates"] += len(CANDS); STATS["groups"] += len(groups)')
|
|
790
|
+
L.append(" out = []")
|
|
791
|
+
L.append(" for g in groups.values():")
|
|
792
|
+
L.append(' STATS["maxGroup"] = max(STATS["maxGroup"], len(g))')
|
|
793
|
+
L.append(" for rule, defeater, urn, positive, row, k in g:")
|
|
794
|
+
L.append(" if defeater: continue")
|
|
795
|
+
L.append(" # §107.1: strict/asserted опора комплемента побеждает безусловно")
|
|
796
|
+
L.append(" if SUP[urn].get(k, 0) & (SN if positive else SP): continue")
|
|
797
|
+
L.append(" # §107.2: противоположный обычный кандидат сильнее")
|
|
798
|
+
L.append(f" if any(not o[1] and o[3] != positive and (o[0], rule) in {prio} for o in g): continue")
|
|
799
|
+
L.append(" # §107.3: дефитер на ту же полярность, не преодолённый приоритетом")
|
|
800
|
+
L.append(f" if any(d[1] and d[3] == positive and (rule, d[0]) not in {prio} for d in g): continue")
|
|
801
|
+
L.append(" out.append((urn, row, positive)); survived.add((rule, urn, positive, k))")
|
|
802
|
+
L.append(" for rule, defeater, urn, positive, row, k in CANDS.values():")
|
|
803
|
+
L.append(' APPLIED.append({"rule": rule, "defeater": defeater, "urn": urn, "positive": positive, "key": k,')
|
|
804
|
+
L.append(' "defeated": not defeater and (rule, urn, positive, k) not in survived})')
|
|
805
|
+
L.append(" return out")
|
|
806
|
+
L.append("")
|
|
807
|
+
for r in plan["rules"]:
|
|
808
|
+
if "steps" in r:
|
|
809
|
+
_rule09(L, r, plan["planVersion"] in (SUPPORTED_PLAN_12, SUPPORTED_PLAN_13, SUPPORTED_PLAN_14, SUPPORTED_PLAN_15, SUPPORTED_PLAN_16, SUPPORTED_PLAN_17, SUPPORTED_PLAN_18, SUPPORTED_PLAN_19))
|
|
810
|
+
continue
|
|
811
|
+
L.append(f'# {r["id"]} ({r["strength"]})')
|
|
812
|
+
L.append(f'def rule_{r["name"]}():')
|
|
813
|
+
L.append(" grew = False")
|
|
814
|
+
if r["window"]:
|
|
815
|
+
if r["window"].get("start"):
|
|
816
|
+
L.append(f' if RT.legal_time() < "{r["window"]["start"]}": return False')
|
|
817
|
+
if r["window"].get("end"):
|
|
818
|
+
L.append(f' if RT.legal_time() >= "{r["window"]["end"]}": return False')
|
|
819
|
+
ind = " "
|
|
820
|
+
levels, late_joins, late_filters = _early(r)
|
|
821
|
+
if not r["atoms"]:
|
|
822
|
+
L.append(f"{ind}for _once in (0,):")
|
|
823
|
+
ind += " "
|
|
824
|
+
for i, a in enumerate(r["atoms"]):
|
|
825
|
+
p = json.dumps(a["pred"])
|
|
826
|
+
_py_wild_open(L, ind, i, a)
|
|
827
|
+
L.append(f'{ind}for i{i}, r{i} in enumerate(TABLES[{p}]):')
|
|
828
|
+
ind += " "
|
|
829
|
+
L.append(f'{ind}if mask_status(SUP[{p}][RK[{p}][i{i}]]) not in {tuple(a["accept"])!r}: continue')
|
|
830
|
+
_py_checks(L, ind, levels.get(i, []))
|
|
831
|
+
_py_wild(L, ind, i, a)
|
|
832
|
+
for (ai, aj, _at), (bi, bj, _bt) in late_joins:
|
|
833
|
+
L.append(f'{ind}if not RT.cmp(r{ai}[{aj}], "eq", r{bi}[{bj}]): continue')
|
|
834
|
+
for f in late_filters:
|
|
835
|
+
L.append(f'{ind}if not RT.eq(r{f["row"]}[{f["col"]}], {term(f["term"])}): continue')
|
|
836
|
+
for c in r["checks"]:
|
|
837
|
+
args = ", ".join(term(a) for a in c["args"])
|
|
838
|
+
L.append(f'{ind}if status({json.dumps(c["pred"])}, [{args}]) not in {tuple(c["accept"])!r}: continue')
|
|
839
|
+
for g in r["guards"]:
|
|
840
|
+
L.append(f'{ind}if not RT.cmp({term(g["l"])}, "{g["op"]}", {term(g["r"])}): continue')
|
|
841
|
+
args = ", ".join(term(a) for a in r["head"]["args"])
|
|
842
|
+
pos = r["head"]["polarity"] == "positive"
|
|
843
|
+
if r["strength"] == "strict":
|
|
844
|
+
L.append(f'{ind}if add({json.dumps(r["head"]["pred"])}, [{args}], {"SP" if pos else "SN"}): grew = True')
|
|
845
|
+
else:
|
|
846
|
+
subst = ", ".join(term(a) for a in r["subst"])
|
|
847
|
+
L.append(f'{ind}cand({json.dumps(r["name"])}, {r["strength"] == "defeater"}, '
|
|
848
|
+
f'{json.dumps(r["head"]["pred"])}, {pos}, [{args}], [{subst}])')
|
|
849
|
+
L.append(" return grew")
|
|
850
|
+
L.append("")
|
|
851
|
+
L.append("# Страты строгого слоя §110 (DECISION-0088): каждая — наименьшая неподвижная точка.")
|
|
852
|
+
L.append("STRICT = [" + ", ".join("[" + ", ".join(f"rule_{n}" for n in s) + "]" for s in plan["strata"]["strict"]) + "]")
|
|
853
|
+
waiting = _waiting_strata(L, plan, lambda name, strata: f"{name} = [" + ", ".join(
|
|
854
|
+
"[" + ", ".join(f"rule_{n}" for n in s) + "]" for s in strata) + "]", "#")
|
|
855
|
+
L.append("")
|
|
856
|
+
L.append("def strict_closure(strata=STRICT):")
|
|
857
|
+
L.append(" for stratum in strata:")
|
|
858
|
+
L.append(" grew = True")
|
|
859
|
+
L.append(" while grew:")
|
|
860
|
+
L.append(" grew = any([rule() for rule in stratum])")
|
|
861
|
+
L.append("")
|
|
862
|
+
for c in plan.get("closures") or []:
|
|
863
|
+
d, pr = json.dumps(c["domain"]), json.dumps(c["pred"])
|
|
864
|
+
L.append(f'# {c["id"]}: замыкание §70 — негатив члену домена без strict/asserted positive-опоры')
|
|
865
|
+
L.append(f'def {c["name"]}():')
|
|
866
|
+
if c["window"]:
|
|
867
|
+
if c["window"].get("start"):
|
|
868
|
+
L.append(f' if RT.legal_time() < "{c["window"]["start"]}": return')
|
|
869
|
+
if c["window"].get("end"):
|
|
870
|
+
L.append(f' if RT.legal_time() >= "{c["window"]["end"]}": return')
|
|
871
|
+
L.append(f" members = [r for i, r in enumerate(TABLES[{d}]) if mask_status(SUP[{d}][RK[{d}][i]]) == \"TRUE_ONLY\"]")
|
|
872
|
+
L.append(" for row in members:")
|
|
873
|
+
L.append(f" if SUP[{pr}].get(RT.key(row), 0) & SP: continue")
|
|
874
|
+
L.append(f" add({pr}, list(row), SN)")
|
|
875
|
+
L.append("")
|
|
876
|
+
if "norms" in plan:
|
|
877
|
+
raise ValueError("NORM_RANKS_UNAVAILABLE")
|
|
878
|
+
L.append("# Расписание фаз §111, решённое планом.")
|
|
879
|
+
L.append("def saturate():")
|
|
880
|
+
if prep:
|
|
881
|
+
L.append(" prepare()")
|
|
882
|
+
for ph in plan["schedule"]:
|
|
883
|
+
kind = ph["phase"]
|
|
884
|
+
if kind not in PHASES:
|
|
885
|
+
raise ValueError(f"фаза расписания не поддержана эмиттером: {kind!r}")
|
|
886
|
+
if kind == "strict":
|
|
887
|
+
L.append(f" strict_closure({waiting[tuple(ph['waiting'])]})" if ph.get("waiting") else " strict_closure()")
|
|
888
|
+
elif kind == "closure":
|
|
889
|
+
if not ph.get("policies"):
|
|
890
|
+
L.append(" # §70: фаза closure зарезервирована (DECISION-0237 F7), политик в срезе нет")
|
|
891
|
+
for name in ph.get("policies") or []:
|
|
892
|
+
L.append(f" {name}()")
|
|
893
|
+
elif kind == "candidates":
|
|
894
|
+
L.append(f' # страта {ph["stratum"]}: кандидаты §111 шаги 2–3')
|
|
895
|
+
L.append(" CANDS.clear()")
|
|
896
|
+
for n in ph["rules"]:
|
|
897
|
+
L.append(f" rule_{n}()")
|
|
898
|
+
elif kind == "defeat":
|
|
899
|
+
L.append(" surviving = defeat()")
|
|
900
|
+
elif kind == "surviving":
|
|
901
|
+
L.append(" for urn, row, positive in surviving: add(urn, row, DP if positive else DN)")
|
|
902
|
+
elif kind == "norms":
|
|
903
|
+
L.append(" # §135: позиции норм-правил после поражаемого слоя (DECISION-0242 K3)")
|
|
904
|
+
for n in plan.get("norms") or []:
|
|
905
|
+
L.append(f" norm_{n['name']}()")
|
|
906
|
+
elif kind == "effects":
|
|
907
|
+
L.append(" materialize_effects() # §127–§128")
|
|
908
|
+
else:
|
|
909
|
+
L.append(f' # страта {ph["stratum"]}: статус фиксирован, выводится при чтении (§111 шаг 7)')
|
|
910
|
+
L.append(" pass")
|
|
911
|
+
L.append("")
|
|
912
|
+
L.append('def register(urn, row, polarity="positive"):')
|
|
913
|
+
L.append(' add(urn, row, SP if polarity == "positive" else SN)')
|
|
914
|
+
L.append("")
|
|
915
|
+
L.append("# KEY_CONFLICT §45.1/§196: считаются accepted-кортежи — strict/asserted positive-опоры.")
|
|
916
|
+
L.append("def key_conflicts():")
|
|
917
|
+
L.append(" n = 0")
|
|
918
|
+
L.append(" for urn, keys in KEYS.items():")
|
|
919
|
+
L.append(" rows = [r for i, r in enumerate(TABLES[urn]) if SUP[urn][RK[urn][i]] & SP]")
|
|
920
|
+
L.append(" for key in keys:")
|
|
921
|
+
L.append(" groups = {}")
|
|
922
|
+
L.append(" for row in rows:")
|
|
923
|
+
L.append(" p = RT.key([row[i] for i in key])")
|
|
924
|
+
L.append(" groups[p] = groups.get(p, 0) + 1")
|
|
925
|
+
L.append(" n += sum(1 for c in groups.values() if c > 1)")
|
|
926
|
+
L.append(" return n")
|
|
927
|
+
L.append("def key_conflict_predicates():")
|
|
928
|
+
L.append(" out = set()")
|
|
929
|
+
L.append(" for urn, keys in KEYS.items():")
|
|
930
|
+
L.append(" rows = [r for i, r in enumerate(TABLES[urn]) if SUP[urn][RK[urn][i]] & SP]")
|
|
931
|
+
L.append(" for key in keys:")
|
|
932
|
+
L.append(" groups = {}")
|
|
933
|
+
L.append(" for row in rows:")
|
|
934
|
+
L.append(" p = RT.key([row[i] for i in key]); groups[p] = groups.get(p, 0) + 1")
|
|
935
|
+
L.append(" if any(c > 1 for c in groups.values()): out.add(urn)")
|
|
936
|
+
L.append(" return out")
|
|
937
|
+
L.append("")
|
|
938
|
+
L.append("# Собственные факты пакета; `reset` возвращает модель к ним (чистое `evaluate` API).")
|
|
939
|
+
L.append("def package_facts():")
|
|
940
|
+
for f in plan["facts"]:
|
|
941
|
+
bit = "SP" if f.get("polarity", "positive") == "positive" else "SN"
|
|
942
|
+
L.append(f' add({json.dumps(f["pred"])}, [' + ", ".join(term(a) for a in f["args"]) + f'], {bit})')
|
|
943
|
+
L.append(" pass")
|
|
944
|
+
L.append("")
|
|
945
|
+
L.append("def reset():")
|
|
946
|
+
L.append(" for urn in TABLES:")
|
|
947
|
+
L.append(" TABLES[urn].clear(); RK[urn].clear(); SUP[urn].clear()")
|
|
948
|
+
L.append(" CANDS.clear(); APPLIED.clear()")
|
|
949
|
+
if _is_09(plan):
|
|
950
|
+
L.append(" RULE_ISSUES.clear(); _ISSUE_SEEN.clear()")
|
|
951
|
+
if plan["planVersion"] in (SUPPORTED_PLAN_12, SUPPORTED_PLAN_13, SUPPORTED_PLAN_14, SUPPORTED_PLAN_15, SUPPORTED_PLAN_16, SUPPORTED_PLAN_17, SUPPORTED_PLAN_18, SUPPORTED_PLAN_19):
|
|
952
|
+
L.append(" _KIND_SEEN.clear()")
|
|
953
|
+
if "norms" in plan:
|
|
954
|
+
L.append(" POSITIONS.clear(); BLOCKED.clear(); BLOCKED_IMMUNITY.clear(); NORM_ISSUES.clear(); _NORM_SEEN.clear(); _NORM_SEQ.clear()")
|
|
955
|
+
L.append(" package_facts()")
|
|
956
|
+
L.append("")
|
|
957
|
+
L.append("package_facts()")
|
|
958
|
+
return _factory("\n".join(L) + "\n", plan)
|
|
959
|
+
|
|
960
|
+
# --- поверхность API (DECISION-0234) ----------------------------------------
|
|
961
|
+
# Вопрос — pub-отношение с производящим правилом (план решает, какие);
|
|
962
|
+
# вход — эмпирический предикат или предикат без производителя, достижимый
|
|
963
|
+
# из вопросов. Обёртка семантики не несёт: она строит факты, зовёт модель
|
|
964
|
+
# и читает таблицу вопроса — то же, что делает раннер.
|
|
965
|
+
|
|
966
|
+
def _api(plan: dict) -> dict:
|
|
967
|
+
return plan.get("api") or {"questions": [], "inputs": []}
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def _api_words(name: str) -> list:
|
|
971
|
+
"""Слова нейтрального имени API: части между `_` и горбы camelCase с учётом
|
|
972
|
+
регистра букв Unicode — основа имени на языке цели (DECISION-0423 §2.7)."""
|
|
973
|
+
words = []
|
|
974
|
+
for part in name.split("_"):
|
|
975
|
+
if not part:
|
|
976
|
+
continue
|
|
977
|
+
cur = part[0]
|
|
978
|
+
for prev, ch, nxt in zip(part, part[1:], part[2:] + " "):
|
|
979
|
+
if ch.isupper() and (prev.islower() or prev.isdigit() or (prev.isupper() and nxt.islower())):
|
|
980
|
+
words.append(cur)
|
|
981
|
+
cur = ch
|
|
982
|
+
else:
|
|
983
|
+
cur += ch
|
|
984
|
+
words.append(cur)
|
|
985
|
+
return words
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def api_names(entries: list) -> dict:
|
|
989
|
+
"""Нейтральное имя плана → имя функции Python в snake_case. Знаки, которых
|
|
990
|
+
нет в идентификаторах Python, заменяются `_`; совпадения после
|
|
991
|
+
преобразования разводятся суффиксом `_2`, `_3` в порядке плана
|
|
992
|
+
(отсортирован по StableId), поэтому имена детерминированы."""
|
|
993
|
+
out, used = {}, set()
|
|
994
|
+
for e in entries:
|
|
995
|
+
base = "_".join(w.lower() for w in _api_words(e["name"]))
|
|
996
|
+
base = "".join(c if ("x" + c).isidentifier() else "_" for c in base) or "x"
|
|
997
|
+
name, n = base, 1
|
|
998
|
+
while name in used:
|
|
999
|
+
n += 1
|
|
1000
|
+
name = f"{base}_{n}"
|
|
1001
|
+
used.add(name)
|
|
1002
|
+
out[e["name"]] = name
|
|
1003
|
+
return out
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def api_doc(entry: dict) -> list:
|
|
1007
|
+
"""Строки комментария над функцией API: метки отношения, затем по
|
|
1008
|
+
каждому производящему правилу — его метка и якоря `локатор · редакция`.
|
|
1009
|
+
Текст — из CLIR как есть; комментарий не переводится и не сокращается."""
|
|
1010
|
+
lines = []
|
|
1011
|
+
for l in entry.get("labels", [])[:2]:
|
|
1012
|
+
lines.append(f'{l["text"]} [{l["language"]}, {l["status"]}]')
|
|
1013
|
+
for src in entry.get("sources", []):
|
|
1014
|
+
head = src["rule"]
|
|
1015
|
+
if src["labels"]:
|
|
1016
|
+
head += ": " + src["labels"][0]["text"]
|
|
1017
|
+
lines.append(head)
|
|
1018
|
+
for a in src["anchors"]:
|
|
1019
|
+
where = a.get("locator") or a["id"].split("#")[-1]
|
|
1020
|
+
lines.append(" @source " + where + (f" · {a['edition']}" if a.get("edition") else ""))
|
|
1021
|
+
return lines
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def emit_api(plan: dict) -> str:
|
|
1025
|
+
"""API порождённого модуля (DECISION-0234, DECISION-0423): конструкторы
|
|
1026
|
+
фактов по входам, чистые функции вопросов и реестры по имени. Всё идёт
|
|
1027
|
+
через `api_base` в единый вход `evaluate`, общий с раннером; ответ —
|
|
1028
|
+
`Answer` одной формы у L0 и L1. Над каждой функцией — метки отношения и
|
|
1029
|
+
якоря `@source`."""
|
|
1030
|
+
check_plan(plan)
|
|
1031
|
+
api = _api(plan)
|
|
1032
|
+
negation = "negation" in (plan.get("capabilities") or ())
|
|
1033
|
+
L = ['"""Поверхность API порождённого модуля (DECISION-0234, DECISION-0423).',
|
|
1034
|
+
"",
|
|
1035
|
+
"Факты строятся функциями `fact_*` (и `negated_*` у модели L1) из литерал-термов",
|
|
1036
|
+
"§53 — конструкторов `integer`, `decimal`, `money`, `text`, `date`, `entity` и",
|
|
1037
|
+
"других — либо из значений языка, однозначных по объявленному типу параметра.",
|
|
1038
|
+
"Вопрос `ask_*` — чистая функция: каждый вызов строит своё состояние модели и",
|
|
1039
|
+
"возвращает `Answer` (статус §62, статус вычисления §175, issues, запросы судье).",
|
|
1040
|
+
'"""',
|
|
1041
|
+
"from api_base import (VALUE_FORMAT, Answer, Fact, InputError, Refused, Term, boolean, # noqa: F401",
|
|
1042
|
+
" date, decimal, entity, from_value, instant, integer, member, money,",
|
|
1043
|
+
" natural, quantity, rational, text, to_value)",
|
|
1044
|
+
"import api_base as _base", "", ""]
|
|
1045
|
+
inp, qs = api_names(api["inputs"]), api_names(api["questions"])
|
|
1046
|
+
L.append("# Факты: по функции на входной предикат, аргументы — по объявленным типам §189.")
|
|
1047
|
+
for i in api["inputs"]:
|
|
1048
|
+
args = ", ".join(f"a{j}" for j in range(len(i["params"])))
|
|
1049
|
+
sig = f'{json.dumps(i["pred"])}, {tuple(i["params"])!r}, {tuple(i["kinds"])!r}'
|
|
1050
|
+
fn = inp[i["name"]]
|
|
1051
|
+
L.append(f'def fact_{fn}({args}) -> Fact:')
|
|
1052
|
+
doc = api_doc(i)
|
|
1053
|
+
if doc:
|
|
1054
|
+
L.append(' """' + "\n ".join(doc) + '"""')
|
|
1055
|
+
L.append(f' return _base.fact({sig}, ({args}{"," if len(i["params"]) == 1 else ""}), "fact_{fn}")')
|
|
1056
|
+
L.append("")
|
|
1057
|
+
if negation:
|
|
1058
|
+
L.append(f'def negated_{fn}({args}) -> Fact:')
|
|
1059
|
+
L.append(' """Отрицательный факт §113 того же входа."""')
|
|
1060
|
+
L.append(f' return _base.fact({sig}, ({args}{"," if len(i["params"]) == 1 else ""}), "negated_{fn}", negative=True)')
|
|
1061
|
+
L.append("")
|
|
1062
|
+
L.append("")
|
|
1063
|
+
L.append("# Вопросы: статус §62 при данных фактах на дату `on` (правовое время §31).")
|
|
1064
|
+
for q in api["questions"]:
|
|
1065
|
+
params = "".join(f", a{j}" for j in range(len(q["params"])))
|
|
1066
|
+
want = ", ".join(f"a{j}" for j in range(len(q["params"])))
|
|
1067
|
+
tail = "," if len(q["params"]) == 1 else ""
|
|
1068
|
+
fn = qs[q["name"]]
|
|
1069
|
+
L.append(f'def ask_{fn}(facts, on{params}, *, query_id=None) -> Answer:')
|
|
1070
|
+
doc = api_doc(q)
|
|
1071
|
+
if doc:
|
|
1072
|
+
L.append(' """' + "\n ".join(doc) + '"""')
|
|
1073
|
+
L.append(f' return _base.ask({json.dumps(q["pred"])}, {tuple(q["params"])!r}, {tuple(q["kinds"])!r}, '
|
|
1074
|
+
f'facts, on, ({want}{tail}), {json.dumps(q["name"])}, query_id)')
|
|
1075
|
+
L.append("")
|
|
1076
|
+
L.append(f'def rows_{fn}(facts, on, *, query_id=None) -> Answer:')
|
|
1077
|
+
L.append(f' """Установленные строки `{q["name"]}` литерал-термами §53."""')
|
|
1078
|
+
L.append(f' return _base.rows({json.dumps(q["pred"])}, facts, on, {json.dumps(q["name"])}, query_id)')
|
|
1079
|
+
L.append("")
|
|
1080
|
+
L.append("")
|
|
1081
|
+
L.append("# Реестры по имени — для обвязки и проверки: вопрос и вход по строке.")
|
|
1082
|
+
L.append("QUESTIONS = {" + ", ".join(f'{json.dumps(q["name"], ensure_ascii=False)}: ask_{qs[q["name"]]}'
|
|
1083
|
+
for q in api["questions"]) + "}")
|
|
1084
|
+
L.append("INPUTS = {" + ", ".join(f'{json.dumps(i["name"], ensure_ascii=False)}: fact_{inp[i["name"]]}'
|
|
1085
|
+
for i in api["inputs"]) + "}")
|
|
1086
|
+
L.append("NEGATED = {" + (", ".join(f'{json.dumps(i["name"], ensure_ascii=False)}: negated_{inp[i["name"]]}'
|
|
1087
|
+
for i in api["inputs"]) if negation else "") + "}")
|
|
1088
|
+
return "\n".join(L) + "\n"
|
|
1089
|
+
|
|
1090
|
+
# --- дескриптор цели: раскладка, сборка, запуск ---------------------------
|
|
1091
|
+
import shutil as _shutil
|
|
1092
|
+
import sys as _sys
|
|
1093
|
+
from pathlib import Path as _Path
|
|
1094
|
+
|
|
1095
|
+
HERE = _Path(__file__).resolve().parent
|
|
1096
|
+
EXT = "py"
|
|
1097
|
+
MODEL = "model.py"
|
|
1098
|
+
# Минимальная версия тулчейна устанавливаемого пакета (DECISION-0423 §2.7) —
|
|
1099
|
+
# версия проверок CI (`.github/workflows/candidate.yml`), а не машины генератора.
|
|
1100
|
+
TOOLCHAIN_MIN = "3.13"
|
|
1101
|
+
API = "api.py"
|
|
1102
|
+
# Виды вопросов, которые исполняет раннер цели; ворота исполняют вектор
|
|
1103
|
+
# только когда его вид поддержан ВСЕМИ активными целями.
|
|
1104
|
+
QUERY_KINDS = {"truth", "term", "collect"}
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def available():
|
|
1108
|
+
return True, "интерпретатор запущенного процесса"
|
|
1109
|
+
|
|
1110
|
+
MAINS = {"runner": "runner.py", "cert": "cert.py", "apicheck": "apicheck.py"}
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
def layout(proj, main: str) -> list:
|
|
1114
|
+
"""Файлы цели в каталоге проекта; модель кладёт вызывающий."""
|
|
1115
|
+
proj = _Path(proj)
|
|
1116
|
+
proj.mkdir(parents=True, exist_ok=True)
|
|
1117
|
+
out = []
|
|
1118
|
+
names = ("runtime.py", MAINS[main]) if main == "cert" else ("runtime.py", "evaluate.py", "api_base.py", MAINS[main])
|
|
1119
|
+
for name in names:
|
|
1120
|
+
_shutil.copyfile(HERE / name, proj / name)
|
|
1121
|
+
out.append(proj / name)
|
|
1122
|
+
return out
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def build(proj):
|
|
1126
|
+
return None
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def run(proj, main: str):
|
|
1130
|
+
"""argv-префикс и cwd; аргументы (дело и вопрос либо фикстура) добавляет вызывающий."""
|
|
1131
|
+
return [_sys.executable, str(_Path(proj) / MAINS[main])], str(proj)
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
if __name__ == "__main__":
|
|
1135
|
+
import sys
|
|
1136
|
+
from pathlib import Path
|
|
1137
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
1138
|
+
import plan as P
|
|
1139
|
+
ir = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
1140
|
+
Path(sys.argv[2]).write_text(emit(P.build_plan(ir)), encoding="utf-8")
|