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
arxo_codegen/plan.py
ADDED
|
@@ -0,0 +1,4053 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLIR -> план исполнения, НЕ ЗАВИСЯЩИЙ от целевого языка.
|
|
3
|
+
|
|
4
|
+
Здесь живёт вся семантика среза: какие таблицы завести, что перебирать, где
|
|
5
|
+
соединение, где гард, какой терм посчитать, что отказать. Целевой язык об
|
|
6
|
+
этом не знает: он получает готовый план и печатает синтаксис.
|
|
7
|
+
|
|
8
|
+
Отказ — обязателен и громок. Незнакомое объявление, поле узла или форма
|
|
9
|
+
терма дают не «меньше ответа», а ДРУГОЙ ответ, поэтому пропускать нельзя.
|
|
10
|
+
"""
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import unicodedata
|
|
15
|
+
from fractions import Fraction
|
|
16
|
+
if __package__:
|
|
17
|
+
from . import temporal
|
|
18
|
+
else:
|
|
19
|
+
import temporal
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
# Версия формата плана. Состав полей описан в README; его изменение меняет
|
|
23
|
+
# версию, и эмиттер, не знающий версии, обязан отказать, а не печатать.
|
|
24
|
+
PLAN_VERSION = "law-backend-plan/0.7"
|
|
25
|
+
# План с возможностями поражаемого слоя (DECISION-0237): `capabilities`,
|
|
26
|
+
# опоры обеих полярностей, страты и расписание фаз §111. План без
|
|
27
|
+
# возможностей остаётся байтово прежним и носит PLAN_VERSION.
|
|
28
|
+
PLAN_VERSION_L1 = "law-backend-plan/0.8"
|
|
29
|
+
# План с агрегатами §59 в правилах и замыканиями §70 (DECISION-0239): подпланы
|
|
30
|
+
# comprehension, порядок шагов тела, фаза closure с уровнями E-0113. План без
|
|
31
|
+
# этих возможностей остаётся 0.8 либо 0.7 байтово.
|
|
32
|
+
PLAN_VERSION_09 = "law-backend-plan/0.9"
|
|
33
|
+
AGG_OPS = ("count", "sum", "min", "max", "only")
|
|
34
|
+
# План с нормативными позициями L2 (DECISION-0242): норм-правила, позиции,
|
|
35
|
+
# эффекты полномочий §127. План без возможности `norm` остаётся прежним.
|
|
36
|
+
PLAN_VERSION_10 = "law-backend-plan/0.10"
|
|
37
|
+
# План с календарными функциями §84–§86 (DECISION-0245): возможности функций,
|
|
38
|
+
# политики `deadline_policy`, снимки `calendar_snapshot`, версия семантики.
|
|
39
|
+
PLAN_VERSION_11 = "law-backend-plan/0.11"
|
|
40
|
+
# План с Rational §35/§58 (DECISION-0252): возможность `value:rational`,
|
|
41
|
+
# статические виды операндов арифметики, фоллибельные гарды `err` и сверка
|
|
42
|
+
# вида головы `kindChecks`. План, где Rational не возникает, остаётся прежним.
|
|
43
|
+
PLAN_VERSION_12 = "law-backend-plan/0.12"
|
|
44
|
+
# План с Quantity §49 v1 и счётчиком-связывателем §86.1 (DECISION-0256):
|
|
45
|
+
# возможность `value:quantity`, вид операнда `q`, терм `qtyvar`.
|
|
46
|
+
PLAN_VERSION_13 = "law-backend-plan/0.13"
|
|
47
|
+
# План с политикой округления §50 (DECISION-0259): возможности `value:round` и
|
|
48
|
+
# `value:div_round`, статические виды аргументов вызова `ak`.
|
|
49
|
+
PLAN_VERSION_14 = "law-backend-plan/0.14"
|
|
50
|
+
# План с Boolean и Natural как значениями (DECISION-0266), границами месяца
|
|
51
|
+
# §84.2 (DECISION-0267) и подстановочным `_` §98 (DECISION-0268):
|
|
52
|
+
# возможности `value:boolean`, `value:natural`, `calendar:month_bound`,
|
|
53
|
+
# `wildcard`. Модель они не меняют — план без них остаётся прежней версии
|
|
54
|
+
# байтово; версия нужна эмиттеру, чтобы незнакомый литерал, вызов или
|
|
55
|
+
# столбец атома не печатались молча.
|
|
56
|
+
PLAN_VERSION_15 = "law-backend-plan/0.15"
|
|
57
|
+
# План с датированными приоритетами §92.3 (DECISION-0269): возможность
|
|
58
|
+
# `priority:dated`, поля `datedPriorities` и `datedNormPriorities` — пары
|
|
59
|
+
# транзитивного замыкания с окнами `[start, end)` оси права; активное
|
|
60
|
+
# множество рёбер модуль готовит один раз на дело (`prepare`, подготовленный
|
|
61
|
+
# контекст). План без датированных рёбер остаётся прежней версии байтово.
|
|
62
|
+
PLAN_VERSION_16 = "law-backend-plan/0.16"
|
|
63
|
+
# План с вычисляемым аргументом атома тела и условным термом §56
|
|
64
|
+
# (DECISION-0270, E-0180): возможности `term:body` и `term:if`; модель 0.9 —
|
|
65
|
+
# ошибка значения аргумента есть issue правила, как у гардов; аргумент
|
|
66
|
+
# считается до перебора таблицы (`pre`), генератор с таким аргументом берётся
|
|
67
|
+
# только после связывания его переменных (`solver.solve`, pick).
|
|
68
|
+
PLAN_VERSION_17 = "law-backend-plan/0.17"
|
|
69
|
+
# План с функциями §46 «функция на функцию», параметрами A3 §A3.2, `let` и
|
|
70
|
+
# `match` §55.1 (DECISION-0273): возможности `function`, `param:a3`,
|
|
71
|
+
# `term:let`, `term:match`; модель 0.9. Функция печатается функцией цели по
|
|
72
|
+
# позиционным параметрам; параметр A3 — окна `[start, end)` оси права со
|
|
73
|
+
# значением, активное значение готовит `prepare` (подготовленный контекст),
|
|
74
|
+
# разрыв — MISSING_PARAMETER_VALUE правила; `match` над перечислением с
|
|
75
|
+
# вариантными образцами без полей и `_`, неполный — NON_EXHAUSTIVE_MATCH.
|
|
76
|
+
PLAN_VERSION_18 = "law-backend-plan/0.18"
|
|
77
|
+
# План с каналом суждения §47.3 (DECISION-0274): возможность `judgment` —
|
|
78
|
+
# программа объявляет отношение суждения (`external judgment relation`).
|
|
79
|
+
# Модель не меняется: запрос судье — обход по готовому состоянию модели
|
|
80
|
+
# (порт `whynot.judgment_blockers` и соседей в рантайме цели), для него план
|
|
81
|
+
# несёт `judgments` (орган и форма запроса), `backchain` с силой правил и
|
|
82
|
+
# гардами, а модель L1 — применения дефитеров и поражённых кандидатов.
|
|
83
|
+
PLAN_VERSION_19 = "law-backend-plan/0.19"
|
|
84
|
+
FN_CAPS = {"function", "param:a3", "term:let", "term:match"}
|
|
85
|
+
TERM_CAPS = {"term:body", "term:if"}
|
|
86
|
+
A3_OK = "urn:law:a3#Result/Ok"
|
|
87
|
+
A3_MISSING = "urn:law:a3#MissingParameterValue"
|
|
88
|
+
A3_RESULT = "urn:law:std#Result"
|
|
89
|
+
V15_CAPS = {"value:boolean", "value:natural", "calendar:month_bound", "wildcard"}
|
|
90
|
+
# §84.2 (E-0084): границы месяца — чистые функции над Date; ни политика §86,
|
|
91
|
+
# ни снимок §85 не читаются, ошибки значения над слотом Date нет.
|
|
92
|
+
MONTH_FNS = {"urn:law:std#month_start_of": "month_start", "urn:law:std#month_end_of": "month_end"}
|
|
93
|
+
# §84.1 (E-0028, E-0058, E-0185; W2 часть 2): физическая длительность между
|
|
94
|
+
# мгновениями и шаг мгновения на длительность — чистые функции над Instant.
|
|
95
|
+
# Не Instant у оракула всегда TYPE_ERROR — отказ плана; смешение мгновений со
|
|
96
|
+
# смещением и без, не-ISO запись — TYPE_ERROR по значению, названный отказ
|
|
97
|
+
# рантайма. Результат `add_duration` — Instant в UTC (`…Z`), дробные секунды
|
|
98
|
+
# отбрасываются, как у оракула (`add_duration_value`).
|
|
99
|
+
DUR_FNS = {"urn:law:std#hours_between": "hours_between", "urn:law:std#minutes_between": "minutes_between",
|
|
100
|
+
"urn:law:std#add_duration": "add_duration"}
|
|
101
|
+
DURATION_UNITS = ("second", "minute", "hour", "day")
|
|
102
|
+
INST = "urn:law:std#Instant"
|
|
103
|
+
# Функции std календаря → (имя в плане, возможность). `days_between` чистая
|
|
104
|
+
# и модели не меняет; остальные берут политику §86 и снимок §85 из
|
|
105
|
+
# окружения и дают ошибку значения, поэтому требуют накопления выводов.
|
|
106
|
+
CAL_FNS = {"urn:law:std#days_between": ("days_between", "calendar:days_between"),
|
|
107
|
+
"urn:law:std#add_calendar_period": ("add_calendar_period", "calendar:period"),
|
|
108
|
+
"urn:law:std#add_business_days": ("add_business_days", "calendar:business"),
|
|
109
|
+
"urn:law:std#add_legal_term": ("add_legal_term", None)}
|
|
110
|
+
CAL_CAPS = {"calendar:days_between", "calendar:period", "calendar:business"}
|
|
111
|
+
# Единицы счётчика (`values.py`): E-0029/E-0109 разводят операции по единице.
|
|
112
|
+
DAY_UNITS = ("business_day", "calendar_day")
|
|
113
|
+
CALENDAR_PERIOD_UNITS = ("calendar_day", "calendar_month", "calendar_year", "calendar_week")
|
|
114
|
+
DEADLINE_UNITS = ("business_day", "calendar_day", "calendar_month", "calendar_year", "calendar_week")
|
|
115
|
+
STD = "urn:law:std#"
|
|
116
|
+
# §140: институциональные литералы, которые публикует жизненный цикл позиций,
|
|
117
|
+
# с арностью; первый аргумент — позиция.
|
|
118
|
+
STD_POSITION = {"created": 1, "instance_of": 2, "bearer_of": 2, "beneficiary_of": 2,
|
|
119
|
+
"pending": 1, "active": 1, "satisfied": 1, "violated": 1, "expired": 1,
|
|
120
|
+
"undetermined": 1, "invalidly_exercised": 1}
|
|
121
|
+
POSITION_TYPE = "urn:law:backend#Position"
|
|
122
|
+
MODALITIES = ("duty", "power", "immunity", "liberty")
|
|
123
|
+
# Поля payload шаблона: читаемые оракулом и те, что оракул не читает вовсе
|
|
124
|
+
# (политики §123, `completenessPolicy`, `over`, `subject`, `before`) — у
|
|
125
|
+
# последних ответ от их значения не зависит, поэтому они принимаются.
|
|
126
|
+
PAYLOAD_FIELDS = {
|
|
127
|
+
"duty": {"kind", "bearer", "holder", "beneficiary", "goal", "window", "allocationPolicy",
|
|
128
|
+
"dischargePolicy", "violationPolicy", "over", "subject"},
|
|
129
|
+
"power": {"kind", "holder", "bearer", "beneficiary", "window", "exercise", "effect", "validWhen",
|
|
130
|
+
"over", "subject"},
|
|
131
|
+
"immunity": {"kind", "holder", "bearer", "beneficiary", "window", "protectedEffect", "against",
|
|
132
|
+
"over", "subject"},
|
|
133
|
+
"liberty": {"kind", "holder", "bearer", "beneficiary", "window", "action", "against", "over", "subject"},
|
|
134
|
+
}
|
|
135
|
+
GOAL_FIELDS = {"kind", "condition", "window", "action", "completenessPolicy", "termination"}
|
|
136
|
+
TEMPLATE_FIELDS = {"id", "kind", "contentHash", "package", "name", "label", "labels", "provenance",
|
|
137
|
+
"anchors", "sourceAnchors", "origin", "modality", "payload", "variables", "parameters"}
|
|
138
|
+
NORM_REFUSAL = "обратная связь L2 вне среза генератора (DECISION-0242 п. 1)"
|
|
139
|
+
# E-0116: пары агрегат/сравнение монотонного гарда и зеркало сравнения §206.
|
|
140
|
+
MONOTONE_GUARD_OPS = {"count": ("ge", "gt"), "sum": ("ge", "gt"), "max": ("ge", "gt"), "min": ("le", "lt")}
|
|
141
|
+
MIRROR_OP = {"ge": "le", "gt": "lt", "le": "ge", "lt": "gt"}
|
|
142
|
+
CLOSURE_FIELDS = {"id", "kind", "contentHash", "package", "name", "label", "labels", "provenance",
|
|
143
|
+
"anchors", "sourceAnchors", "origin", "predicate", "domain", "snapshot",
|
|
144
|
+
"completeAsOf", "deriveExplicitNegative", "effective"}
|
|
145
|
+
|
|
146
|
+
# Статусные тесты §65 в срезе L1 и сырые статусы §62 положительного атома,
|
|
147
|
+
# при которых конъюнкт выполнен, по полярности литерала (§63 зеркалит
|
|
148
|
+
# отрицательный). `supported` вычислительно тождествен `monotone` (E-0018),
|
|
149
|
+
# различие — слой спецификации, поэтому возможности у них раздельные.
|
|
150
|
+
STATUS_TESTS = ("established", "monotone", "supported", "not_known", "unknown", "refuted")
|
|
151
|
+
GENERATING = ("established", "monotone", "supported")
|
|
152
|
+
ACCEPT = {
|
|
153
|
+
("established", "positive"): ["TRUE_ONLY"], ("established", "negative"): ["FALSE_ONLY"],
|
|
154
|
+
("monotone", "positive"): ["TRUE_ONLY", "BOTH"], ("monotone", "negative"): ["FALSE_ONLY", "BOTH"],
|
|
155
|
+
("supported", "positive"): ["TRUE_ONLY", "BOTH"], ("supported", "negative"): ["FALSE_ONLY", "BOTH"],
|
|
156
|
+
# §67/§113: not_known истинен без positive-опоры — при NEITHER и FALSE_ONLY.
|
|
157
|
+
("not_known", "positive"): ["NEITHER", "FALSE_ONLY"], ("not_known", "negative"): ["NEITHER", "TRUE_ONLY"],
|
|
158
|
+
("unknown", "positive"): ["NEITHER"], ("unknown", "negative"): ["NEITHER"],
|
|
159
|
+
("refuted", "positive"): ["FALSE_ONLY"], ("refuted", "negative"): ["TRUE_ONLY"],
|
|
160
|
+
}
|
|
161
|
+
PRIORITY_FIELDS = {"id", "kind", "contentHash", "package", "name", "label", "labels", "provenance",
|
|
162
|
+
"anchors", "sourceAnchors", "origin", "condition", "higher", "lower",
|
|
163
|
+
"priorityKind", "reason"}
|
|
164
|
+
TRUE_FORMULA = {"kind": "boolean", "value": True}
|
|
165
|
+
UNSTRATIFIED = "программа не стратифицируема (§110, NON_EXECUTABLE_UNSTRATIFIED)"
|
|
166
|
+
LATE = "поздний производитель (§111, LDC-E4103)"
|
|
167
|
+
|
|
168
|
+
STD_FN = {"urn:law:std#round": "round", "urn:law:std#min": "min", "urn:law:std#max": "max",
|
|
169
|
+
"urn:law:std#abs": "abs", "urn:law:std#pow": "pow", "urn:law:std#div_round": "div_round"}
|
|
170
|
+
TEXT_URN = "urn:law:std#Text"
|
|
171
|
+
# §50 (DECISION-0259): режимы и предел точности `div_round` — порт `values`.
|
|
172
|
+
ROUNDING_MODES = ("CEILING", "DOWN", "FLOOR", "HALF_DOWN", "HALF_EVEN", "HALF_UP", "UP")
|
|
173
|
+
DIV_ROUND_MAX_PRECISION = 34
|
|
174
|
+
BINOP = {"mul": "mul", "add": "add", "sub": "sub", "div": "div", "mod": "mod"}
|
|
175
|
+
INT, DEC, MON = "urn:law:std#Integer", "urn:law:std#Decimal", "urn:law:std#Money"
|
|
176
|
+
# DECISION-0266: Natural — неотрицательное целое; у оракула значение вида
|
|
177
|
+
# `integer` (`values._TYPE_KINDS`), а тип-строка §189 своя: Natural не
|
|
178
|
+
# подходит слоту Integer и наоборот (`facts._conforms`).
|
|
179
|
+
NATURAL = "urn:law:std#Natural"
|
|
180
|
+
BOOL = "urn:law:std#Boolean"
|
|
181
|
+
# DECISION-0252: Rational §35 и динамический вид значение-зависимого частного
|
|
182
|
+
# §58 (Decimal/Decimal, Money/Money) — Decimal либо Rational по значению.
|
|
183
|
+
RAT, DYN = "urn:law:std#Rational", "urn:law:backend#DecimalOrRational"
|
|
184
|
+
# DECISION-0256: Quantity §49 v1 — вид без тега; тег — свойство значения.
|
|
185
|
+
QTY = "urn:law:std#Quantity"
|
|
186
|
+
NUMERIC = {INT, DEC, MON, RAT, DYN, QTY, NATURAL}
|
|
187
|
+
# Статический вид операнда в терме `bin` плана 0.12: рантайм не различает
|
|
188
|
+
# Integer и Decimal по значению, и пара §58 решается видами.
|
|
189
|
+
KIND_CODE = {INT: "i", DEC: "d", MON: "m", RAT: "r", DYN: "n", QTY: "q", NATURAL: "i"}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _num(urn):
|
|
193
|
+
"""Вид арифметики §58 по типу слота: Natural считает как Integer
|
|
194
|
+
(DECISION-0266), тип-строка при этом остаётся своей для §189."""
|
|
195
|
+
return INT if urn == NATURAL else urn
|
|
196
|
+
# §59 (errata E-0195): имена типов, у которых ВИД есть, — порт
|
|
197
|
+
# `values._TYPE_KINDS` плюс `_OPAQUE_KINDS`. С видом элемента, объявленным
|
|
198
|
+
# проекцией comprehension §52.1, сверяется вид политики пустой коллекции; имя
|
|
199
|
+
# вне набора (сущность, перечисление, чужой символ) вида не предъявляет, и
|
|
200
|
+
# сверка молчит: Unknown есть «утверждать нечего», а не «совместимо со всем».
|
|
201
|
+
EMPTY_KIND_NAMES = frozenset({INT, NATURAL, DEC, MON, TEXT_URN, "urn:law:std#Date", BOOL, RAT, QTY,
|
|
202
|
+
"urn:law:std#Instant", "urn:law:std#Magnitude", "urn:law:std#Bounds"})
|
|
203
|
+
CMP = {"lt", "le", "gt", "ge", "eq", "ne"}
|
|
204
|
+
|
|
205
|
+
TYPES = {
|
|
206
|
+
"urn:law:std#Money": "money",
|
|
207
|
+
"urn:law:std#Text": "text",
|
|
208
|
+
"urn:law:std#Decimal": "dec",
|
|
209
|
+
"urn:law:std#Integer": "dec",
|
|
210
|
+
"urn:law:std#Boolean": "bool",
|
|
211
|
+
# Natural (DECISION-0266): в ячейке — точное целое, как Integer.
|
|
212
|
+
"urn:law:std#Natural": "dec",
|
|
213
|
+
# Date §2.15: хранится и сравнивается по календарному дню; канон — ISO
|
|
214
|
+
# `YYYY-MM-DD`, поэтому порядок записи и есть порядок дня. Календарные
|
|
215
|
+
# функции §85 остаются вне среза.
|
|
216
|
+
"urn:law:std#Date": "date",
|
|
217
|
+
# Instant §2.15 (W2 часть 2): момент, а не запись (E-0048) — порядок и равенство
|
|
218
|
+
# по моменту, ключ атома по записи (E-0214); с Date не сравнивается (§57).
|
|
219
|
+
"urn:law:std#Instant": "inst",
|
|
220
|
+
# Rational §35 (DECISION-0252): несократимая дробь; ключ и сравнение — свои.
|
|
221
|
+
"urn:law:std#Rational": "rat",
|
|
222
|
+
# Quantity §49 v1 (DECISION-0256): число с тегом единицы.
|
|
223
|
+
"urn:law:std#Quantity": "qty",
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#: §261 `law.std.actors` и корни событий §79–§80: сущности стандартной
|
|
227
|
+
#: библиотеки — идентичности, как сущности пакета (`entity_ref` дела типа не
|
|
228
|
+
#: несёт, §189 их не сверяет). Событие в слоте — ссылка `entity_ref` на
|
|
229
|
+
#: закреплённый снимок (§79.1); конструктор-запись события в факте остаётся
|
|
230
|
+
#: названным отказом раннера («аргумент вне среза раннера: record») до W7,
|
|
231
|
+
#: где свёртка §161.1 читает его поля. Instant §2.15 — своё значение (W2, часть 2).
|
|
232
|
+
#: `LegalContext`, `LegalRole` (§261) и `Jurisdiction` (§257, E-0039) — того же
|
|
233
|
+
#: рода: у оракула нет ни вида значения (`values._TYPE_KINDS`), ни литерала,
|
|
234
|
+
#: дело подаёт их `entity_ref`, и сверка §189 их не трогает (W2, релиз 0.1.5).
|
|
235
|
+
STD_ENTITY_TYPES = frozenset(f"urn:law:std#{n}" for n in (
|
|
236
|
+
"Actor", "NaturalPerson", "LegalPerson", "Organization",
|
|
237
|
+
"PublicAuthority", "Official", "Court", "Collective", "Event", "Action",
|
|
238
|
+
"LegalContext", "LegalRole", "Jurisdiction"))
|
|
239
|
+
#: `Interval<T>` §37 в слоте отношения — непрозрачная идентичность: литерала
|
|
240
|
+
#: такого вида у оракула нет (`values.from_literal_term`), `entity_ref` в слоте
|
|
241
|
+
#: §189 не сверяет, а интервальный терм дела раннер отвергает поимённо
|
|
242
|
+
#: («аргумент вне среза раннера: interval»). Пакет, лишь объявляющий такое
|
|
243
|
+
#: отношение (окно льготы `variance_granted` FNMA), получает план; дело с
|
|
244
|
+
#: интервалом — названный отказ, а не иной ответ; литерал-интервал в теле
|
|
245
|
+
#: правила — отказ плана («терм в аргументе тела вне среза»).
|
|
246
|
+
STD_OPAQUE_SLOT_TYPES = frozenset({"urn:law:std#Interval"})
|
|
247
|
+
|
|
248
|
+
RULE_META = {"id", "kind", "contentHash", "package", "name", "label", "labels",
|
|
249
|
+
"provenance", "anchors", "sourceAnchors", "variables"}
|
|
250
|
+
RULE_FIELDS = RULE_META | {"strength", "head", "body", "effective"}
|
|
251
|
+
RULE_EMPTY_OK = {"scope", "interpretations", "governs"}
|
|
252
|
+
|
|
253
|
+
EXTERNAL_JUDGMENT_FIELDS = {"kind", "id", "package", "symbol", "effect", "authority", "capability",
|
|
254
|
+
"requestType", "contentHash", "labels", "anchors"}
|
|
255
|
+
FUNCTION_FIELDS = {"kind", "id", "package", "symbol", "parameters", "body", "effect", "anchors",
|
|
256
|
+
"provenance", "contentHash", "labels", "visibility"}
|
|
257
|
+
KNOWN_NODES = {("symbol_decl", "relation"), ("symbol_decl", "constant"),
|
|
258
|
+
("type_decl", None), ("rule", None),
|
|
259
|
+
("unit_decl", None), ("assertion", None),
|
|
260
|
+
# Объявление функции само по себе ничего не считает: вызов вне
|
|
261
|
+
# STD_FN отвергает `term`, ссылка через const_ref — `const_value`.
|
|
262
|
+
("symbol_decl", "function"), ("function_decl", None)}
|
|
263
|
+
ALWAYS = "urn:law:backend#always"
|
|
264
|
+
|
|
265
|
+
# Инертные конструкции (CODEGEN-COVERAGE-PLAN §1.1, W1): присутствуют в пакете,
|
|
266
|
+
# но на ответ поддержанных видов вопросов не влияют — план их ПРОПУСКАЕТ, а
|
|
267
|
+
# не отвергает. Каждая строка манифеста `inert-nodes.json` держится парой
|
|
268
|
+
# векторов «с конструкцией / без неё» с одним документом оракула; строку без
|
|
269
|
+
# такого свидетеля роняют ворота check_conformance (модуль `inert`). Здесь
|
|
270
|
+
# манифест только читается: план не решает, что инертно, — он исполняет
|
|
271
|
+
# закреплённое.
|
|
272
|
+
_INERT_MANIFEST = json.loads((Path(__file__).resolve().with_name("inert-nodes.json")).read_text(encoding="utf-8"))
|
|
273
|
+
INERT_NODE_KINDS = frozenset(c.removeprefix("node:") for c in _INERT_MANIFEST["constructs"] if c.startswith("node:"))
|
|
274
|
+
INERT_RULE_FIELDS = frozenset(c.removeprefix("rulefield:") for c in _INERT_MANIFEST["constructs"]
|
|
275
|
+
if c.startswith("rulefield:"))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class Refused(Exception):
|
|
279
|
+
"""Отказ среза с названной причиной."""
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _reject_withdrawn_language_version(ir: dict) -> None:
|
|
283
|
+
"""Stage R accepts one exact semantic release and one IR schema."""
|
|
284
|
+
version, schema = ir.get("semanticVersion"), ir.get("schemaVersion")
|
|
285
|
+
if version != "0.2.11" or schema != "law.core.ir/0.7":
|
|
286
|
+
raise Refused("SEMANTIC_RELEASE_UNAVAILABLE: требуется law.core/0.2.11 "
|
|
287
|
+
f"и law.core.ir/0.7; получено {version!r} / {schema!r}")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _reject_withdrawn_language_versions(ir: dict, world: list | None = None) -> None:
|
|
291
|
+
"""Проверить корневой IR и каждый IR мира до их семантического связывания."""
|
|
292
|
+
_reject_withdrawn_language_version(ir)
|
|
293
|
+
for dependency in world or ():
|
|
294
|
+
_reject_withdrawn_language_version(dependency)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# --- редакции §31/§92.3 (E-0095, DECISION-0236) --------------------------------
|
|
298
|
+
# Порт `lawref.applicability.edition_state`: состояние редакции на момент —
|
|
299
|
+
# последнее наступившее событие, чей extent покрывает момент. План не знает
|
|
300
|
+
# даты права, поэтому считает состояние во всех точках излома (начала и
|
|
301
|
+
# концы событий) и сворачивает применимость в интервалы; интервал — то же
|
|
302
|
+
# окно `window`, что у `effective`, и эмиттеры о редакциях не знают.
|
|
303
|
+
OPENING = ("in_force", "restored")
|
|
304
|
+
SUSPENDING = ("suspended",)
|
|
305
|
+
CLOSING = ("repealed", "expired", "invalidated")
|
|
306
|
+
NORMATIVE_KINDS = ("assertion", "constraint", "norm_template", "priority_rule", "rule")
|
|
307
|
+
T0 = "0000-01-01"
|
|
308
|
+
NO_START = "\uffff"
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _is_date(t) -> bool:
|
|
312
|
+
return isinstance(t, str) and len(t) == 10 and t[4] == "-" and t[7] == "-"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _event_start(extent):
|
|
316
|
+
if isinstance(extent, dict):
|
|
317
|
+
start = extent.get("start")
|
|
318
|
+
return None if start in (None, "-infinity") else str(start)
|
|
319
|
+
return None if extent is None else str(extent)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _ordered_events(edition: dict) -> list:
|
|
323
|
+
events = [e for e in edition.get("lifecycle") or [] if e.get("status")]
|
|
324
|
+
return [e for _, e in sorted(enumerate(events),
|
|
325
|
+
key=lambda pair: (_event_start(pair[1].get("extent")) or NO_START, pair[0]))]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _covers(extent, t: str) -> bool:
|
|
329
|
+
"""Покрывает ли extent момент t; границы обязаны быть днями §2.15 —
|
|
330
|
+
иную гранулярность вызывающий отсеял до сюда."""
|
|
331
|
+
if isinstance(extent, dict):
|
|
332
|
+
start, end = extent.get("start"), extent.get("end")
|
|
333
|
+
if start not in (None, "-infinity"):
|
|
334
|
+
if not (start <= t if extent.get("startInclusive", True) else start < t):
|
|
335
|
+
return False
|
|
336
|
+
if end not in (None, "infinity"):
|
|
337
|
+
if not (t <= end if extent.get("endInclusive", False) else t < end):
|
|
338
|
+
return False
|
|
339
|
+
return True
|
|
340
|
+
return extent <= t
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def edition_applicable(edition: dict, t: str) -> bool:
|
|
344
|
+
if edition.get("applicability") == "timeless":
|
|
345
|
+
return True
|
|
346
|
+
state, closed = "not_yet", False
|
|
347
|
+
for event in _ordered_events(edition):
|
|
348
|
+
status = event["status"]
|
|
349
|
+
if status not in OPENING + SUSPENDING + CLOSING:
|
|
350
|
+
continue
|
|
351
|
+
extent = event.get("extent")
|
|
352
|
+
start = _event_start(extent)
|
|
353
|
+
if start is not None and start > t:
|
|
354
|
+
continue
|
|
355
|
+
if status in CLOSING:
|
|
356
|
+
closed, state = True, "closed"
|
|
357
|
+
continue
|
|
358
|
+
if closed:
|
|
359
|
+
continue
|
|
360
|
+
covers = _covers(extent, t)
|
|
361
|
+
if status in SUSPENDING:
|
|
362
|
+
state = "suspended" if covers else "applicable"
|
|
363
|
+
else:
|
|
364
|
+
state = "applicable" if covers else "closed"
|
|
365
|
+
return state == "applicable"
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def edition_intervals(edition: dict):
|
|
369
|
+
"""Применимость редакции как список окон `[start, end)`; строка —
|
|
370
|
+
причина, по которой окно не вычислимо (граница не дневной гранулярности:
|
|
371
|
+
оракул такую редакцию читает как вне датировки с предупреждением).
|
|
372
|
+
Редакция без событий применимости §31 и без `timeless` — пустой список:
|
|
373
|
+
по §27.2 она `not_yet` на любую дату и узел датирует (§92 усл. 3,
|
|
374
|
+
errata E-0350, шаг 2)."""
|
|
375
|
+
if edition.get("applicability") == "timeless":
|
|
376
|
+
return [(None, None)]
|
|
377
|
+
events = [e for e in edition.get("lifecycle") or []
|
|
378
|
+
if e.get("status") in OPENING + SUSPENDING + CLOSING]
|
|
379
|
+
if not events:
|
|
380
|
+
return []
|
|
381
|
+
points = set()
|
|
382
|
+
for e in events:
|
|
383
|
+
extent = e.get("extent")
|
|
384
|
+
bounds = [extent.get("start"), extent.get("end")] if isinstance(extent, dict) else [extent]
|
|
385
|
+
for b in bounds:
|
|
386
|
+
if b in (None, "-infinity", "infinity"):
|
|
387
|
+
continue
|
|
388
|
+
if not _is_date(b):
|
|
389
|
+
return f"граница lifecycle не дневной гранулярности: {b}"
|
|
390
|
+
points.add(str(b))
|
|
391
|
+
marks = [T0] + sorted(points)
|
|
392
|
+
out, cur = [], None
|
|
393
|
+
for i, m in enumerate(marks):
|
|
394
|
+
on = edition_applicable(edition, m)
|
|
395
|
+
if on and cur is None:
|
|
396
|
+
cur = None if m == T0 else m
|
|
397
|
+
cur = (cur,)
|
|
398
|
+
elif not on and cur is not None:
|
|
399
|
+
out.append((cur[0], m))
|
|
400
|
+
cur = None
|
|
401
|
+
if cur is not None:
|
|
402
|
+
out.append((cur[0], None))
|
|
403
|
+
return out
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _intersect(a, b):
|
|
407
|
+
"""Пересечение двух окон `[start, end)`; None — пусто."""
|
|
408
|
+
start = a[0] if b[0] is None else (b[0] if a[0] is None else max(a[0], b[0]))
|
|
409
|
+
end = a[1] if b[1] is None else (b[1] if a[1] is None else min(a[1], b[1]))
|
|
410
|
+
if start is not None and end is not None and start >= end:
|
|
411
|
+
return None
|
|
412
|
+
return (start, end)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _contains(window, seg) -> bool:
|
|
416
|
+
"""Лежит ли отрезок `seg` целиком в окне `window` (`None` — бесконечность)."""
|
|
417
|
+
s, e = window
|
|
418
|
+
a, b = seg
|
|
419
|
+
return (s is None or (a is not None and s <= a)) and (e is None or (b is not None and b <= e))
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _union(intervals: list) -> list:
|
|
423
|
+
"""Объединение окон в непересекающиеся, по возрастанию."""
|
|
424
|
+
items = sorted(intervals, key=lambda w: (w[0] is not None, w[0] or ""))
|
|
425
|
+
out: list = []
|
|
426
|
+
for w in items:
|
|
427
|
+
if out and (out[-1][1] is None or (w[0] is not None and w[0] <= out[-1][1])):
|
|
428
|
+
last = out.pop()
|
|
429
|
+
end = None if last[1] is None or w[1] is None else max(last[1], w[1])
|
|
430
|
+
out.append((last[0], end))
|
|
431
|
+
else:
|
|
432
|
+
out.append(w)
|
|
433
|
+
return out
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _payload_vars(value, out: list | None = None) -> list:
|
|
437
|
+
"""Все переменные payload шаблона — порт `norms._payload_vars`: payload не
|
|
438
|
+
формула, и `_free` по видам формул его узлов не видит."""
|
|
439
|
+
out = [] if out is None else out
|
|
440
|
+
if isinstance(value, dict):
|
|
441
|
+
if value.get("kind") == "var":
|
|
442
|
+
if value["var"] not in out:
|
|
443
|
+
out.append(value["var"])
|
|
444
|
+
else:
|
|
445
|
+
for item in value.values():
|
|
446
|
+
_payload_vars(item, out)
|
|
447
|
+
elif isinstance(value, list):
|
|
448
|
+
for item in value:
|
|
449
|
+
_payload_vars(item, out)
|
|
450
|
+
return out
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _next_day(day: str) -> str:
|
|
454
|
+
"""Следующий календарный день ISO §2.15 — граница полуинтервала."""
|
|
455
|
+
import datetime
|
|
456
|
+
return (datetime.date.fromisoformat(day) + datetime.timedelta(days=1)).isoformat()
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class _Excluded(Exception):
|
|
460
|
+
"""Узел исключён проекцией §92.3 на всей оси: в плане его нет."""
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _slug(urn: str) -> str:
|
|
464
|
+
"""Внутреннее имя порождённого кода из StableId (таблица, функция, правило).
|
|
465
|
+
|
|
466
|
+
ASCII-локальная часть — с заменой символов вне `[A-Za-z0-9_]` на `_`, как
|
|
467
|
+
прежде. Замена букв вне ASCII (кириллица и другие письменности) стирала
|
|
468
|
+
различие имён: у отношений одного пакета получалось одно имя таблицы.
|
|
469
|
+
Такому слагу добавляются 8 знаков SHA-256 локальной части — имя остаётся
|
|
470
|
+
ASCII для всех целей и различимым (DECISION-0423 §7)."""
|
|
471
|
+
local = urn.split("#")[-1]
|
|
472
|
+
slug = re.sub(r"[^A-Za-z0-9_]", "_", local)
|
|
473
|
+
if local.isascii():
|
|
474
|
+
return slug
|
|
475
|
+
digest = hashlib.sha256(local.encode("utf-8")).hexdigest()[:8]
|
|
476
|
+
compact = re.sub(r"_+", "_", slug).strip("_")
|
|
477
|
+
return f"{compact}_{digest}" if compact else f"u_{digest}"
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _api_slug(urn: str) -> str:
|
|
481
|
+
"""Нейтральное имя вопроса или входа API из StableId (DECISION-0423 §2.7).
|
|
482
|
+
|
|
483
|
+
ASCII-имя совпадает с `_slug`. Имя с буквами вне ASCII их сохраняет (NFC):
|
|
484
|
+
знаки, не являющиеся буквой, цифрой или `_`, сворачиваются в `_`. Слова и
|
|
485
|
+
регистр по языку (snake_case, camelCase, PascalCase) выводит эмиттер цели."""
|
|
486
|
+
local = urn.split("#")[-1]
|
|
487
|
+
if local.isascii():
|
|
488
|
+
return re.sub(r"[^A-Za-z0-9_]", "_", local)
|
|
489
|
+
name = re.sub(r"[^\w]+|_+", "_", unicodedata.normalize("NFC", local)).strip("_")
|
|
490
|
+
return name or "u_" + hashlib.sha256(local.encode("utf-8")).hexdigest()[:8]
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
LABEL_ORDER = {"official": 0, "plain": 1, "unofficial": 2, "translation": 3}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _labels(node: dict) -> list:
|
|
497
|
+
"""Метки §218 узла для комментариев API: официальная первой, затем
|
|
498
|
+
прочие и переводы; текст и язык — как в CLIR, без правки."""
|
|
499
|
+
out = [{"language": l.get("language"), "status": l.get("status"), "text": l.get("text")}
|
|
500
|
+
for l in (node.get("labels") or []) if l.get("text")]
|
|
501
|
+
return sorted(out, key=lambda l: (LABEL_ORDER.get(l["status"], 9), l["language"] or ""))
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _sccs(vertices: list, edges: list) -> dict:
|
|
505
|
+
"""Компоненты сильной связности (Тарьян, итеративно): вершина → номер."""
|
|
506
|
+
succ: dict = {v: [] for v in vertices}
|
|
507
|
+
for a, b in edges:
|
|
508
|
+
succ.setdefault(a, []).append(b)
|
|
509
|
+
succ.setdefault(b, [])
|
|
510
|
+
index, low, comp, stack, on = {}, {}, {}, [], set()
|
|
511
|
+
counter = [0]
|
|
512
|
+
for root in sorted(succ):
|
|
513
|
+
if root in index:
|
|
514
|
+
continue
|
|
515
|
+
work = [(root, 0)]
|
|
516
|
+
while work:
|
|
517
|
+
v, i = work.pop()
|
|
518
|
+
if i == 0:
|
|
519
|
+
index[v] = low[v] = counter[0]
|
|
520
|
+
counter[0] += 1
|
|
521
|
+
stack.append(v)
|
|
522
|
+
on.add(v)
|
|
523
|
+
recurse = False
|
|
524
|
+
for j in range(i, len(succ[v])):
|
|
525
|
+
w = succ[v][j]
|
|
526
|
+
if w not in index:
|
|
527
|
+
work.append((v, j + 1))
|
|
528
|
+
work.append((w, 0))
|
|
529
|
+
recurse = True
|
|
530
|
+
break
|
|
531
|
+
if w in on:
|
|
532
|
+
low[v] = min(low[v], index[w])
|
|
533
|
+
if recurse:
|
|
534
|
+
continue
|
|
535
|
+
if low[v] == index[v]:
|
|
536
|
+
while True:
|
|
537
|
+
w = stack.pop()
|
|
538
|
+
on.discard(w)
|
|
539
|
+
comp[w] = index[v]
|
|
540
|
+
if w == v:
|
|
541
|
+
break
|
|
542
|
+
if work:
|
|
543
|
+
parent = work[-1][0]
|
|
544
|
+
low[parent] = min(low[parent], low[v])
|
|
545
|
+
return comp
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _covered(inner, outer: list) -> bool:
|
|
549
|
+
"""Покрывает ли объединение окон `outer` окно `inner` (`None` — вся ось)."""
|
|
550
|
+
need = [(None, None)] if inner is None else inner
|
|
551
|
+
# `_union` сливает пересекающиеся и смежные окна, поэтому окно внутри
|
|
552
|
+
# объединения лежит целиком в одном его окне.
|
|
553
|
+
have = [(None, None)] if outer is None else _union(outer)
|
|
554
|
+
return all(any((h[0] is None or (w[0] is not None and h[0] <= w[0]))
|
|
555
|
+
and (h[1] is None or (w[1] is not None and w[1] <= h[1])) for h in have)
|
|
556
|
+
for w in need)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _admit_current_slice(ir, world):
|
|
560
|
+
_reject_withdrawn_language_versions(ir, world)
|
|
561
|
+
for document in [ir, *(world or [])]:
|
|
562
|
+
temporal.validate_ir(document, Refused)
|
|
563
|
+
if any(n.get("kind") == "norm_template" or
|
|
564
|
+
(n.get("head") or {}).get("kind") == "norm_template_ref"
|
|
565
|
+
for n in document.get("nodes") or []):
|
|
566
|
+
raise Refused("NORM_RANKS_UNAVAILABLE: norm runtime requires current ranks (W6)")
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
class Builder:
|
|
570
|
+
def __init__(self, ir: dict, world: list | None = None, want: set | None = None,
|
|
571
|
+
resources: dict | None = None):
|
|
572
|
+
"""`world` — CLIR зависимостей (замыкание импортов). Мир собирается
|
|
573
|
+
как у `resolver.link_world`: корень, затем зависимости по имени;
|
|
574
|
+
узел с уже занятым StableId пропускается, если его contentHash тот же
|
|
575
|
+
(копии фактов `facts pkg::Group` у потребителя), иначе — отказ.
|
|
576
|
+
|
|
577
|
+
`want` — возможности, которых требует не программа, а её вызывающий:
|
|
578
|
+
`negation` для дела с отрицательными фактами (DECISION-0237)."""
|
|
579
|
+
_admit_current_slice(ir, world)
|
|
580
|
+
self.linked = world is not None
|
|
581
|
+
self.world_passport = []
|
|
582
|
+
self.dated_facts: list = []
|
|
583
|
+
if world:
|
|
584
|
+
ir = self._link(ir, world)
|
|
585
|
+
self.ir = ir
|
|
586
|
+
self.consts: dict = {}
|
|
587
|
+
self.entities: set = set()
|
|
588
|
+
self.aliases: dict = {}
|
|
589
|
+
self.types: dict = {}
|
|
590
|
+
self.want = set(want or ())
|
|
591
|
+
self.caps: set = set()
|
|
592
|
+
self.l1 = False
|
|
593
|
+
self._meta: dict = {}
|
|
594
|
+
self._prio: list = []
|
|
595
|
+
self.v09 = False
|
|
596
|
+
self.v10 = False
|
|
597
|
+
self.v11 = False
|
|
598
|
+
self.v12 = False
|
|
599
|
+
self.v13 = False
|
|
600
|
+
self.v14 = False
|
|
601
|
+
self.v15 = False
|
|
602
|
+
self.v16 = False
|
|
603
|
+
self.v17 = False
|
|
604
|
+
self.v18 = False
|
|
605
|
+
# DECISION-0274: отношения суждения §47.3 объявлены.
|
|
606
|
+
self.v19 = False
|
|
607
|
+
self.judgments: dict = {}
|
|
608
|
+
# DECISION-0273: функции §46, параметры A3, `let`, `match` встретились.
|
|
609
|
+
self._function_seen = False
|
|
610
|
+
self._a3_seen = False
|
|
611
|
+
self._let_seen = False
|
|
612
|
+
self._match_seen = False
|
|
613
|
+
self._fn_plan: dict = {}
|
|
614
|
+
self._fn_stack: list = []
|
|
615
|
+
self._a3params: dict = {}
|
|
616
|
+
self._let_n = 0
|
|
617
|
+
# DECISION-0270: вычисляемый аргумент тела и условный терм встретились.
|
|
618
|
+
self._body_term_seen = False
|
|
619
|
+
self._if_seen = False
|
|
620
|
+
# DECISION-0269: датированное ребро приоритета встретилось при разборе.
|
|
621
|
+
self._dated_prio_seen = False
|
|
622
|
+
# DECISION-0266–0268: Boolean, Natural, границы месяца и `_` встретились
|
|
623
|
+
# при компиляции — `build_plan` строит план заново с возможностями.
|
|
624
|
+
self._boolean_seen = False
|
|
625
|
+
self._natural_seen = False
|
|
626
|
+
self._month_seen = False
|
|
627
|
+
self._wildcard_seen = False
|
|
628
|
+
# DECISION-0259 п. 1: `round` с политикой для рантайма и `div_round`.
|
|
629
|
+
self._round_seen = False
|
|
630
|
+
self._div_round_seen = False
|
|
631
|
+
# DECISION-0256 п. 1: Quantity первым классом встретилась при компиляции.
|
|
632
|
+
self._quantity_seen = False
|
|
633
|
+
# W2 часть 2: Instant первым классом встретилась при компиляции.
|
|
634
|
+
self._instant_seen = False
|
|
635
|
+
# DECISION-0252 п. 1: Rational либо значение-зависимое частное встретились
|
|
636
|
+
# при компиляции — `build_plan` строит план заново с `value:rational`.
|
|
637
|
+
self._rational_seen = False
|
|
638
|
+
# DECISION-0252 п. 4: правила с ошибкой значения над незавершённым входом —
|
|
639
|
+
# ошибку не ловят, она становится названным отказом раннера.
|
|
640
|
+
self._value_refuse: set = set()
|
|
641
|
+
# Байты ресурсов по `resourceUri` (снимки §85, DECISION-0245); без
|
|
642
|
+
# записи — явный MISSING_CALENDAR; исходное дерево не читается.
|
|
643
|
+
self.resources = dict(resources or {})
|
|
644
|
+
# Explicit maps (including an empty map) never read repository resources.
|
|
645
|
+
# Standalone planning never discovers a source checkout.
|
|
646
|
+
self.repository_resources = False
|
|
647
|
+
self._norms: list = []
|
|
648
|
+
self._norm_meta: dict = {}
|
|
649
|
+
self._atoms: list = []
|
|
650
|
+
self._aggs: list = []
|
|
651
|
+
self._agg_cache: dict = {}
|
|
652
|
+
# E-0105: в плане есть агрегат с ошибкой значения — нужен `backchain`.
|
|
653
|
+
self._agg_fallible = False
|
|
654
|
+
self._agg_reads: list = []
|
|
655
|
+
self._agg_inner: list = []
|
|
656
|
+
self._closures: list = []
|
|
657
|
+
|
|
658
|
+
def _link(self, root: dict, world: list) -> dict:
|
|
659
|
+
nodes, owner = [], {}
|
|
660
|
+
parts = [((root.get("package") or {}).get("name", "<root>"), root)]
|
|
661
|
+
parts += sorted(((d.get("package") or {}).get("name", "?"), d) for d in world)
|
|
662
|
+
for name, ir in parts:
|
|
663
|
+
for n in ir.get("nodes", []):
|
|
664
|
+
nid = n.get("id", "")
|
|
665
|
+
prev = owner.get(nid)
|
|
666
|
+
if prev is not None:
|
|
667
|
+
if prev.get("contentHash") != n.get("contentHash"):
|
|
668
|
+
raise Refused(f"коллизия StableId между пакетами мира с разным содержимым: {nid}")
|
|
669
|
+
continue
|
|
670
|
+
owner[nid] = n
|
|
671
|
+
nodes.append(n)
|
|
672
|
+
if ir is not root:
|
|
673
|
+
self.world_passport.append({"name": name, "namespace": (ir.get("package") or {}).get("namespace"),
|
|
674
|
+
"version": (ir.get("package") or {}).get("version")})
|
|
675
|
+
return dict(root, nodes=nodes, imports=[])
|
|
676
|
+
|
|
677
|
+
def _foreign_type_aliases(self, ir: dict, rels: list) -> None:
|
|
678
|
+
"""Чужой тип в форме `имя.пакета#Тип` (так понижается
|
|
679
|
+
`algo.description::Algorithm`, lawc `lower/decls.rs`): вид слота
|
|
680
|
+
берётся у объявления `namespace#Тип`, где namespace — пакета мира с
|
|
681
|
+
ТОЧНО этим именем. Строка типа в сигнатурах, константах и
|
|
682
|
+
наследовании не меняется: сверка §189 у оракула (`facts._conforms`)
|
|
683
|
+
сравнивает строки, и нормализация приняла бы то, что он отвергает.
|
|
684
|
+
Имя без пакета в мире или без объявления остаётся вне среза."""
|
|
685
|
+
names = {(ir.get("package") or {}).get("name"): (ir.get("package") or {}).get("namespace")}
|
|
686
|
+
names.update({w["name"]: w["namespace"] for w in self.world_passport})
|
|
687
|
+
raw = {t for r in rels for t in self.types[r["id"]] if t}
|
|
688
|
+
raw |= {c["type"] for c in self.consts.values() if c["type"]}
|
|
689
|
+
raw |= {b for b in self.bases.values() if b}
|
|
690
|
+
for name in sorted(raw):
|
|
691
|
+
if name.startswith("urn:") or name.count("#") != 1:
|
|
692
|
+
continue
|
|
693
|
+
package, local = name.split("#")
|
|
694
|
+
ns = names.get(package)
|
|
695
|
+
if not ns:
|
|
696
|
+
continue
|
|
697
|
+
# Цель сама бывает алиасом §40 (`kz.corpus.osms#Person` →
|
|
698
|
+
# `kz.corpus.civilcode#Person`): вид берётся за цепочкой, строка
|
|
699
|
+
# ссылки при этом не меняется (W2, часть 1).
|
|
700
|
+
urn = self._t(f"{ns}#{local}")
|
|
701
|
+
if urn in self.entities:
|
|
702
|
+
self.entities.add(name)
|
|
703
|
+
elif urn in self.enums:
|
|
704
|
+
self.enums[name] = self.enums[urn]
|
|
705
|
+
|
|
706
|
+
def _t(self, name):
|
|
707
|
+
"""Имя типа за цепочкой алиасов §40 (E-0097/E-0098) — как
|
|
708
|
+
`facts._unalias` оракула: обе стороны сверки §189 берутся за цепочкой,
|
|
709
|
+
цикл обрывается на повторе. `None` проходит как есть."""
|
|
710
|
+
seen: set = set()
|
|
711
|
+
current = name
|
|
712
|
+
while current is not None and current not in seen:
|
|
713
|
+
seen.add(current)
|
|
714
|
+
nxt = self.aliases.get(current)
|
|
715
|
+
if nxt is None:
|
|
716
|
+
break
|
|
717
|
+
current = nxt
|
|
718
|
+
return current
|
|
719
|
+
|
|
720
|
+
def ts(self, name):
|
|
721
|
+
name = self._t(name)
|
|
722
|
+
if name == "urn:law:std#Number":
|
|
723
|
+
raise Refused("NUMBER_UNAVAILABLE: exact Number and slot narrowing (E-0316/E-0319/E-0334) are outside this slice")
|
|
724
|
+
if name == RAT:
|
|
725
|
+
self._rational_seen = True
|
|
726
|
+
if name == QTY:
|
|
727
|
+
self._quantity_seen = True
|
|
728
|
+
if name == INST:
|
|
729
|
+
self._instant_seen = True
|
|
730
|
+
if name == BOOL:
|
|
731
|
+
self._boolean_seen = True
|
|
732
|
+
if name == NATURAL:
|
|
733
|
+
self._natural_seen = True
|
|
734
|
+
if name in TYPES:
|
|
735
|
+
return TYPES[name]
|
|
736
|
+
if name in self.entities:
|
|
737
|
+
return "text"
|
|
738
|
+
if name in self.enums:
|
|
739
|
+
return "enum"
|
|
740
|
+
raise Refused(f"тип аргумента вне среза генератора: {name}")
|
|
741
|
+
|
|
742
|
+
def term(self, node: dict, binds: dict) -> dict:
|
|
743
|
+
k = node["kind"]
|
|
744
|
+
if k == "var":
|
|
745
|
+
if node["var"] not in binds:
|
|
746
|
+
raise Refused(f"несвязанная переменная в голове: {node['var']}")
|
|
747
|
+
bind = binds[node["var"]]
|
|
748
|
+
if bind[0] == "p":
|
|
749
|
+
# Параметр подплана агрегата: значение внешней переменной (§52.1).
|
|
750
|
+
return {"k": "param", "i": bind[1], "t": bind[2]}
|
|
751
|
+
if bind[0] == "f":
|
|
752
|
+
# DECISION-0273: параметр функции §46 — позиционный аргумент.
|
|
753
|
+
return {"k": "fvar", "i": bind[1], "t": bind[2]}
|
|
754
|
+
if bind[0] == "l":
|
|
755
|
+
# DECISION-0273: переменная `let` — локальное связывание.
|
|
756
|
+
return {"k": "lvar", "i": bind[1], "t": bind[2]}
|
|
757
|
+
i, j, t = bind
|
|
758
|
+
return {"k": "var", "row": i, "col": j, "t": t}
|
|
759
|
+
if k == "aggregate" and self.v09:
|
|
760
|
+
return self._aggregate(node, binds)[0]
|
|
761
|
+
if k == "let":
|
|
762
|
+
return self._let_term(node, binds)
|
|
763
|
+
if k == "match":
|
|
764
|
+
return self._match_term(node, binds)
|
|
765
|
+
if k == "value":
|
|
766
|
+
t = (node.get("type") or {}).get("name", "")
|
|
767
|
+
if t == "urn:law:std#Number":
|
|
768
|
+
raise Refused("NUMBER_UNAVAILABLE: exact Number and slot narrowing (E-0316/E-0319/E-0334) are outside this slice")
|
|
769
|
+
if t == "urn:law:std#Money":
|
|
770
|
+
return {"k": "lit", "t": "money", "v": str(node["value"]), "cur": node["currency"]}
|
|
771
|
+
if t in ("urn:law:std#Decimal", "urn:law:std#Integer"):
|
|
772
|
+
return {"k": "lit", "t": "dec", "v": str(node["value"])}
|
|
773
|
+
if t == "urn:law:std#Text":
|
|
774
|
+
return {"k": "lit", "t": "text", "v": node["value"]}
|
|
775
|
+
if t == "urn:law:std#Date":
|
|
776
|
+
if not temporal.date_valid(node.get("value")):
|
|
777
|
+
raise Refused("TEMPORAL_VALUE_INVALID: Date requires ASCII Gregorian YYYY-MM-DD")
|
|
778
|
+
return {"k": "lit", "t": "date", "v": str(node["value"])}
|
|
779
|
+
if t == INST:
|
|
780
|
+
if not temporal.instant_valid(node.get("value")):
|
|
781
|
+
raise Refused("TEMPORAL_VALUE_INVALID: Instant requires an explicit valid offset")
|
|
782
|
+
# Запись мгновения — идентичность (E-0214), сравнение — по моменту (E-0048).
|
|
783
|
+
self._instant_seen = True
|
|
784
|
+
return {"k": "lit", "t": "inst", "v": str(node["value"])}
|
|
785
|
+
if t == RAT:
|
|
786
|
+
# §35 (E-0005): запись литерала бывает сократимой (`5/100` в датасете
|
|
787
|
+
# A3), оракул её сокращает (`from_literal_term`); рантайм цели принимает
|
|
788
|
+
# только канон §168.1 — сокращаем здесь. Не `n/d` с целыми, ноль в
|
|
789
|
+
# знаменателе — у оракула TYPE_ERROR/DIVISION_BY_ZERO всегда: отказ.
|
|
790
|
+
#
|
|
791
|
+
# Errata E-0213 сокращение НЕ отменяет, и это замер, а не осторожность.
|
|
792
|
+
# Канон §208 теперь печатает несократимую дробь, и порождаемый CLIR
|
|
793
|
+
# корпуса после перевыпуска несократим весь (0 из 1 281 снимка), — но
|
|
794
|
+
# план читает CLIR КАК ЗАПИСАН, а не канонизирует его: сократимая
|
|
795
|
+
# запись остаётся ЗАКОННЫМ входом (§208, раздел лексики E-0203),
|
|
796
|
+
# схема `LiteralTerm.value` намеренно не сужена, и рукописный либо
|
|
797
|
+
# чужой CLIR вправе прийти с `5/100`. Правило здесь то же самое:
|
|
798
|
+
# `Fraction` даёт взаимно простую пару с положительным знаменателем —
|
|
799
|
+
# ровно канон `canonical_rational`, — и `5/1` остаётся дробью.
|
|
800
|
+
raw = str(node["value"])
|
|
801
|
+
try:
|
|
802
|
+
num, den = (int(x) for x in raw.split("/"))
|
|
803
|
+
except ValueError as exc:
|
|
804
|
+
raise Refused(f"литерал Rational не 'n/d' вне среза генератора (§35): {raw}") from exc
|
|
805
|
+
if den == 0:
|
|
806
|
+
raise Refused("литерал Rational со знаменателем 0 вне среза генератора (§35)")
|
|
807
|
+
frac = Fraction(num, den)
|
|
808
|
+
self._rational_seen = True
|
|
809
|
+
return {"k": "lit", "t": "rat", "v": f"{frac.numerator}/{frac.denominator}"}
|
|
810
|
+
if t == QTY and node.get("unit"):
|
|
811
|
+
self._quantity_seen = True
|
|
812
|
+
return {"k": "lit", "t": "qty", "v": str(node["value"]), "unit": node["unit"]}
|
|
813
|
+
if t == BOOL:
|
|
814
|
+
# DECISION-0266: литерал обязан быть JSON-булевым — иное у
|
|
815
|
+
# оракула всегда TYPE_ERROR (`values.from_literal_term`).
|
|
816
|
+
if not isinstance(node.get("value"), bool):
|
|
817
|
+
raise Refused("литерал Boolean не true/false вне среза генератора (§48)")
|
|
818
|
+
self._boolean_seen = True
|
|
819
|
+
return {"k": "lit", "t": "bool", "v": node["value"]}
|
|
820
|
+
if t == NATURAL:
|
|
821
|
+
# DECISION-0266: неотрицательное целое; иное у оракула всегда TYPE_ERROR.
|
|
822
|
+
v = node.get("value")
|
|
823
|
+
if isinstance(v, bool) or not isinstance(v, int) or v < 0:
|
|
824
|
+
raise Refused("литерал Natural не неотрицательное целое вне среза генератора (§48)")
|
|
825
|
+
self._natural_seen = True
|
|
826
|
+
return {"k": "lit", "t": "dec", "v": str(v)}
|
|
827
|
+
raise Refused(f"тип значения вне среза генератора: {t}")
|
|
828
|
+
if k == "entity_ref":
|
|
829
|
+
return {"k": "lit", "t": "text", "v": node["id"]}
|
|
830
|
+
if k == "const_ref":
|
|
831
|
+
if self.is_member(node["id"]):
|
|
832
|
+
return {"k": "lit", "t": "enum", "v": node["id"]}
|
|
833
|
+
if self._is_a3(node["id"]):
|
|
834
|
+
return self._a3_term(node["id"])
|
|
835
|
+
return self.term(self.const_value(node["id"]), binds)
|
|
836
|
+
if k == "binary":
|
|
837
|
+
op = BINOP.get(node["op"])
|
|
838
|
+
if op is None:
|
|
839
|
+
raise Refused(f"операция вне среза генератора: {node['op']}")
|
|
840
|
+
out = {"k": "bin", "op": op, "l": self.term(node["left"], binds),
|
|
841
|
+
"r": self.term(node["right"], binds)}
|
|
842
|
+
self.static_type(node, binds) # таблицы видов §58 — статически
|
|
843
|
+
# Current arithmetic always carries operand kinds (i64 boundary included).
|
|
844
|
+
# DECISION-0252 п. 3: статические виды операндов для `arith` рантайма
|
|
845
|
+
# (DECISION-0270: и у планов с вычисляемыми аргументами — код ошибки).
|
|
846
|
+
out["lk"] = KIND_CODE[self.static_type(node["left"], binds)]
|
|
847
|
+
out["rk"] = KIND_CODE[self.static_type(node["right"], binds)]
|
|
848
|
+
return out
|
|
849
|
+
if k == "call" and node.get("function") in CAL_FNS:
|
|
850
|
+
return self._calendar_call(node, binds)
|
|
851
|
+
if k == "call" and node.get("function") in MONTH_FNS:
|
|
852
|
+
return self._month_call(node, binds)
|
|
853
|
+
if k == "call" and node.get("function") in DUR_FNS:
|
|
854
|
+
return self._duration_call(node, binds)
|
|
855
|
+
if k == "if":
|
|
856
|
+
return self._if_term(node, binds)
|
|
857
|
+
if k == "call" and node.get("function") in self.fdecls:
|
|
858
|
+
return self._fcall(node, binds)
|
|
859
|
+
if k == "call":
|
|
860
|
+
fn = STD_FN.get(node["function"])
|
|
861
|
+
if fn is None:
|
|
862
|
+
raise Refused(f"функция вне среза генератора: {node['function']}")
|
|
863
|
+
out = {"k": "call", "fn": fn, "args": [self.term(a, binds) for a in node["args"]]}
|
|
864
|
+
self.static_type(node, binds)
|
|
865
|
+
if fn in ("round", "div_round"):
|
|
866
|
+
ak = self._policy(node, binds)
|
|
867
|
+
if self.v14:
|
|
868
|
+
# DECISION-0259 п. 2: вид аргумента — из плана, не из значения.
|
|
869
|
+
out["ak"] = ak
|
|
870
|
+
return out
|
|
871
|
+
raise Refused(f"узел терма вне среза генератора: {k}")
|
|
872
|
+
|
|
873
|
+
# --- DECISION-0273: функции §46, параметры A3, let, match -----------------
|
|
874
|
+
|
|
875
|
+
def _is_a3(self, cid: str) -> bool:
|
|
876
|
+
"""Функция объявлена параметром-датасетом A3 (`calendar.is_a3_parameter`)."""
|
|
877
|
+
decl = self.fdecls.get(cid)
|
|
878
|
+
return decl is not None and any((e.get("attributes") or {}).get("a3Kind") == "parameter-dataset"
|
|
879
|
+
for e in decl.get("provenance") or [])
|
|
880
|
+
|
|
881
|
+
def _a3_range(self, cond: dict) -> tuple:
|
|
882
|
+
"""Условие строки параметра (`a3._range_condition`): `true`, `v0 >= start`,
|
|
883
|
+
`v0 < end` либо их конъюнкция; иная форма — отказ."""
|
|
884
|
+
k = (cond or {}).get("kind")
|
|
885
|
+
if k == "boolean" and cond.get("value") is True:
|
|
886
|
+
return None, None
|
|
887
|
+
if k == "and":
|
|
888
|
+
items = cond.get("items") or []
|
|
889
|
+
if len(items) == 2:
|
|
890
|
+
s1, e1 = self._a3_range(items[0])
|
|
891
|
+
s2, e2 = self._a3_range(items[1])
|
|
892
|
+
if s1 is not None and e1 is None and s2 is None and e2 is not None:
|
|
893
|
+
return s1, e2
|
|
894
|
+
raise Refused("форма параметра A3 вне среза генератора (DECISION-0273)")
|
|
895
|
+
if k == "comparison" and (cond.get("left") or {}).get("kind") == "var" \
|
|
896
|
+
and (cond.get("right") or {}).get("kind") == "value" \
|
|
897
|
+
and ((cond["right"].get("type") or {}).get("name") == "urn:law:std#Date"):
|
|
898
|
+
if cond.get("op") == "ge":
|
|
899
|
+
return str(cond["right"]["value"]), None
|
|
900
|
+
if cond.get("op") == "lt":
|
|
901
|
+
return None, str(cond["right"]["value"])
|
|
902
|
+
raise Refused("форма параметра A3 вне среза генератора (DECISION-0273)")
|
|
903
|
+
|
|
904
|
+
def _a3_term(self, cid: str) -> dict:
|
|
905
|
+
"""Параметр A3 §A3.2 через `const_ref`: оракул вызывает функцию датасета на
|
|
906
|
+
`legalTime` (`values.eval_term`), строки — окна `[start, end)` на оси
|
|
907
|
+
права (`a3.lower_parameter`). План разбирает тело структурно: цепочка
|
|
908
|
+
`if` по окнам → вызов функции строки → `Result/Ok [литерал]`, хвост —
|
|
909
|
+
`MissingParameterValue`. Значение готовит `prepare` один раз на дело;
|
|
910
|
+
разрыв — MISSING_PARAMETER_VALUE правила (молчание права, статус
|
|
911
|
+
NEITHER/COMPUTED, обход E-0105 код пропускает). Ось Instant — отказ."""
|
|
912
|
+
if cid in self._a3params:
|
|
913
|
+
p = self._a3params[cid]
|
|
914
|
+
return {"k": "a3", "id": cid, "t": p["kind"]}
|
|
915
|
+
decl, sym = self.fdecls[cid], self.fsyms.get(cid)
|
|
916
|
+
if sym is None:
|
|
917
|
+
raise Refused(f"параметр A3 без объявления символа: {cid}")
|
|
918
|
+
params = decl.get("parameters") or []
|
|
919
|
+
if len(params) != 1 or self._t((params[0].get("type") or {}).get("name")) != "urn:law:std#Date":
|
|
920
|
+
raise Refused("параметр A3 по оси Instant вне среза генератора (DECISION-0273)")
|
|
921
|
+
rt = sym.get("returnType") or {}
|
|
922
|
+
if rt.get("name") != A3_RESULT or not rt.get("args"):
|
|
923
|
+
raise Refused("форма параметра A3 вне среза генератора (DECISION-0273)")
|
|
924
|
+
vt = self._t((rt["args"][0] or {}).get("name"))
|
|
925
|
+
kind = self.ts(vt)
|
|
926
|
+
windows, body = [], decl.get("body") or {}
|
|
927
|
+
while True:
|
|
928
|
+
if body.get("kind") == "variant_value" and body.get("constructor") == A3_MISSING:
|
|
929
|
+
break
|
|
930
|
+
if body.get("kind") != "if":
|
|
931
|
+
raise Refused("форма параметра A3 вне среза генератора (DECISION-0273)")
|
|
932
|
+
start, end = self._a3_range(body.get("condition"))
|
|
933
|
+
then = body.get("then") or {}
|
|
934
|
+
row = self.fdecls.get(then.get("function")) if then.get("kind") == "call" and not then.get("args") else None
|
|
935
|
+
rb = (row or {}).get("body") or {}
|
|
936
|
+
if rb.get("kind") != "variant_value" or rb.get("constructor") != A3_OK or len(rb.get("fields") or []) != 1:
|
|
937
|
+
raise Refused("форма параметра A3 вне среза генератора (DECISION-0273)")
|
|
938
|
+
lit = rb["fields"][0]
|
|
939
|
+
if self.static_type(lit, {}) != vt:
|
|
940
|
+
raise Refused("тип строки параметра A3 не совпадает с типом значения (DECISION-0273)")
|
|
941
|
+
windows.append([start, end, self.term(lit, {})])
|
|
942
|
+
body = body.get("else") or {}
|
|
943
|
+
if not windows:
|
|
944
|
+
raise Refused("параметр A3 без строк вне среза генератора (DECISION-0273)")
|
|
945
|
+
self._a3params[cid] = {"id": cid, "type": vt, "kind": kind, "windows": windows}
|
|
946
|
+
self._a3_seen = True
|
|
947
|
+
return {"k": "a3", "id": cid, "t": kind}
|
|
948
|
+
|
|
949
|
+
def _fn_sig(self, fn: str) -> dict:
|
|
950
|
+
"""Скомпилированная функция §46: тело по позиционным параметрам,
|
|
951
|
+
статический тип результата против объявленного (`_require_type`
|
|
952
|
+
оракула), фоллибельность тела. Рекурсия у оракула —
|
|
953
|
+
NON_EXECUTABLE_FUNCTION_RECURSION всегда: отказ плана."""
|
|
954
|
+
if fn in self._fn_plan:
|
|
955
|
+
return self._fn_plan[fn]
|
|
956
|
+
if fn in self._fn_stack:
|
|
957
|
+
raise Refused(f"рекурсия функции §46 (NON_EXECUTABLE_FUNCTION_RECURSION) вне среза генератора: {fn}")
|
|
958
|
+
if self._is_a3(fn):
|
|
959
|
+
raise Refused(f"вызов параметра A3 как функции вне среза генератора: {fn}")
|
|
960
|
+
decl, sym = self.fdecls[fn], self.fsyms.get(fn)
|
|
961
|
+
if sym is None:
|
|
962
|
+
raise Refused(f"функция без объявления символа: {fn}")
|
|
963
|
+
for f in sorted(set(decl) - FUNCTION_FIELDS):
|
|
964
|
+
raise Refused(f"поле функции вне среза генератора: {f}")
|
|
965
|
+
params = decl.get("parameters") or []
|
|
966
|
+
urns = [self._t((p.get("type") or {}).get("name")) for p in params]
|
|
967
|
+
kinds = [self.ts(u) for u in urns]
|
|
968
|
+
binds = {p["id"]: ("f", i, kinds[i], urns[i]) for i, p in enumerate(params)}
|
|
969
|
+
declared = self._t((sym.get("returnType") or {}).get("name"))
|
|
970
|
+
returns = self.ts(declared)
|
|
971
|
+
name = _slug(fn)
|
|
972
|
+
if any(v["name"] == name for v in self._fn_plan.values()):
|
|
973
|
+
raise Refused(f"совпадающие имена функций в мире вне среза генератора: {name}")
|
|
974
|
+
self._fn_stack.append(fn)
|
|
975
|
+
try:
|
|
976
|
+
body = self.term(decl["body"], binds)
|
|
977
|
+
actual = self.static_type(decl["body"], binds)
|
|
978
|
+
if actual != declared:
|
|
979
|
+
raise Refused("тип результата функции §46 не совпадает с объявленным вне среза генератора (DECISION-0273)")
|
|
980
|
+
fallible = self._fallible_value(decl["body"], binds)
|
|
981
|
+
finally:
|
|
982
|
+
self._fn_stack.pop()
|
|
983
|
+
out = {"name": name, "id": fn, "params": kinds, "paramTypes": urns, "returns": returns,
|
|
984
|
+
"returnType": declared, "body": body}
|
|
985
|
+
if fallible:
|
|
986
|
+
out["fallible"] = True
|
|
987
|
+
self._fn_plan[fn] = out
|
|
988
|
+
self._function_seen = True
|
|
989
|
+
return out
|
|
990
|
+
|
|
991
|
+
def _fcall(self, node: dict, binds: dict) -> dict:
|
|
992
|
+
"""Вызов функции §46: арность и типы аргументов статически (у оракула —
|
|
993
|
+
TYPE_ERROR при исполнении, здесь отказ плана)."""
|
|
994
|
+
sig = self._fn_sig(node["function"])
|
|
995
|
+
self._function_seen = True
|
|
996
|
+
args = node.get("args") or []
|
|
997
|
+
if len(args) != len(sig["params"]):
|
|
998
|
+
raise Refused("арность вызова функции §46 не совпадает с объявлением вне среза генератора")
|
|
999
|
+
for a, want in zip(args, sig["paramTypes"]):
|
|
1000
|
+
actual = self.static_type(a, binds)
|
|
1001
|
+
if actual != want:
|
|
1002
|
+
raise Refused("тип аргумента функции §46 не совпадает с параметром вне среза генератора (DECISION-0273)")
|
|
1003
|
+
return {"k": "fcall", "fn": sig["name"], "args": [self.term(a, binds) for a in args]}
|
|
1004
|
+
|
|
1005
|
+
def _let_binds(self, node: dict, binds: dict, i: int) -> dict:
|
|
1006
|
+
var = node.get("variable") or {}
|
|
1007
|
+
vt = self._t((var.get("type") or {}).get("name"))
|
|
1008
|
+
kind = self.ts(vt)
|
|
1009
|
+
actual = self.static_type(node.get("value") or {}, binds)
|
|
1010
|
+
if actual != vt:
|
|
1011
|
+
raise Refused("тип значения `let` не совпадает с объявленным вне среза генератора (DECISION-0273)")
|
|
1012
|
+
out = dict(binds)
|
|
1013
|
+
out[str(var.get("id"))] = ("l", i, kind, vt)
|
|
1014
|
+
return out
|
|
1015
|
+
|
|
1016
|
+
def _let_term(self, node: dict, binds: dict) -> dict:
|
|
1017
|
+
"""`let` §55: значение вычисляется один раз, тело — с локальным связыванием;
|
|
1018
|
+
тип значения обязан совпасть с объявленным (`_require_type`)."""
|
|
1019
|
+
i = self._let_n
|
|
1020
|
+
self._let_n += 1
|
|
1021
|
+
inner = self._let_binds(node, binds, i)
|
|
1022
|
+
self._let_seen = True
|
|
1023
|
+
return {"k": "let", "i": i, "v": self.term(node["value"], binds), "b": self.term(node["body"], inner)}
|
|
1024
|
+
|
|
1025
|
+
def _match_arms(self, node: dict, binds: dict) -> tuple:
|
|
1026
|
+
"""Образцы `match` §55.1 над перечислением: вариант без полей —
|
|
1027
|
+
идентичность члена (`_match_pattern`, E-0067), `_` — любой; иные образцы
|
|
1028
|
+
и не-перечисление — отказ. Возвращает (enum, [(член|None, значение)],
|
|
1029
|
+
исчерпывающий ли)."""
|
|
1030
|
+
st = self.static_type(node.get("subject") or {}, binds)
|
|
1031
|
+
if st not in self.enums:
|
|
1032
|
+
raise Refused("match §55.1 над не-перечислением вне среза генератора (DECISION-0273)")
|
|
1033
|
+
members = self.enums[st]
|
|
1034
|
+
arms, covered, wild = [], set(), False
|
|
1035
|
+
for arm in node.get("arms") or []:
|
|
1036
|
+
p = arm.get("pattern") or {}
|
|
1037
|
+
if p.get("kind") == "wildcard":
|
|
1038
|
+
m, wild = None, True
|
|
1039
|
+
elif p.get("kind") == "variant":
|
|
1040
|
+
if p.get("fields"):
|
|
1041
|
+
raise Refused("вариантный образец с полями над перечислением вне среза генератора (§38)")
|
|
1042
|
+
enum_urn, _, member = str(p.get("constructor")).rpartition("/")
|
|
1043
|
+
if enum_urn != st:
|
|
1044
|
+
raise Refused("образец match другого перечисления вне среза генератора (DECISION-0273)")
|
|
1045
|
+
m = next((x for x in members if x.rsplit("#", 1)[-1] == member), None)
|
|
1046
|
+
if m is None:
|
|
1047
|
+
raise Refused(f"член перечисления в образце не объявлен: {p.get('constructor')}")
|
|
1048
|
+
covered.add(m)
|
|
1049
|
+
else:
|
|
1050
|
+
raise Refused(f"образец match вне среза генератора: {p.get('kind')}")
|
|
1051
|
+
arms.append((m, arm.get("value") or {}))
|
|
1052
|
+
if not arms:
|
|
1053
|
+
raise Refused("match без ветвей вне среза генератора")
|
|
1054
|
+
return st, arms, wild or covered >= set(members)
|
|
1055
|
+
|
|
1056
|
+
def _match_type(self, node: dict, binds: dict) -> str | None:
|
|
1057
|
+
_st, arms, _ex = self._match_arms(node, binds)
|
|
1058
|
+
kinds = {self.static_type(v, binds) for _m, v in arms}
|
|
1059
|
+
if len(kinds) != 1:
|
|
1060
|
+
raise Refused("ветви match §55.1 разных видов вне среза генератора (DECISION-0273)")
|
|
1061
|
+
return kinds.pop()
|
|
1062
|
+
|
|
1063
|
+
def _match_term(self, node: dict, binds: dict) -> dict:
|
|
1064
|
+
self._match_type(node, binds)
|
|
1065
|
+
_st, arms, exhaustive = self._match_arms(node, binds)
|
|
1066
|
+
self._match_seen = True
|
|
1067
|
+
out = {"k": "match", "s": self.term(node["subject"], binds),
|
|
1068
|
+
"arms": [{"m": m, "v": self.term(v, binds)} for m, v in arms]}
|
|
1069
|
+
if not exhaustive:
|
|
1070
|
+
# §55.1: ни один образец не совпал — NON_EXHAUSTIVE_MATCH по подстановке.
|
|
1071
|
+
out["x"] = False
|
|
1072
|
+
return out
|
|
1073
|
+
|
|
1074
|
+
def _if_term(self, node: dict, binds: dict) -> dict:
|
|
1075
|
+
"""Условный терм §56 (E-0180, DECISION-0270): условие — чистое
|
|
1076
|
+
data-условие (`values.is_data_condition`: boolean, comparison, and,
|
|
1077
|
+
or); статусный тест в условии читает опору, не данные — отказ. Ветви
|
|
1078
|
+
одного статического вида; иное — отказ (у оракула вид по значению
|
|
1079
|
+
ветви, срез видами ветвей расходился бы с ключом атома). Ветвь
|
|
1080
|
+
вычисляется лениво — только выбранная (`select_conditionals`)."""
|
|
1081
|
+
cond = self._data_cond(node.get("condition"), binds)
|
|
1082
|
+
kt = self.static_type(node.get("then") or {}, binds)
|
|
1083
|
+
ke = self.static_type(node.get("else") or {}, binds)
|
|
1084
|
+
if kt != ke:
|
|
1085
|
+
raise Refused("ветви условного терма §56 разных видов вне среза генератора (DECISION-0270)")
|
|
1086
|
+
self._if_seen = True
|
|
1087
|
+
return {"k": "if", "c": cond, "t": self.term(node["then"], binds), "e": self.term(node["else"], binds)}
|
|
1088
|
+
|
|
1089
|
+
def _data_cond(self, f: dict, binds: dict) -> dict:
|
|
1090
|
+
k = (f or {}).get("kind")
|
|
1091
|
+
if k == "boolean":
|
|
1092
|
+
return {"k": "bool", "v": bool(f.get("value"))}
|
|
1093
|
+
if k == "comparison":
|
|
1094
|
+
if f.get("op") not in CMP:
|
|
1095
|
+
raise Refused(f"сравнение вне среза генератора: {f.get('op')}")
|
|
1096
|
+
self.guard_enums(f, binds)
|
|
1097
|
+
self.guard_kinds(f, binds)
|
|
1098
|
+
self.guard_instant(f, binds)
|
|
1099
|
+
return {"k": "cmp", "op": f["op"], "l": self.term(f["left"], binds), "r": self.term(f["right"], binds)}
|
|
1100
|
+
if k in ("and", "or"):
|
|
1101
|
+
items = f.get("items") or []
|
|
1102
|
+
if not items:
|
|
1103
|
+
raise Refused("пустое условие терма §56 вне среза генератора")
|
|
1104
|
+
return {"k": k, "items": [self._data_cond(x, binds) for x in items]}
|
|
1105
|
+
raise Refused(f"условие терма §56 вне data-условия вне среза генератора (DECISION-0270): {k}")
|
|
1106
|
+
|
|
1107
|
+
def _body_arg(self, ai: int, j: int, t: str, arg: dict, expected: str, binds: dict) -> dict:
|
|
1108
|
+
"""Фильтр атома тела по аргументу. Константа — как прежде (`body_const`);
|
|
1109
|
+
вычисляемый терм (E-0180, DECISION-0270) — тип обязан совпасть с типом
|
|
1110
|
+
слота статически (оракул сопоставляет канонические байты, значение И
|
|
1111
|
+
тип), значение считается ДО перебора таблицы (`pre`), ошибка значения —
|
|
1112
|
+
issue правила (`err`), как у гарда."""
|
|
1113
|
+
if arg["kind"] in ("value", "entity_ref", "const_ref"):
|
|
1114
|
+
return {"row": ai, "col": j, "t": t, "term": self.body_const(arg, expected)}
|
|
1115
|
+
if arg["kind"] not in ("binary", "call", "if", "let", "match"):
|
|
1116
|
+
raise Refused(f"терм в аргументе тела вне среза генератора: {arg['kind']}")
|
|
1117
|
+
missing = sorted(set(self._free(arg)) - set(binds))
|
|
1118
|
+
if missing:
|
|
1119
|
+
raise Refused(f"переменная без позитивного генератора (§190/§113): {', '.join(missing)}")
|
|
1120
|
+
actual = self.static_type(arg, binds)
|
|
1121
|
+
if actual != expected and (expected in TYPES or expected in self.enums):
|
|
1122
|
+
raise Refused("тип аргумента тела не совпадает с параметром §189")
|
|
1123
|
+
self._body_term_seen = True
|
|
1124
|
+
out = {"row": ai, "col": j, "t": t, "term": self.term(arg, binds), "pre": True}
|
|
1125
|
+
if self._fallible_calendar(arg) or self._fallible_value(arg, binds):
|
|
1126
|
+
out["err"] = True
|
|
1127
|
+
return out
|
|
1128
|
+
|
|
1129
|
+
def _month_call(self, node: dict, binds: dict) -> dict:
|
|
1130
|
+
"""§84.2 (E-0084, DECISION-0267): граница месяца — чистая функция над
|
|
1131
|
+
Date. Не Date у оракула всегда TYPE_ERROR — отказ плана; над слотом
|
|
1132
|
+
Date ошибки значения нет, поэтому вызов не фоллибелен и модели не
|
|
1133
|
+
меняет (как `days_between`, DECISION-0245 п. 1)."""
|
|
1134
|
+
name = MONTH_FNS[node["function"]]
|
|
1135
|
+
args = node.get("args") or []
|
|
1136
|
+
if len(args) != 1:
|
|
1137
|
+
raise Refused(f"{name}: арность вне среза генератора (§84.2)")
|
|
1138
|
+
out = self.term(args[0], binds)
|
|
1139
|
+
if self.static_type(args[0], binds) != "urn:law:std#Date":
|
|
1140
|
+
raise Refused(f"{name}: аргумент не Date вне среза генератора (§57/§2.15)")
|
|
1141
|
+
self._month_seen = True
|
|
1142
|
+
return {"k": "call", "fn": name, "args": [out]}
|
|
1143
|
+
|
|
1144
|
+
def _duration_call(self, node: dict, binds: dict) -> dict:
|
|
1145
|
+
"""§84.1 (W2 часть 2): `hours_between`/`minutes_between` — полных единиц
|
|
1146
|
+
между мгновениями со знаком; `add_duration` — мгновение через целую
|
|
1147
|
+
Quantity секунд/минут/часов/суток, канон UTC. Виды аргументов
|
|
1148
|
+
проверяются статически, как `values` при исполнении: не Instant, не
|
|
1149
|
+
Quantity, чужая единица, дробный счётчик — TYPE_ERROR оракула всегда,
|
|
1150
|
+
здесь отказ плана. Смешение записи со смещением и без — отказ рантайма."""
|
|
1151
|
+
name = DUR_FNS[node["function"]]
|
|
1152
|
+
args = node.get("args") or []
|
|
1153
|
+
|
|
1154
|
+
def moment(a):
|
|
1155
|
+
out = self.term(a, binds)
|
|
1156
|
+
if self.static_type(a, binds) != INST:
|
|
1157
|
+
raise Refused(f"{name}: аргумент не Instant вне среза генератора (§84.1/§2.15)")
|
|
1158
|
+
return out
|
|
1159
|
+
|
|
1160
|
+
self._instant_seen = True
|
|
1161
|
+
if name != "add_duration":
|
|
1162
|
+
if len(args) != 2:
|
|
1163
|
+
raise Refused(f"{name}: арность вне среза генератора (§84.1)")
|
|
1164
|
+
return {"k": "call", "fn": name, "args": [moment(args[0]), moment(args[1])]}
|
|
1165
|
+
if len(args) != 2:
|
|
1166
|
+
raise Refused("add_duration: арность вне среза генератора (§84.1)")
|
|
1167
|
+
count = args[1]
|
|
1168
|
+
if count.get("kind") != "value" or (count.get("type") or {}).get("name") != QTY:
|
|
1169
|
+
raise Refused("add_duration: длительность не литерал Quantity вне среза генератора (§84.1)")
|
|
1170
|
+
unit = count.get("unit")
|
|
1171
|
+
if unit not in DURATION_UNITS:
|
|
1172
|
+
raise Refused(f"add_duration: единица {unit} вне среза генератора (§84.1)")
|
|
1173
|
+
try:
|
|
1174
|
+
value = float(str(count.get("value")))
|
|
1175
|
+
except ValueError:
|
|
1176
|
+
raise Refused("add_duration: длительность не число вне среза генератора (§84.1)")
|
|
1177
|
+
if value != int(value):
|
|
1178
|
+
raise Refused("add_duration: дробная длительность вне среза генератора (§84.1)")
|
|
1179
|
+
return {"k": "call", "fn": "add_duration", "args": [moment(args[0]), {"k": "qty", "v": int(value), "unit": unit}]}
|
|
1180
|
+
|
|
1181
|
+
def _calendar_call(self, node: dict, binds: dict) -> dict:
|
|
1182
|
+
"""Календарная функция §84–§86 (DECISION-0245). Аргументы проверяются
|
|
1183
|
+
статически так, как `values` проверяет их при исполнении: то, что у
|
|
1184
|
+
оракула всегда TYPE_ERROR (не Date, чужая единица, дробный счётчик),
|
|
1185
|
+
здесь отказ плана; ошибки политики и снимка — исполнения."""
|
|
1186
|
+
name = CAL_FNS[node["function"]][0]
|
|
1187
|
+
args = node.get("args") or []
|
|
1188
|
+
date = "urn:law:std#Date"
|
|
1189
|
+
|
|
1190
|
+
def dated(a):
|
|
1191
|
+
out = self.term(a, binds)
|
|
1192
|
+
if self.static_type(a, binds) != date:
|
|
1193
|
+
raise Refused(f"{name}: аргумент не Date вне среза генератора (§57/§2.15)")
|
|
1194
|
+
return out
|
|
1195
|
+
|
|
1196
|
+
if name == "days_between":
|
|
1197
|
+
if len(args) != 2:
|
|
1198
|
+
raise Refused("days_between: арность вне среза генератора (§84.4)")
|
|
1199
|
+
return {"k": "call", "fn": name, "args": [dated(args[0]), dated(args[1])]}
|
|
1200
|
+
if len(args) not in (2, 3) or (len(args) == 3 and name != "add_business_days"):
|
|
1201
|
+
raise Refused(f"{name}: арность вне среза генератора (§86)")
|
|
1202
|
+
count = args[1]
|
|
1203
|
+
if count.get("kind") != "value" and self.static_type(count, binds) == QTY:
|
|
1204
|
+
# DECISION-0256 п. 6: связыватель Quantity — вид, единица и целочисленность
|
|
1205
|
+
# на деле (`counter` рантайма); у `add_legal_term` ветвь выбирает единица.
|
|
1206
|
+
self._quantity_seen = True
|
|
1207
|
+
out = {"k": "call", "fn": name,
|
|
1208
|
+
"args": [dated(args[0]), {"k": "qtyvar", "fn": name, "term": self.term(count, binds)}]}
|
|
1209
|
+
if len(args) == 3:
|
|
1210
|
+
if self.static_type(args[2], binds) != "urn:law:std#Text":
|
|
1211
|
+
raise Refused("add_business_days: after_time не Text вне среза генератора (§86 cutoff)")
|
|
1212
|
+
out["args"].append(self.term(args[2], binds))
|
|
1213
|
+
return out
|
|
1214
|
+
if count.get("kind") != "value" or (count.get("type") or {}).get("name") != "urn:law:std#Quantity":
|
|
1215
|
+
raise Refused(f"{name}: счётчик не литерал Quantity вне среза генератора (DECISION-0245 п. 2)")
|
|
1216
|
+
unit = count.get("unit")
|
|
1217
|
+
allowed = {"add_calendar_period": CALENDAR_PERIOD_UNITS, "add_business_days": DAY_UNITS,
|
|
1218
|
+
"add_legal_term": DEADLINE_UNITS}[name]
|
|
1219
|
+
if unit not in allowed:
|
|
1220
|
+
raise Refused(f"{name}: единица счётчика {unit} вне среза генератора (E-0029/E-0109)")
|
|
1221
|
+
try:
|
|
1222
|
+
value = float(str(count.get("value")))
|
|
1223
|
+
except ValueError:
|
|
1224
|
+
raise Refused(f"{name}: счётчик не число вне среза генератора (§84)")
|
|
1225
|
+
if value != int(value):
|
|
1226
|
+
raise Refused(f"{name}: дробный счётчик вне среза генератора (§84)")
|
|
1227
|
+
if name == "add_legal_term":
|
|
1228
|
+
# §86.1: единица выбирает одну из двух нормативных операций.
|
|
1229
|
+
name = "add_business_days" if unit in DAY_UNITS else "add_calendar_period"
|
|
1230
|
+
out = {"k": "call", "fn": name, "args": [dated(args[0]), {"k": "qty", "v": int(value), "unit": unit}]}
|
|
1231
|
+
if len(args) == 3:
|
|
1232
|
+
if self.static_type(args[2], binds) != "urn:law:std#Text":
|
|
1233
|
+
raise Refused("add_business_days: after_time не Text вне среза генератора (§86 cutoff)")
|
|
1234
|
+
out["args"].append(self.term(args[2], binds))
|
|
1235
|
+
return out
|
|
1236
|
+
|
|
1237
|
+
@staticmethod
|
|
1238
|
+
def _fallible_calendar(node) -> bool:
|
|
1239
|
+
"""Терм содержит календарную функцию с ошибкой значения (§86)."""
|
|
1240
|
+
stack = [node]
|
|
1241
|
+
while stack:
|
|
1242
|
+
x = stack.pop()
|
|
1243
|
+
if isinstance(x, list):
|
|
1244
|
+
stack.extend(x)
|
|
1245
|
+
elif isinstance(x, dict):
|
|
1246
|
+
if x.get("kind") == "call" and x.get("function") in CAL_FNS \
|
|
1247
|
+
and x["function"] != "urn:law:std#days_between":
|
|
1248
|
+
return True
|
|
1249
|
+
stack.extend(v for v in x.values() if isinstance(v, (dict, list)))
|
|
1250
|
+
return False
|
|
1251
|
+
|
|
1252
|
+
def static_type(self, node: dict, binds: dict) -> str | None:
|
|
1253
|
+
"""Тип терма головы, известный статически: URN типа `std` либо None,
|
|
1254
|
+
если проверять нечего (сущность — у `entity_ref` носителя типа нет)."""
|
|
1255
|
+
k = node["kind"]
|
|
1256
|
+
if k == "var":
|
|
1257
|
+
if node["var"] not in binds:
|
|
1258
|
+
raise Refused(f"несвязанная переменная в голове: {node['var']}")
|
|
1259
|
+
bind = binds[node["var"]]
|
|
1260
|
+
if bind[0] in ("p", "f", "l"):
|
|
1261
|
+
urn = bind[3]
|
|
1262
|
+
return urn if urn in TYPES or urn in self.enums else None
|
|
1263
|
+
pred = self._atoms[bind[0]]["predicate"]
|
|
1264
|
+
return self._slot_type(pred, self._var_col(node["var"], binds))
|
|
1265
|
+
if k == "value":
|
|
1266
|
+
return (node.get("type") or {}).get("name")
|
|
1267
|
+
if k == "const_ref":
|
|
1268
|
+
if self.is_member(node["id"]):
|
|
1269
|
+
return self.members[node["id"]]
|
|
1270
|
+
if self._is_a3(node["id"]):
|
|
1271
|
+
return self._a3params[node["id"]]["type"] if node["id"] in self._a3params \
|
|
1272
|
+
else self._a3params[self._a3_term(node["id"])["id"]]["type"]
|
|
1273
|
+
return self.static_type(self.const_value(node["id"]), binds)
|
|
1274
|
+
if k == "let":
|
|
1275
|
+
return self.static_type(node.get("body") or {}, self._let_binds(node, binds, -1))
|
|
1276
|
+
if k == "match":
|
|
1277
|
+
return self._match_type(node, binds)
|
|
1278
|
+
if k == "call" and node.get("function") in self.fdecls:
|
|
1279
|
+
return self._fn_sig(node["function"])["returnType"]
|
|
1280
|
+
if k == "binary":
|
|
1281
|
+
return self.arith_kind(node["op"], self.static_type(node["left"], binds),
|
|
1282
|
+
self.static_type(node["right"], binds))
|
|
1283
|
+
if k == "call" and node.get("function") in CAL_FNS:
|
|
1284
|
+
return INT if node["function"] == "urn:law:std#days_between" else "urn:law:std#Date"
|
|
1285
|
+
if k == "call" and node.get("function") in MONTH_FNS:
|
|
1286
|
+
return "urn:law:std#Date"
|
|
1287
|
+
if k == "call" and node.get("function") in DUR_FNS:
|
|
1288
|
+
return INST if DUR_FNS[node["function"]] == "add_duration" else INT
|
|
1289
|
+
if k == "if":
|
|
1290
|
+
kt = self.static_type(node.get("then") or {}, binds)
|
|
1291
|
+
ke = self.static_type(node.get("else") or {}, binds)
|
|
1292
|
+
if kt != ke:
|
|
1293
|
+
raise Refused("ветви условного терма §56 разных видов вне среза генератора (DECISION-0270)")
|
|
1294
|
+
return kt
|
|
1295
|
+
if k == "call":
|
|
1296
|
+
return self.call_kind(node["function"], [self.static_type(a, binds) for a in node["args"]])
|
|
1297
|
+
if k == "aggregate" and self.v09:
|
|
1298
|
+
return self._aggregate(node, binds)[1]
|
|
1299
|
+
return None
|
|
1300
|
+
|
|
1301
|
+
def arith_kind(self, op: str, l: str | None, r: str | None) -> str | None:
|
|
1302
|
+
"""Таблицы видов §58 для среза (Integer, Decimal, Money; Rational и
|
|
1303
|
+
динамический Decimal|Rational — DECISION-0252). Пара вне таблицы —
|
|
1304
|
+
отказ плана, как TYPE_ERROR у оракула; вид, зависящий от значения,
|
|
1305
|
+
план несёт динамическим, а ошибку решает рантайм (`arith`, issue
|
|
1306
|
+
модели 0.12). Операция вне BINOP сюда не доходит."""
|
|
1307
|
+
if op not in BINOP:
|
|
1308
|
+
return None
|
|
1309
|
+
l, r = _num(l), _num(r)
|
|
1310
|
+
if l not in NUMERIC or r not in NUMERIC:
|
|
1311
|
+
raise Refused(f"арифметика вне среза (§58): не числовой операнд {op}")
|
|
1312
|
+
if RAT in (l, r) or DYN in (l, r) or QTY in (l, r):
|
|
1313
|
+
if QTY in (l, r):
|
|
1314
|
+
self._quantity_seen = True
|
|
1315
|
+
kind = self._arith_rational(op, l, r)
|
|
1316
|
+
if RAT in (l, r) or DYN in (l, r) or kind in (RAT, DYN):
|
|
1317
|
+
self._rational_seen = True
|
|
1318
|
+
return kind
|
|
1319
|
+
pair = {l, r}
|
|
1320
|
+
if op in ("add", "sub"):
|
|
1321
|
+
if l != r:
|
|
1322
|
+
raise Refused(f"арифметика вне среза (§58): виды операндов не совпадают у {op}")
|
|
1323
|
+
return l
|
|
1324
|
+
if op == "mul":
|
|
1325
|
+
if pair == {MON}:
|
|
1326
|
+
raise Refused("арифметика вне среза (§58): произведение двух размерных величин не определено")
|
|
1327
|
+
if INT in pair:
|
|
1328
|
+
return l if r == INT else r # скаляр §49 не меняет вида
|
|
1329
|
+
return MON if MON in pair else DEC
|
|
1330
|
+
if op == "div":
|
|
1331
|
+
if (l, r) == (DEC, DEC) or (l, r) == (MON, MON):
|
|
1332
|
+
# E-0005/E-0083: вид частного зависит от значения (DECISION-0252 п. 2).
|
|
1333
|
+
self._rational_seen = True
|
|
1334
|
+
return DYN
|
|
1335
|
+
if (l, r) == (MON, DEC):
|
|
1336
|
+
return MON
|
|
1337
|
+
if (l, r) == (INT, INT):
|
|
1338
|
+
# E-0020: единственный источник Rational из целых.
|
|
1339
|
+
self._rational_seen = True
|
|
1340
|
+
return RAT
|
|
1341
|
+
raise Refused("арифметика вне среза (§58): пара вне таблицы делимых")
|
|
1342
|
+
if op == "mod":
|
|
1343
|
+
if (l, r) != (INT, INT):
|
|
1344
|
+
raise Refused("арифметика вне среза (§58): остаток только Integer mod Integer")
|
|
1345
|
+
return INT
|
|
1346
|
+
return None
|
|
1347
|
+
|
|
1348
|
+
def call_kind(self, fn: str, kinds: list) -> str | None:
|
|
1349
|
+
name = STD_FN.get(fn)
|
|
1350
|
+
kinds = [_num(k) for k in kinds]
|
|
1351
|
+
if name in ("min", "max"):
|
|
1352
|
+
if len(kinds) == 2 and (RAT in kinds or DYN in kinds or QTY in kinds):
|
|
1353
|
+
# DECISION-0252/0256: выбор операнда требует совпадения видов, как
|
|
1354
|
+
# сложение; динамический операнд совпадает своим исходом.
|
|
1355
|
+
if QTY in kinds:
|
|
1356
|
+
self._quantity_seen = True
|
|
1357
|
+
if RAT in kinds or DYN in kinds:
|
|
1358
|
+
self._rational_seen = True
|
|
1359
|
+
return self._arith_rational("add", kinds[0], kinds[1])
|
|
1360
|
+
if len(kinds) != 2 or kinds[0] not in NUMERIC or kinds[0] != kinds[1]:
|
|
1361
|
+
raise Refused(f"арифметика вне среза (§58): виды операндов не совпадают у {name}")
|
|
1362
|
+
return kinds[0]
|
|
1363
|
+
if name == "abs":
|
|
1364
|
+
if len(kinds) != 1 or kinds[0] not in NUMERIC:
|
|
1365
|
+
raise Refused("арифметика вне среза (§58): abs над нечисловым видом")
|
|
1366
|
+
if kinds[0] in (RAT, DYN):
|
|
1367
|
+
self._rational_seen = True
|
|
1368
|
+
if kinds[0] == QTY:
|
|
1369
|
+
self._quantity_seen = True
|
|
1370
|
+
return kinds[0]
|
|
1371
|
+
if name == "pow":
|
|
1372
|
+
if kinds != [INT, INT]:
|
|
1373
|
+
raise Refused("арифметика вне среза (§58): pow только Integer pow Integer")
|
|
1374
|
+
return INT
|
|
1375
|
+
if name == "round":
|
|
1376
|
+
# §50, E-0110: над Integer при precision >= 0 — тождество того же вида.
|
|
1377
|
+
if kinds and kinds[0] == RAT:
|
|
1378
|
+
self._rational_seen = True
|
|
1379
|
+
raise Refused("round над Rational вне среза генератора (§50: round не берёт Rational)")
|
|
1380
|
+
if kinds and kinds[0] == QTY:
|
|
1381
|
+
self._quantity_seen = True # §50: round сохраняет тег (DECISION-0256)
|
|
1382
|
+
if kinds and kinds[0] == DYN:
|
|
1383
|
+
# DECISION-0259 п. 3: Rational по значению — TYPE_ERROR рантайма.
|
|
1384
|
+
self._rational_seen = self._round_seen = True
|
|
1385
|
+
elif not kinds or kinds[0] not in (MON, DEC, INT, QTY):
|
|
1386
|
+
raise Refused("round вне среза генератора: вид операнда не числовой (§50)")
|
|
1387
|
+
self._policy_kinds("round", kinds[1:])
|
|
1388
|
+
return DEC if kinds[0] == DYN else kinds[0]
|
|
1389
|
+
if name == "div_round":
|
|
1390
|
+
# §50/§58 (E-0036, E-0170, DECISION-0259 п. 3): четыре пары; иная —
|
|
1391
|
+
# у оракула всегда TYPE_ERROR, отказ плана.
|
|
1392
|
+
if len(kinds) != 4:
|
|
1393
|
+
raise Refused("div_round вне среза генератора: ожидаются делимое, делитель, точность и режим (§50)")
|
|
1394
|
+
self._div_round_seen = True
|
|
1395
|
+
if RAT in kinds[:2]:
|
|
1396
|
+
self._rational_seen = True
|
|
1397
|
+
raise Refused("div_round над Rational вне среза генератора (§50/§58, E-0036)")
|
|
1398
|
+
if DYN in kinds[:2]:
|
|
1399
|
+
self._rational_seen = True
|
|
1400
|
+
if QTY in kinds[:2]:
|
|
1401
|
+
self._quantity_seen = True
|
|
1402
|
+
pair = tuple(DEC if k == DYN else k for k in kinds[:2])
|
|
1403
|
+
if pair not in ((MON, DEC), (QTY, DEC), (DEC, DEC), (MON, MON)):
|
|
1404
|
+
raise Refused("div_round вне среза генератора: пара видов вне E-0036/E-0170 (§50/§58)")
|
|
1405
|
+
self._policy_kinds("div_round", kinds[2:])
|
|
1406
|
+
return {(MON, DEC): MON, (QTY, DEC): QTY}.get(pair, DEC)
|
|
1407
|
+
return None
|
|
1408
|
+
|
|
1409
|
+
@staticmethod
|
|
1410
|
+
def _policy_kinds(fn: str, kinds: list) -> None:
|
|
1411
|
+
"""§50: точность — Integer либо Decimal, режим — Text; иной вид у оракула
|
|
1412
|
+
всегда TYPE_ERROR (DECISION-0259 п. 3)."""
|
|
1413
|
+
if len(kinds) != 2:
|
|
1414
|
+
raise Refused(f"{fn} вне среза генератора: ожидаются точность и режим (§50)")
|
|
1415
|
+
if kinds[0] not in (INT, DEC):
|
|
1416
|
+
raise Refused(f"{fn} вне среза генератора: точность не Integer и не Decimal (§50)")
|
|
1417
|
+
if kinds[1] != TEXT_URN:
|
|
1418
|
+
raise Refused(f"{fn} вне среза генератора: режим не Text (§50)")
|
|
1419
|
+
|
|
1420
|
+
@staticmethod
|
|
1421
|
+
def _pair_kinds(op: str, a: str, b: str) -> set:
|
|
1422
|
+
"""Виды исхода конкретной пары §58 — порт `values.binary`; пустое
|
|
1423
|
+
множество — у оракула TYPE_ERROR. Decimal/Decimal и Money/Money дают
|
|
1424
|
+
Decimal либо Rational по значению (E-0005, E-0083)."""
|
|
1425
|
+
if op in ("add", "sub"):
|
|
1426
|
+
return {a} if a == b else set()
|
|
1427
|
+
if op == "mul":
|
|
1428
|
+
if a == b == INT:
|
|
1429
|
+
return {INT}
|
|
1430
|
+
if INT in (a, b) and {a, b} & {MON, DEC, QTY}:
|
|
1431
|
+
return {a, b} - {INT}
|
|
1432
|
+
if {a, b} == {INT, RAT} or a == b == RAT:
|
|
1433
|
+
return {RAT}
|
|
1434
|
+
if {a, b} == {MON, RAT}:
|
|
1435
|
+
return {MON} # E-0085: доля масштабирует деньги
|
|
1436
|
+
if RAT in (a, b) or a == b == MON or QTY in (a, b) and {a, b} != {QTY, DEC}:
|
|
1437
|
+
return set() # Q×Q, Q×Money, Q×Rational — TYPE_ERROR
|
|
1438
|
+
return {QTY} if QTY in (a, b) else {MON} if MON in (a, b) else {DEC}
|
|
1439
|
+
if op == "div":
|
|
1440
|
+
if a == b == RAT or a == b == INT:
|
|
1441
|
+
return {RAT}
|
|
1442
|
+
if RAT in (a, b):
|
|
1443
|
+
return set()
|
|
1444
|
+
if (a, b) in ((DEC, DEC), (MON, MON), (QTY, QTY)):
|
|
1445
|
+
return {DEC, RAT} # E-0068: частное одной единицы безразмерно
|
|
1446
|
+
return {MON} if (a, b) == (MON, DEC) else {QTY} if (a, b) == (QTY, DEC) else set()
|
|
1447
|
+
if op == "mod":
|
|
1448
|
+
return {INT} if a == b == INT else set()
|
|
1449
|
+
return set()
|
|
1450
|
+
|
|
1451
|
+
def _arith_rational(self, op: str, l: str, r: str) -> str:
|
|
1452
|
+
"""Пара §58 с Rational либо динамическим операндом (DECISION-0252 п. 2):
|
|
1453
|
+
вид — у исходов, которые не ошибка; пара без таких исходов у оракула
|
|
1454
|
+
всегда TYPE_ERROR и отвергается планом."""
|
|
1455
|
+
outcomes = set()
|
|
1456
|
+
for a in ((DEC, RAT) if l == DYN else (l,)):
|
|
1457
|
+
for b in ((DEC, RAT) if r == DYN else (r,)):
|
|
1458
|
+
outcomes |= self._pair_kinds(op, a, b)
|
|
1459
|
+
if not outcomes:
|
|
1460
|
+
raise Refused(f"арифметика вне среза (§58): {l.split('#')[-1]} {op} {r.split('#')[-1]} "
|
|
1461
|
+
"у оракула всегда TYPE_ERROR")
|
|
1462
|
+
if outcomes == {DEC, RAT}:
|
|
1463
|
+
return DYN
|
|
1464
|
+
if len(outcomes) != 1:
|
|
1465
|
+
raise Refused(f"арифметика вне среза (§58): вид исхода {op} не определён статически")
|
|
1466
|
+
return outcomes.pop()
|
|
1467
|
+
|
|
1468
|
+
def _lit(self, node: dict) -> dict | None:
|
|
1469
|
+
"""Литерал за ссылками на константы §54 либо None."""
|
|
1470
|
+
while node.get("kind") == "const_ref" and not self.is_member(node["id"]):
|
|
1471
|
+
node = self.const_value(node["id"])
|
|
1472
|
+
return node if node.get("kind") == "value" else None
|
|
1473
|
+
|
|
1474
|
+
def _policy(self, node: dict, binds: dict) -> list:
|
|
1475
|
+
"""DECISION-0259 п. 1–3: политика `round`/`div_round`. Литеральная
|
|
1476
|
+
точность либо режим, у оракула всегда TYPE_ERROR, — отказ плана.
|
|
1477
|
+
`value:div_round` — любой `div_round`; `value:round` — `round` с
|
|
1478
|
+
нелитеральной политикой, режимом не HALF_UP либо динамическим видом.
|
|
1479
|
+
Результат — статические виды аргументов `ak` (`t` — Text)."""
|
|
1480
|
+
fn = STD_FN[node["function"]]
|
|
1481
|
+
args = node["args"]
|
|
1482
|
+
kinds = [self.static_type(a, binds) for a in args]
|
|
1483
|
+
prec, mode = self._lit(args[-2]), self._lit(args[-1])
|
|
1484
|
+
if prec is not None:
|
|
1485
|
+
raw = str(prec["value"])
|
|
1486
|
+
if "." in raw or "e" in raw.lower():
|
|
1487
|
+
raise Refused(f"{fn} вне среза генератора: точность не целое (§50)")
|
|
1488
|
+
scale = int(raw)
|
|
1489
|
+
if scale < 0 or (fn == "div_round" and scale > DIV_ROUND_MAX_PRECISION):
|
|
1490
|
+
raise Refused(f"{fn} вне среза генератора: точность вне диапазона (§50, E-0036)")
|
|
1491
|
+
if mode is not None and mode.get("value") not in ROUNDING_MODES:
|
|
1492
|
+
raise Refused(f"{fn} вне среза генератора: режим вне семи (§50)")
|
|
1493
|
+
if fn == "div_round":
|
|
1494
|
+
self._div_round_seen = True
|
|
1495
|
+
elif prec is None or mode is None or mode["value"] != "HALF_UP" or kinds[0] == DYN:
|
|
1496
|
+
self._round_seen = True
|
|
1497
|
+
return [KIND_CODE.get(k, "t") for k in kinds]
|
|
1498
|
+
|
|
1499
|
+
def _policy_fallible(self, node: dict, binds: dict) -> bool:
|
|
1500
|
+
"""DECISION-0259 п. 5: вызов может дать ошибку значения. `round` — при
|
|
1501
|
+
нелитеральной политике либо динамическом виде; `div_round` — кроме
|
|
1502
|
+
ненулевого литерального делителя при литеральной политике и паре не
|
|
1503
|
+
Money/Money."""
|
|
1504
|
+
args = node["args"]
|
|
1505
|
+
if self._lit(args[-2]) is None or self._lit(args[-1]) is None:
|
|
1506
|
+
return True
|
|
1507
|
+
kinds = [self.static_type(a, binds) for a in args[:-2]]
|
|
1508
|
+
if DYN in kinds:
|
|
1509
|
+
return True
|
|
1510
|
+
if STD_FN[node["function"]] == "round":
|
|
1511
|
+
return False
|
|
1512
|
+
divisor = self._lit(args[1])
|
|
1513
|
+
return divisor is None or kinds == [MON, MON] or \
|
|
1514
|
+
not any(ch in "123456789" for ch in str(divisor["value"]))
|
|
1515
|
+
|
|
1516
|
+
def kind_checks(self, head: dict, binds: dict) -> list:
|
|
1517
|
+
"""Позиции головы с динамическим видом в слоте Decimal либо Rational —
|
|
1518
|
+
сверка E-0077 рантаймом по значению (DECISION-0252 п. 5)."""
|
|
1519
|
+
params = self.types.get(head["predicate"]) or []
|
|
1520
|
+
return [[pos, KIND_CODE[expected]]
|
|
1521
|
+
for pos, (expected, arg) in enumerate(zip(params, head["args"]))
|
|
1522
|
+
if expected in (DEC, RAT) and self.static_type(arg, binds) == DYN]
|
|
1523
|
+
|
|
1524
|
+
def _fallible_value(self, node, binds: dict, gens: list | None = None) -> bool:
|
|
1525
|
+
"""Терм может дать ошибку значения §58 (DECISION-0252 п. 4): деление и
|
|
1526
|
+
остаток (ноль, неточность), арифметика над Rational и динамическим
|
|
1527
|
+
видом, сравнение с динамическим видом. Расхождение валют у денег сюда
|
|
1528
|
+
не входит: как и до плана 0.12, это названный отказ рантайма. Внутрь
|
|
1529
|
+
агрегата обход не спускается: его ошибки — G3."""
|
|
1530
|
+
saved = self._atoms
|
|
1531
|
+
if gens is not None:
|
|
1532
|
+
self._atoms = gens
|
|
1533
|
+
try:
|
|
1534
|
+
stack = [node]
|
|
1535
|
+
while stack:
|
|
1536
|
+
x = stack.pop()
|
|
1537
|
+
if isinstance(x, list):
|
|
1538
|
+
stack.extend(x)
|
|
1539
|
+
continue
|
|
1540
|
+
if not isinstance(x, dict) or x.get("kind") in ("aggregate", "comprehension"):
|
|
1541
|
+
continue
|
|
1542
|
+
if x.get("kind") == "let":
|
|
1543
|
+
# DECISION-0273: тело — с локальным связыванием.
|
|
1544
|
+
if self._fallible_value(x.get("value"), binds) \
|
|
1545
|
+
or self._fallible_value(x.get("body"), self._let_binds(x, binds, -1)):
|
|
1546
|
+
return True
|
|
1547
|
+
continue
|
|
1548
|
+
if x.get("kind") == "match":
|
|
1549
|
+
_st, arms, exhaustive = self._match_arms(x, binds)
|
|
1550
|
+
if not exhaustive:
|
|
1551
|
+
return True # NON_EXHAUSTIVE_MATCH по подстановке
|
|
1552
|
+
stack.append(x.get("subject"))
|
|
1553
|
+
stack.extend(v for _m, v in arms)
|
|
1554
|
+
continue
|
|
1555
|
+
if x.get("kind") == "const_ref" and self._is_a3(x["id"]):
|
|
1556
|
+
return True # MISSING_PARAMETER_VALUE на разрыве датасета
|
|
1557
|
+
if x.get("kind") == "call" and x.get("function") in self.fdecls \
|
|
1558
|
+
and self._fn_sig(x["function"]).get("fallible"):
|
|
1559
|
+
return True # ошибка значения в теле функции §46
|
|
1560
|
+
if x.get("kind") in ("binary", "comparison"):
|
|
1561
|
+
kinds = {self.static_type(x["left"], binds), self.static_type(x["right"], binds)}
|
|
1562
|
+
if x["kind"] == "binary" and (x.get("op") in ("div", "mod") or kinds & {RAT, DYN, QTY}):
|
|
1563
|
+
return True
|
|
1564
|
+
if x["kind"] == "comparison" and (DYN in kinds or kinds == {QTY}):
|
|
1565
|
+
return True # DECISION-0256 п. 4: разные единицы
|
|
1566
|
+
if x.get("kind") == "call" and STD_FN.get(x.get("function")) in ("round", "div_round") \
|
|
1567
|
+
and self._policy_fallible(x, binds):
|
|
1568
|
+
return True # DECISION-0259 п. 5
|
|
1569
|
+
stack.extend(v for v in x.values() if isinstance(v, (dict, list)))
|
|
1570
|
+
return False
|
|
1571
|
+
finally:
|
|
1572
|
+
self._atoms = saved
|
|
1573
|
+
|
|
1574
|
+
def guard_instant(self, g: dict, binds: dict) -> None:
|
|
1575
|
+
"""§57/§2.15 (W2 часть 2): Instant сравнивается только с Instant; с Date у
|
|
1576
|
+
оракула DATE_INSTANT_MISMATCH, с прочими видами TYPE_ERROR — всегда,
|
|
1577
|
+
поэтому отказ плана. Стоит отдельно от `guard_kinds`: L0-путь строгих
|
|
1578
|
+
правил её не звал, и расширять там сверку Rational значило бы менять
|
|
1579
|
+
закреплённые исходы."""
|
|
1580
|
+
kinds = []
|
|
1581
|
+
for side in ("left", "right"):
|
|
1582
|
+
try:
|
|
1583
|
+
kinds.append(self.static_type(g[side], binds))
|
|
1584
|
+
except Refused:
|
|
1585
|
+
return
|
|
1586
|
+
for x, y in ((kinds[0], kinds[1]), (kinds[1], kinds[0])):
|
|
1587
|
+
if x == INST and y is not None and y != INST:
|
|
1588
|
+
raise Refused("сравнение Instant с иным видом вне среза генератора (§57/§2.15)")
|
|
1589
|
+
|
|
1590
|
+
def guard_kinds(self, g: dict, binds: dict) -> None:
|
|
1591
|
+
"""§57/§48 (DECISION-0252 п. 2): Rational сравнивается только с Rational;
|
|
1592
|
+
сравнение, которое у оракула всегда TYPE_ERROR, — отказ плана."""
|
|
1593
|
+
kinds = []
|
|
1594
|
+
for side in ("left", "right"):
|
|
1595
|
+
try:
|
|
1596
|
+
kinds.append(_num(self.static_type(g[side], binds)))
|
|
1597
|
+
except Refused:
|
|
1598
|
+
return
|
|
1599
|
+
for x, y in ((kinds[0], kinds[1]), (kinds[1], kinds[0])):
|
|
1600
|
+
if (x == RAT and y in (INT, DEC, MON)) or (x == DYN and y in (INT, MON, QTY)) \
|
|
1601
|
+
or (x == QTY and y in (INT, DEC, MON, RAT)):
|
|
1602
|
+
raise Refused("сравнение Rational с иным видом вне среза генератора (§57/§48)")
|
|
1603
|
+
|
|
1604
|
+
def _var_col(self, name: str, binds: dict) -> int:
|
|
1605
|
+
return binds[name][1]
|
|
1606
|
+
|
|
1607
|
+
def _slot_type(self, pred: str, col: int) -> str | None:
|
|
1608
|
+
t = self.types[pred][col]
|
|
1609
|
+
return t if t in TYPES or t in self.enums else None
|
|
1610
|
+
|
|
1611
|
+
def head_types(self, head: dict, binds: dict) -> None:
|
|
1612
|
+
"""Типы аргументов ВЫВЕДЕННОЙ головы против сигнатуры §189 (E-0077).
|
|
1613
|
+
|
|
1614
|
+
Оракул отвергает вывод с Integer в Decimal-слоте при исполнении
|
|
1615
|
+
(`check_derived_types`), а рантайм среза сравнивает числа по величине
|
|
1616
|
+
и ответил бы TRUE_ONLY там, где оракул даёт NEITHER. Здесь это отказ
|
|
1617
|
+
плана: сущности не проверяются (у `entity_ref` типа нет — как у
|
|
1618
|
+
оракула), остальное обязано совпасть с объявленным типом слота."""
|
|
1619
|
+
params = self.types.get(head["predicate"])
|
|
1620
|
+
if params is None:
|
|
1621
|
+
raise Refused(f"предикат головы не объявлен в пакете: {head['predicate']}")
|
|
1622
|
+
if len(params) != len(head["args"]):
|
|
1623
|
+
raise Refused("арность головы правила не совпадает с сигнатурой §189")
|
|
1624
|
+
for arg in head["args"]:
|
|
1625
|
+
if arg.get("kind") == "const_ref" and self._is_a3(arg["id"]):
|
|
1626
|
+
# DECISION-0273: голый параметр в голове — позиция идентичности у оракула
|
|
1627
|
+
# (`a3_runner`: «голый const_ref в голове … не вычисляется»).
|
|
1628
|
+
raise Refused("голый `const_ref` параметра A3 в позиции идентичности (голова, аргумент атома) вне среза генератора: у оракула он не вычисляется, читайте вызовом (DECISION-0273)")
|
|
1629
|
+
for expected, arg in zip(params, head["args"]):
|
|
1630
|
+
actual = self.static_type(arg, binds)
|
|
1631
|
+
if actual is None or (expected not in TYPES and expected not in self.enums):
|
|
1632
|
+
continue
|
|
1633
|
+
if actual == DYN and expected in (DEC, RAT):
|
|
1634
|
+
continue # DECISION-0252 п. 5: сверка рантаймом (`kindChecks`)
|
|
1635
|
+
if actual != expected:
|
|
1636
|
+
raise Refused("тип аргумента головы не совпадает с параметром §189 (E-0077)")
|
|
1637
|
+
|
|
1638
|
+
def guard_enums(self, g: dict, binds: dict) -> None:
|
|
1639
|
+
"""§57/E-0067 для гарда: порядок над перечислением не определён, члены
|
|
1640
|
+
разных перечислений несравнимы — у оракула это TYPE_ERROR при
|
|
1641
|
+
исполнении, рантайм среза ответил бы false. Здесь — отказ плана.
|
|
1642
|
+
То же для Boolean (DECISION-0266): `values.binary` сравнивает виды
|
|
1643
|
+
(`_check_same_kind`), а порядок над `boolean` — TYPE_ERROR."""
|
|
1644
|
+
kinds = []
|
|
1645
|
+
for side in ("left", "right"):
|
|
1646
|
+
try:
|
|
1647
|
+
kinds.append(self.static_type(g[side], binds))
|
|
1648
|
+
except Refused:
|
|
1649
|
+
kinds.append(None)
|
|
1650
|
+
if BOOL in kinds:
|
|
1651
|
+
if kinds[0] != kinds[1]:
|
|
1652
|
+
raise Refused("сравнение Boolean с иным видом вне среза генератора (§57/§48)")
|
|
1653
|
+
if g["op"] not in ("eq", "ne"):
|
|
1654
|
+
raise Refused("порядок над Boolean не определён §57 вне среза генератора")
|
|
1655
|
+
enum_sides = [t for t in kinds if t in self.enums]
|
|
1656
|
+
if not enum_sides:
|
|
1657
|
+
return
|
|
1658
|
+
if g["op"] not in ("eq", "ne"):
|
|
1659
|
+
raise Refused("порядок над перечислением не определён §57 (E-0067)")
|
|
1660
|
+
if len(set(enum_sides)) > 1:
|
|
1661
|
+
raise Refused("члены разных перечислений несравнимы §57 (E-0067)")
|
|
1662
|
+
|
|
1663
|
+
def is_member(self, cid: str) -> bool:
|
|
1664
|
+
"""`const_ref` — член перечисления §38, если это не объявленная константа
|
|
1665
|
+
со значением: таблица разыменования §54 знает объявленные константы и
|
|
1666
|
+
применяется первой, а одно имя бывает и константой, и членом
|
|
1667
|
+
перечисления того же пакета (`kz.corpus.mortgage`,
|
|
1668
|
+
`ObrashchenieVSobstvennost`)."""
|
|
1669
|
+
c = self.consts.get(cid)
|
|
1670
|
+
return cid in self.members and not (c is not None and c.get("value") is not None)
|
|
1671
|
+
|
|
1672
|
+
def const_value(self, cid: str) -> dict:
|
|
1673
|
+
"""Значение константы §54; ссылка на функцию или параметр A3 через
|
|
1674
|
+
`const_ref` — отдельный отказ: обзор корпуса иначе читал их как
|
|
1675
|
+
«константа не найдена» и обещал дешёвый рычаг, которого нет."""
|
|
1676
|
+
c = self.consts.get(cid)
|
|
1677
|
+
if c is not None and c.get("value") is not None:
|
|
1678
|
+
return c["value"]
|
|
1679
|
+
if cid in self.functions:
|
|
1680
|
+
raise Refused(f"ссылка на функцию или параметр через const_ref вне среза генератора: {cid}")
|
|
1681
|
+
if c is not None:
|
|
1682
|
+
raise Refused(f"константа без значения вне среза генератора: {cid}")
|
|
1683
|
+
raise Refused(f"константа не найдена в CLIR: {cid}")
|
|
1684
|
+
|
|
1685
|
+
def body_const(self, arg: dict, expected: str) -> dict:
|
|
1686
|
+
"""Константа в аргументе атома тела: оракул сопоставляет её с ground-
|
|
1687
|
+
атомом по каноническим байтам терма (`solver.solve`), то есть по
|
|
1688
|
+
значению И типу. Рантайм сравнивает `eq` — по значению, поэтому тип
|
|
1689
|
+
литерала обязан совпасть с объявленным типом слота статически: литерал
|
|
1690
|
+
Integer в Decimal-слоте у оракула не совпадает ни с одной строкой, а
|
|
1691
|
+
рантайм по величине ответил бы иначе. Сущности типа не несут, как у
|
|
1692
|
+
оракула. Подстановочный `_` §98 и терм с вычислением — отказ."""
|
|
1693
|
+
k = arg["kind"]
|
|
1694
|
+
if k == "wildcard":
|
|
1695
|
+
raise Refused("подстановочный аргумент `_` §98 вне среза генератора")
|
|
1696
|
+
if k in ("binary", "call", "if", "let", "match"):
|
|
1697
|
+
# DECISION-0270: вычисляемый аргумент требует модели 0.9 — признак
|
|
1698
|
+
# для второго прохода `build_plan`, план строится заново с `term:body`.
|
|
1699
|
+
self._body_term_seen = True
|
|
1700
|
+
if k not in ("value", "entity_ref", "const_ref"):
|
|
1701
|
+
raise Refused(f"терм в аргументе тела вне среза генератора: {k}")
|
|
1702
|
+
if k == "const_ref" and self._is_a3(arg["id"]):
|
|
1703
|
+
# DECISION-0273: голый параметр в атоме — идентичность у оракула (как член
|
|
1704
|
+
# перечисления), не значение; корпус читает параметр вызовом (`round(MRP, …)`).
|
|
1705
|
+
raise Refused("голый `const_ref` параметра A3 в позиции идентичности (голова, аргумент атома) вне среза генератора: у оракула он не вычисляется, читайте вызовом (DECISION-0273)")
|
|
1706
|
+
if k == "const_ref" and self.is_member(arg["id"]):
|
|
1707
|
+
# Член перечисления — идентичность `{namespace}#{Член}` (§38); одно
|
|
1708
|
+
# имя члена бывает у нескольких перечислений пакета, поэтому
|
|
1709
|
+
# проверяется принадлежность слоту, а не «тип члена».
|
|
1710
|
+
if expected not in self.enums or arg["id"] not in self.enums[expected]:
|
|
1711
|
+
raise Refused("тип аргумента тела не совпадает с параметром §189")
|
|
1712
|
+
return self.term(arg, {})
|
|
1713
|
+
actual = self.static_type(arg, {})
|
|
1714
|
+
if actual is not None and (expected in TYPES or expected in self.enums) and actual != expected:
|
|
1715
|
+
raise Refused("тип аргумента тела не совпадает с параметром §189")
|
|
1716
|
+
return self.term(arg, {})
|
|
1717
|
+
|
|
1718
|
+
def parts(self, body: dict, atoms: list, guards: list) -> None:
|
|
1719
|
+
k = body.get("kind")
|
|
1720
|
+
if k == "status":
|
|
1721
|
+
if body.get("status") != "established":
|
|
1722
|
+
raise Refused(f"статусный тест вне L0-среза: {body.get('status')}")
|
|
1723
|
+
atoms.append(body["formula"])
|
|
1724
|
+
elif k == "literal":
|
|
1725
|
+
atoms.append(body)
|
|
1726
|
+
elif k == "comparison":
|
|
1727
|
+
if body["op"] not in CMP:
|
|
1728
|
+
raise Refused(f"сравнение вне среза генератора: {body['op']}")
|
|
1729
|
+
guards.append(body)
|
|
1730
|
+
elif k in ("and", "conjunction"):
|
|
1731
|
+
subs = body.get("items") or body.get("args") or body.get("operands")
|
|
1732
|
+
if not subs:
|
|
1733
|
+
raise Refused("конъюнкция без членов в CLIR")
|
|
1734
|
+
for s in subs:
|
|
1735
|
+
self.parts(s, atoms, guards)
|
|
1736
|
+
else:
|
|
1737
|
+
raise Refused(f"форма тела вне среза генератора: {k}")
|
|
1738
|
+
|
|
1739
|
+
# --- Единицы проверки. `build` останавливается на первом отказе, `survey`
|
|
1740
|
+
# проходит все единицы и собирает КАЖДЫЙ отказ: карта причин по корпусу
|
|
1741
|
+
# нужна для выбора следующего шага среза, а первая причина скрывает
|
|
1742
|
+
# остальные. Внутри одной единицы (правило, факт) отказ по-прежнему первый.
|
|
1743
|
+
|
|
1744
|
+
def _node(self, n: dict) -> None:
|
|
1745
|
+
sig = (n.get("kind"), n.get("symbolKind"))
|
|
1746
|
+
if sig == ("source_edition", None):
|
|
1747
|
+
# Жизненный цикл редакции §31/§92.3 решает, участвует ли
|
|
1748
|
+
# заякоренное правило в вычислении: это НЕ провенанс. План
|
|
1749
|
+
# сворачивает применимость в окно по дате права (`_dating`).
|
|
1750
|
+
return
|
|
1751
|
+
if sig in (("source_publication", None), ("publication", None), ("fragment", None)):
|
|
1752
|
+
# Провенанс §194: закреплённый текст, его хэши и адрес. На
|
|
1753
|
+
# вычисление не влияет ни в одной реализации — как source_work.
|
|
1754
|
+
return
|
|
1755
|
+
if sig == ("norm_template", None) and self.v10:
|
|
1756
|
+
# Шаблон нормы §122 разбирает `_norm_v10` у ссылающегося правила.
|
|
1757
|
+
for f in sorted(set(n) - TEMPLATE_FIELDS):
|
|
1758
|
+
raise Refused(f"поле шаблона нормы вне среза генератора: {f}")
|
|
1759
|
+
return
|
|
1760
|
+
if sig in (("deadline_policy", None), ("calendar_snapshot", None)) and self.v11:
|
|
1761
|
+
# Политика §86 и снимок §85 выбираются делом (DECISION-0245).
|
|
1762
|
+
return
|
|
1763
|
+
if sig == ("closure_policy", None) and self.v09:
|
|
1764
|
+
# Замыкание §70 разбирает `_closure` (DECISION-0239 G4).
|
|
1765
|
+
return
|
|
1766
|
+
if sig == ("external_decl", None) and n.get("effect") == "judgment":
|
|
1767
|
+
# DECISION-0274: носитель органа канала суждения §47.3 — читается
|
|
1768
|
+
# в `judgments` (`queries.judgment_declarations`).
|
|
1769
|
+
for f in sorted(set(n) - EXTERNAL_JUDGMENT_FIELDS):
|
|
1770
|
+
raise Refused(f"поле external_decl канала суждения вне среза генератора: {f}")
|
|
1771
|
+
return
|
|
1772
|
+
if sig == ("priority_rule", None) and self.l1:
|
|
1773
|
+
# Приоритет §106 разбирает `_priority`: в срезе только безусловное
|
|
1774
|
+
# недатированное ребро между правилами (DECISION-0237 F4).
|
|
1775
|
+
return
|
|
1776
|
+
if sig[1] is None and sig[0] in INERT_NODE_KINDS:
|
|
1777
|
+
# Инертное объявление: свидетель — пара векторов манифеста
|
|
1778
|
+
# `inert-nodes.json` (source_work §28, anchor §33, revision §32,
|
|
1779
|
+
# entrenchment §92.3, factor_vocabulary/precedent_decl §201,
|
|
1780
|
+
# counterfactual_decl §186).
|
|
1781
|
+
return
|
|
1782
|
+
if sig not in KNOWN_NODES:
|
|
1783
|
+
raise Refused(f"объявление вне среза генератора: {n.get('kind')}/{n.get('symbolKind')}")
|
|
1784
|
+
if n.get("kind") == "rule":
|
|
1785
|
+
for f in sorted(set(n) - RULE_FIELDS):
|
|
1786
|
+
if f in RULE_EMPTY_OK and not n[f]:
|
|
1787
|
+
continue
|
|
1788
|
+
if f in INERT_RULE_FIELDS:
|
|
1789
|
+
# Инертное поле правила (`visibility` §23.2, DECISION-0386):
|
|
1790
|
+
# свидетель — пара манифеста инертности.
|
|
1791
|
+
continue
|
|
1792
|
+
if f == "scope" and n[f] == {"kind": "boolean", "value": True}:
|
|
1793
|
+
continue
|
|
1794
|
+
raise Refused(f"поле правила вне среза генератора: {f}")
|
|
1795
|
+
|
|
1796
|
+
def _index(self) -> list[dict]:
|
|
1797
|
+
ir = self.ir
|
|
1798
|
+
rels = [n for n in ir["nodes"] if n.get("symbolKind") == "relation"]
|
|
1799
|
+
self.root_ns = (ir.get("package") or {}).get("namespace", "")
|
|
1800
|
+
by_id = {n["id"]: n for n in ir["nodes"] if "id" in n}
|
|
1801
|
+
self.editions = {n["id"]: edition_intervals(n) for n in ir["nodes"] if n.get("kind") == "source_edition"}
|
|
1802
|
+
self.frag_edition = {n["id"]: n.get("edition") for n in ir["nodes"] if n.get("kind") == "fragment"}
|
|
1803
|
+
self.by_id = by_id
|
|
1804
|
+
self.entities = {n["id"] for n in ir["nodes"] if n.get("typeKind") == "entity"}
|
|
1805
|
+
self.consts = {n["id"]: {"type": (n.get("returnType") or {}).get("name"),
|
|
1806
|
+
"value": n.get("value") if isinstance(n.get("value"), dict) else None}
|
|
1807
|
+
for n in ir["nodes"] if n.get("symbolKind") in ("constant", "const")}
|
|
1808
|
+
self.functions = {n["id"] for n in ir["nodes"] if n.get("symbolKind") == "function"}
|
|
1809
|
+
# DECISION-0273: тела функций §46 по символу и их объявления.
|
|
1810
|
+
self.fdecls = {n["symbol"]: n for n in ir["nodes"] if n.get("kind") == "function_decl" and n.get("symbol")}
|
|
1811
|
+
self.fsyms = {n["id"]: n for n in ir["nodes"] if n.get("symbolKind") == "function"}
|
|
1812
|
+
# Перечисления §38: член — значение со stable qualified identity
|
|
1813
|
+
# `{namespace}#{Член}`; узла у члена нет, ссылка на него — `const_ref`,
|
|
1814
|
+
# который оракул оставляет в ключе атома как есть (§54: таблица
|
|
1815
|
+
# разыменования знает только объявленные константы).
|
|
1816
|
+
# Пространство имён члена — пакет-владелец перечисления (в мире
|
|
1817
|
+
# импортов узлы разных пакетов лежат рядом; `package` узла — его URN).
|
|
1818
|
+
ns = (ir.get("package") or {}).get("namespace", "")
|
|
1819
|
+
self.enums = {n["id"]: [f"{n.get('package') or ns}#{v}" for v in (n.get("variants") or [])]
|
|
1820
|
+
for n in ir["nodes"] if n.get("kind") == "type_decl" and n.get("typeKind") == "enum"}
|
|
1821
|
+
self.members = {m: e for e, ms in self.enums.items() for m in ms}
|
|
1822
|
+
# Наследование сущностей (`entity A : B`): константа подтипа
|
|
1823
|
+
# подходит параметру базового типа — как у оракула (`_conforms`).
|
|
1824
|
+
self.bases = {n["id"]: (n.get("base") or {}).get("name") for n in ir["nodes"]
|
|
1825
|
+
if n.get("kind") == "type_decl" and (n.get("base") or {}).get("name")}
|
|
1826
|
+
self.types = {r["id"]: [(p.get("type") or {}).get("name") for p in r["parameters"]] for r in rels}
|
|
1827
|
+
# §40 (E-0097/E-0098): алиас типа прозрачен на исполнении — как у оракула
|
|
1828
|
+
# (`facts._unalias`: обе стороны сверки §189 берутся за цепочкой алиасов).
|
|
1829
|
+
# Цель алиаса компилятор пишет в форме `имя.пакета#Тип`; вид слота берётся
|
|
1830
|
+
# у объявления пакета мира с ТОЧНО этим именем. Имя без пакета в мире
|
|
1831
|
+
# остаётся строкой цели и уходит в отказ с этим именем (CODEGEN-COVERAGE-PLAN W2).
|
|
1832
|
+
# Нормализуются ТОЛЬКО ссылки на объявленные алиасы: сырые строки
|
|
1833
|
+
# `имя.пакета#Тип` в сигнатурах оракул сравнивает как есть, их не трогает
|
|
1834
|
+
# и `_foreign_type_aliases` ниже.
|
|
1835
|
+
names = {(ir.get("package") or {}).get("name"): ns}
|
|
1836
|
+
names.update({w["name"]: w["namespace"] for w in self.world_passport})
|
|
1837
|
+
self.aliases = {}
|
|
1838
|
+
for n in ir["nodes"]:
|
|
1839
|
+
if n.get("kind") != "type_decl" or n.get("typeKind") != "alias":
|
|
1840
|
+
continue
|
|
1841
|
+
target = (n.get("target") or {}).get("name")
|
|
1842
|
+
if not target:
|
|
1843
|
+
continue
|
|
1844
|
+
if not target.startswith("urn:") and target.count("#") == 1:
|
|
1845
|
+
package, local = target.split("#")
|
|
1846
|
+
if names.get(package):
|
|
1847
|
+
target = f"{names[package]}#{local}"
|
|
1848
|
+
self.aliases[n["id"]] = target
|
|
1849
|
+
if self.aliases:
|
|
1850
|
+
self.types = {r: [self._t(t) for t in ts] for r, ts in self.types.items()}
|
|
1851
|
+
for c in self.consts.values():
|
|
1852
|
+
c["type"] = self._t(c["type"])
|
|
1853
|
+
self.bases = {k: self._t(v) for k, v in self.bases.items()}
|
|
1854
|
+
# §261: сущности стандартной библиотеки — идентичности, как сущности пакета;
|
|
1855
|
+
# слот `Interval<T>` — непрозрачная идентичность (релиз 0.1.5).
|
|
1856
|
+
self.entities |= STD_ENTITY_TYPES | STD_OPAQUE_SLOT_TYPES
|
|
1857
|
+
self._foreign_type_aliases(ir, rels)
|
|
1858
|
+
self.public = {r["id"] for r in rels if r.get("visibility") == "public"}
|
|
1859
|
+
self.rel_kind = {r["id"]: r.get("relationKind") for r in rels}
|
|
1860
|
+
# DECISION-0274: отношения суждения §47.3 — орган и форма запроса
|
|
1861
|
+
# (`queries.judgment_declarations`): `external_decl` перекрывает
|
|
1862
|
+
# исторический объект `judgment` символа; без обоих орган — сам символ.
|
|
1863
|
+
self.judgments = {}
|
|
1864
|
+
for r in rels:
|
|
1865
|
+
if r.get("relationKind") == "judgment":
|
|
1866
|
+
legacy = r.get("judgment") or {}
|
|
1867
|
+
self.judgments[r["id"]] = {"authority": legacy.get("authority", r["id"])}
|
|
1868
|
+
if legacy.get("requestSchema") is not None:
|
|
1869
|
+
self.judgments[r["id"]]["requestSchema"] = legacy["requestSchema"]
|
|
1870
|
+
for n in ir["nodes"]:
|
|
1871
|
+
if n.get("kind") == "external_decl" and n.get("effect") == "judgment" and n.get("symbol") in self.judgments:
|
|
1872
|
+
d = self.judgments[n["symbol"]]
|
|
1873
|
+
authority = (n.get("authority") or {}).get("id")
|
|
1874
|
+
if authority:
|
|
1875
|
+
d["authority"] = authority
|
|
1876
|
+
if "requestType" in n:
|
|
1877
|
+
d["requestSchema"] = n["requestType"]
|
|
1878
|
+
if self.judgments:
|
|
1879
|
+
influenced = set(self.judgments)
|
|
1880
|
+
dependencies = []
|
|
1881
|
+
for rule in self.ir["nodes"]:
|
|
1882
|
+
if rule.get("kind") != "rule":
|
|
1883
|
+
continue
|
|
1884
|
+
body = rule.get("body") or {}
|
|
1885
|
+
stack, reads, variables = [body], set(), set()
|
|
1886
|
+
while stack:
|
|
1887
|
+
part = stack.pop()
|
|
1888
|
+
if isinstance(part, list): stack.extend(part)
|
|
1889
|
+
elif isinstance(part, dict):
|
|
1890
|
+
if part.get("kind") == "literal": reads.add(part.get("predicate"))
|
|
1891
|
+
if part.get("kind") == "var": variables.add(part["var"])
|
|
1892
|
+
stack.extend(v for v in part.values() if isinstance(v, (dict, list)))
|
|
1893
|
+
head = rule.get("head") or {}
|
|
1894
|
+
headvars = {a["var"] for a in head.get("args") or [] if a.get("kind") == "var"}
|
|
1895
|
+
dependencies.append((head.get("predicate"), reads, variables - headvars))
|
|
1896
|
+
while True:
|
|
1897
|
+
more = {head for head, reads, _ in dependencies if reads & influenced}
|
|
1898
|
+
if more <= influenced: break
|
|
1899
|
+
influenced |= more
|
|
1900
|
+
if any(hidden and reads & influenced for _, reads, hidden in dependencies):
|
|
1901
|
+
raise Refused("JUDGMENT_WITNESS_UNAVAILABLE: witness-bound backchaining requires E-0189")
|
|
1902
|
+
if self.judgments and "aggregate" in self.caps:
|
|
1903
|
+
raise Refused("AGGREGATE_JUDGMENT_UNAVAILABLE: open aggregate members require E-0357")
|
|
1904
|
+
self.rel_labels = {r["id"]: _labels(r) for r in rels}
|
|
1905
|
+
# Провенанс для комментариев API: фрагмент → локатор и редакция §194.
|
|
1906
|
+
editions = {n["id"]: _labels(n) for n in ir["nodes"] if n.get("kind") == "source_edition"}
|
|
1907
|
+
self.fragments = {n["id"]: {"locator": n.get("locator"),
|
|
1908
|
+
"edition": (editions.get(n.get("edition")) or [{}])[0].get("text")}
|
|
1909
|
+
for n in ir["nodes"] if n.get("kind") == "fragment"}
|
|
1910
|
+
self.rule_nodes = {n["id"]: n for n in ir["nodes"] if n.get("kind") == "rule"}
|
|
1911
|
+
return rels
|
|
1912
|
+
|
|
1913
|
+
def table_name(self, pred: str) -> str:
|
|
1914
|
+
"""Имя таблицы в порождённом коде: локальное имя; при коллизии между
|
|
1915
|
+
пакетами мира — с префиксом пространства имён."""
|
|
1916
|
+
if not hasattr(self, "_names"):
|
|
1917
|
+
self._names = {}
|
|
1918
|
+
name = _slug(pred)
|
|
1919
|
+
if self._names.get(name, pred) != pred:
|
|
1920
|
+
name = _slug(pred.split("#")[0]) + "__" + name
|
|
1921
|
+
if self._names.get(name, pred) != pred:
|
|
1922
|
+
# Два отношения с одним именем таблицы и после префикса — иначе
|
|
1923
|
+
# порождённый код молча свёл бы их в одну таблицу.
|
|
1924
|
+
raise Refused(f"совпадающие имена таблиц в мире вне среза генератора: {name}")
|
|
1925
|
+
self._names[name] = pred
|
|
1926
|
+
return name
|
|
1927
|
+
|
|
1928
|
+
def _table(self, r: dict) -> dict:
|
|
1929
|
+
# Тип КАЖДОГО параметра каждого отношения проверяется здесь, а не только
|
|
1930
|
+
# у атомов правил: таблица с параметром вне среза (Rational, Instant,
|
|
1931
|
+
# Date) не может хранить значения дела, и отказ обязан жить в плане, а
|
|
1932
|
+
# не всплывать в раннере одной из целей (T151/T155 до этой проверки).
|
|
1933
|
+
# Объявленные типы уезжают в план: раннер сверяет с ними аргументы
|
|
1934
|
+
# дела по §189/§48 (errata E-0046) так же, как оракул.
|
|
1935
|
+
# §45.1: ключевая проекция. Два различных accepted-кортежа при одной
|
|
1936
|
+
# проекции на ключ — KEY_CONFLICT (§196); evaluator ничего не выбирает
|
|
1937
|
+
# и не удаляет, ответ от этого не меняется. Ключи уезжают в план
|
|
1938
|
+
# как позиции столбцов: раннер считает конфликты после насыщения —
|
|
1939
|
+
# у оракула в счёт идут и утверждения, и strict-выводы.
|
|
1940
|
+
arity = len(r["parameters"])
|
|
1941
|
+
keys = []
|
|
1942
|
+
for key in r.get("keys") or []:
|
|
1943
|
+
if not key or any(not isinstance(i, int) or i < 0 or i >= arity for i in key):
|
|
1944
|
+
raise Refused("ключ отношения §45.1 вне арности")
|
|
1945
|
+
keys.append(list(key))
|
|
1946
|
+
for t in self.types[r["id"]]:
|
|
1947
|
+
self.ts(t)
|
|
1948
|
+
return {"pred": r["id"], "name": self.table_name(r["id"]), "arity": arity,
|
|
1949
|
+
"params": list(self.types[r["id"]]), "keys": keys,
|
|
1950
|
+
"kinds": [self.ts(t) for t in self.types[r["id"]]]}
|
|
1951
|
+
|
|
1952
|
+
def _dating(self, node: dict):
|
|
1953
|
+
"""Окна применимости узла по его якорям (§92.3): `None` — узел вне
|
|
1954
|
+
датировки (якорь не разрешается), иначе объединение окон датирующих
|
|
1955
|
+
редакций. Редакция без событий применимости даёт пустое окно (§27.2,
|
|
1956
|
+
errata E-0350): узел, все якоря которого ведут на такие редакции,
|
|
1957
|
+
снят на всей оси. Достаточно одной применимой редакции; граница не
|
|
1958
|
+
дневной гранулярности читается как вне датировки — как у оракула
|
|
1959
|
+
(узел остаётся, предупреждение)."""
|
|
1960
|
+
windows, dated = [], False
|
|
1961
|
+
for anchor in node.get("anchors") or []:
|
|
1962
|
+
target = self.by_id.get(anchor)
|
|
1963
|
+
eid = anchor if target is not None and target.get("kind") == "source_edition" \
|
|
1964
|
+
else self.frag_edition.get(anchor)
|
|
1965
|
+
iv = self.editions.get(eid) if eid else None
|
|
1966
|
+
if iv is None or isinstance(iv, str):
|
|
1967
|
+
return None
|
|
1968
|
+
dated = True
|
|
1969
|
+
windows += iv
|
|
1970
|
+
return _union(windows) if dated else None
|
|
1971
|
+
|
|
1972
|
+
def _rule(self, i: int, rule: dict) -> dict:
|
|
1973
|
+
if rule.get("strength") not in (None, "strict"):
|
|
1974
|
+
raise Refused(f"сила правила вне L0-среза: {rule.get('strength')}")
|
|
1975
|
+
atoms, guards = [], []
|
|
1976
|
+
self.parts(rule["body"], atoms, guards)
|
|
1977
|
+
if not atoms:
|
|
1978
|
+
raise Refused("тело без перебираемых атомов вне среза генератора")
|
|
1979
|
+
binds, joins, filters, plan_atoms = {}, [], [], []
|
|
1980
|
+
self._atoms = atoms
|
|
1981
|
+
for ai, atom in enumerate(atoms):
|
|
1982
|
+
if atom.get("polarity") != "positive":
|
|
1983
|
+
raise Refused("отрицательный литерал в теле вне L0-среза")
|
|
1984
|
+
params = self.types.get(atom["predicate"])
|
|
1985
|
+
if params is None:
|
|
1986
|
+
raise Refused(f"предикат тела не объявлен в пакете: {atom['predicate']}")
|
|
1987
|
+
if len(params) != len(atom["args"]):
|
|
1988
|
+
raise Refused("арность атома тела не совпадает с сигнатурой §189")
|
|
1989
|
+
plan_atoms.append({"pred": atom["predicate"]})
|
|
1990
|
+
for j, arg in enumerate(atom["args"]):
|
|
1991
|
+
t = self.ts(params[j])
|
|
1992
|
+
if self._wild(plan_atoms[-1], arg, j, len(params)):
|
|
1993
|
+
continue
|
|
1994
|
+
if arg["kind"] != "var":
|
|
1995
|
+
filters.append({"row": ai, "col": j, "t": t, "term": self.body_const(arg, params[j])})
|
|
1996
|
+
continue
|
|
1997
|
+
cell = (ai, j, t)
|
|
1998
|
+
prev = binds.get(arg["var"])
|
|
1999
|
+
if prev is None:
|
|
2000
|
+
binds[arg["var"]] = cell
|
|
2001
|
+
else:
|
|
2002
|
+
joins.append([list(prev), list(cell)])
|
|
2003
|
+
head = rule["head"]
|
|
2004
|
+
if head.get("polarity") != "positive":
|
|
2005
|
+
raise Refused("отрицательная голова правила вне L0-среза")
|
|
2006
|
+
self.head_types(head, binds)
|
|
2007
|
+
window, dating = self._window(rule)
|
|
2008
|
+
for g in guards:
|
|
2009
|
+
self.guard_enums(g, binds)
|
|
2010
|
+
self.guard_instant(g, binds)
|
|
2011
|
+
out = {
|
|
2012
|
+
"name": f"{i}_{_slug(rule['id'])}", "id": rule["id"], "window": window,
|
|
2013
|
+
"atoms": plan_atoms, "joins": joins, "filters": filters,
|
|
2014
|
+
"guards": [{"op": g["op"], "l": self.term(g["left"], binds), "r": self.term(g["right"], binds)}
|
|
2015
|
+
for g in guards],
|
|
2016
|
+
"head": {"pred": head["predicate"], "args": [self.term(a, binds) for a in head["args"]]},
|
|
2017
|
+
}
|
|
2018
|
+
self._remember(out, rule, "strict", [("established", a["predicate"], "positive") for a in atoms], dating)
|
|
2019
|
+
return out
|
|
2020
|
+
|
|
2021
|
+
def _wild(self, atom: dict, arg: dict, j: int, arity: int) -> bool:
|
|
2022
|
+
"""§98 (E-0165, DECISION-0268): `_` в аргументе положительного литерала
|
|
2023
|
+
тела — существование: позиция совпадает с любым значением, ничего не
|
|
2024
|
+
связывает, и строки, различающиеся только в ней, дают ОДНУ
|
|
2025
|
+
подстановку (`solver.solve`: дедупликация по остальным столбцам).
|
|
2026
|
+
План отмечает у атома столбцы `wild` и `keep`; эмиттер печатает
|
|
2027
|
+
дедупликацию по проекции на `keep`. Свидетель у оракула — наименьший
|
|
2028
|
+
по ключу — влияет только на proof, которого срез не несёт."""
|
|
2029
|
+
if arg.get("kind") != "wildcard":
|
|
2030
|
+
return False
|
|
2031
|
+
self._wildcard_seen = True
|
|
2032
|
+
atom.setdefault("wild", []).append(j)
|
|
2033
|
+
atom["keep"] = [c for c in range(arity) if c not in atom["wild"]]
|
|
2034
|
+
return True
|
|
2035
|
+
|
|
2036
|
+
@staticmethod
|
|
2037
|
+
def _has_wild(lit: dict) -> bool:
|
|
2038
|
+
return any(isinstance(a, dict) and a.get("kind") == "wildcard" for a in lit.get("args") or [])
|
|
2039
|
+
|
|
2040
|
+
def _window(self, rule: dict):
|
|
2041
|
+
"""Окно действия §31 правила и окна датирующих редакций §92.3."""
|
|
2042
|
+
eff = rule.get("effective")
|
|
2043
|
+
window = None
|
|
2044
|
+
if eff:
|
|
2045
|
+
if eff.get("axis") != "legal_time":
|
|
2046
|
+
raise Refused(f"ось окна effective вне среза генератора: {eff.get('axis')}")
|
|
2047
|
+
if eff.get("startInclusive") is False or eff.get("endInclusive") is True:
|
|
2048
|
+
raise Refused("полузакрытость окна effective вне среза генератора")
|
|
2049
|
+
window = {"start": eff.get("start"), "end": eff.get("end")}
|
|
2050
|
+
# Редакции §92.3: окно правила — пересечение `effective` с
|
|
2051
|
+
# применимостью датирующих редакций. Несколько окон в одном языке
|
|
2052
|
+
# не выразимы прежним планом — названный отказ; пустое окно — правило
|
|
2053
|
+
# исключено (у оракула — issue `EDITION_NOT_APPLICABLE`, ответ тот же).
|
|
2054
|
+
dating = self._dating(rule)
|
|
2055
|
+
if dating is not None:
|
|
2056
|
+
base = (window or {}).get("start"), (window or {}).get("end")
|
|
2057
|
+
cut = [w for w in (_intersect(base, d) for d in dating) if w is not None]
|
|
2058
|
+
if not cut:
|
|
2059
|
+
raise _Excluded(rule["id"])
|
|
2060
|
+
if len(cut) > 1:
|
|
2061
|
+
raise Refused("прерывистая применимость редакции §92.3 вне среза генератора")
|
|
2062
|
+
window = {"start": cut[0][0], "end": cut[0][1]}
|
|
2063
|
+
return window, dating
|
|
2064
|
+
|
|
2065
|
+
def _remember(self, out: dict, rule: dict, strength: str, reads: list, dating) -> None:
|
|
2066
|
+
"""Сведения о правиле для структурного анализа (не входят в план):
|
|
2067
|
+
сила, голова, чтения `(статус, предикат, полярность)` и датировка
|
|
2068
|
+
редакцией — у оракула проекция §92.3 снимает узел до подготовки слоёв."""
|
|
2069
|
+
self._meta[out["name"]] = {
|
|
2070
|
+
"name": out["name"], "id": rule["id"], "strength": strength,
|
|
2071
|
+
"head": out["head"]["pred"], "polarity": out["head"].get("polarity", "positive"),
|
|
2072
|
+
"reads": reads, "dating": dating,
|
|
2073
|
+
"computed": any(a["k"] in ("bin", "call") for a in out["head"]["args"])}
|
|
2074
|
+
|
|
2075
|
+
# --- поражаемый слой L1 (DECISION-0237) ------------------------------------
|
|
2076
|
+
|
|
2077
|
+
def _capabilities(self) -> set:
|
|
2078
|
+
"""Возможности, которых требует программа: сила правила §104,
|
|
2079
|
+
отрицательные головы, литералы и факты §99/§113, приоритеты §106,
|
|
2080
|
+
статусные тесты §65 кроме `established`. Пустое множество — L0-план."""
|
|
2081
|
+
caps = set(self.want)
|
|
2082
|
+
|
|
2083
|
+
def walk(f) -> None:
|
|
2084
|
+
if not isinstance(f, dict):
|
|
2085
|
+
return
|
|
2086
|
+
k = f.get("kind")
|
|
2087
|
+
if k == "literal" and f.get("polarity") == "negative":
|
|
2088
|
+
caps.add("negation")
|
|
2089
|
+
if k == "status":
|
|
2090
|
+
if f.get("status") in STATUS_TESTS and f.get("status") != "established":
|
|
2091
|
+
caps.add(f"status:{f['status']}")
|
|
2092
|
+
walk(f.get("formula"))
|
|
2093
|
+
for s in f.get("items") or f.get("args") or f.get("operands") or []:
|
|
2094
|
+
if isinstance(s, dict) and s.get("kind") in ("literal", "status", "and", "conjunction"):
|
|
2095
|
+
walk(s)
|
|
2096
|
+
|
|
2097
|
+
for n in self.ir["nodes"]:
|
|
2098
|
+
k = n.get("kind")
|
|
2099
|
+
if k == "rule":
|
|
2100
|
+
if n.get("strength") in ("defeasible", "defeater"):
|
|
2101
|
+
caps.add("defeasible")
|
|
2102
|
+
if (n.get("head") or {}).get("polarity") == "negative":
|
|
2103
|
+
caps.add("negation")
|
|
2104
|
+
walk(n.get("body"))
|
|
2105
|
+
elif k == "priority_rule":
|
|
2106
|
+
caps.add("priority")
|
|
2107
|
+
elif k == "closure_policy":
|
|
2108
|
+
caps.add("closure")
|
|
2109
|
+
elif k == "norm_template":
|
|
2110
|
+
caps.add("norm")
|
|
2111
|
+
elif k == "symbol_decl" and n.get("relationKind") == "judgment":
|
|
2112
|
+
# DECISION-0274: объявленное отношение суждения — запрос судье
|
|
2113
|
+
# возможен из любого носителя канала (E-0100), не только тела.
|
|
2114
|
+
caps.add("judgment")
|
|
2115
|
+
if k == "rule" and (n.get("head") or {}).get("kind") == "norm_template_ref":
|
|
2116
|
+
caps.add("norm")
|
|
2117
|
+
elif k == "assertion" and (n.get("literal") or {}).get("polarity") == "negative":
|
|
2118
|
+
caps.add("negation")
|
|
2119
|
+
if k in ("rule", "norm_template"):
|
|
2120
|
+
# DECISION-0245: календарные функции в термах правил и в
|
|
2121
|
+
# границах окон шаблонов позиций.
|
|
2122
|
+
stack = [n.get("body"), n.get("head"), n.get("payload")]
|
|
2123
|
+
while stack:
|
|
2124
|
+
x = stack.pop()
|
|
2125
|
+
if isinstance(x, list):
|
|
2126
|
+
stack.extend(x)
|
|
2127
|
+
elif isinstance(x, dict):
|
|
2128
|
+
if x.get("kind") == "call" and x.get("function") in CAL_FNS:
|
|
2129
|
+
caps |= self._cal_caps(x)
|
|
2130
|
+
stack.extend(v for v in x.values() if isinstance(v, (dict, list)))
|
|
2131
|
+
if k == "rule":
|
|
2132
|
+
# DECISION-0239: агрегат §59 в теле или голове; генератор его
|
|
2133
|
+
# comprehension несёт свои статусы и отрицания.
|
|
2134
|
+
stack = [n.get("body"), n.get("head")]
|
|
2135
|
+
while stack:
|
|
2136
|
+
x = stack.pop()
|
|
2137
|
+
if isinstance(x, list):
|
|
2138
|
+
stack.extend(x)
|
|
2139
|
+
elif isinstance(x, dict):
|
|
2140
|
+
if x.get("kind") == "aggregate":
|
|
2141
|
+
caps.add("aggregate")
|
|
2142
|
+
walk((x.get("input") or {}).get("generator"))
|
|
2143
|
+
stack.extend(v for v in x.values() if isinstance(v, (dict, list)))
|
|
2144
|
+
return caps
|
|
2145
|
+
|
|
2146
|
+
@staticmethod
|
|
2147
|
+
def _cal_caps(node: dict) -> set:
|
|
2148
|
+
"""Возможность вызова календарной функции; `add_legal_term` §86.1 —
|
|
2149
|
+
по единице литерала счётчика (иначе отказ плана при разборе терма)."""
|
|
2150
|
+
name, cap = CAL_FNS[node["function"]]
|
|
2151
|
+
if cap is not None:
|
|
2152
|
+
return {cap}
|
|
2153
|
+
args = node.get("args") or []
|
|
2154
|
+
if len(args) > 1 and isinstance(args[1], dict) and args[1].get("kind") != "value":
|
|
2155
|
+
# DECISION-0256 п. 6: единица связывателя известна лишь на деле — обе ветви.
|
|
2156
|
+
return {"calendar:period", "calendar:business"}
|
|
2157
|
+
unit = args[1].get("unit") if len(args) > 1 and isinstance(args[1], dict) else None
|
|
2158
|
+
return {"calendar:business"} if unit in DAY_UNITS else {"calendar:period"}
|
|
2159
|
+
|
|
2160
|
+
def parts_l1(self, body: dict, out: list) -> None:
|
|
2161
|
+
"""Тело правила L1 → конъюнкты `(статус, литерал)` и гарды `("guard", …)`."""
|
|
2162
|
+
k = body.get("kind")
|
|
2163
|
+
if k == "status":
|
|
2164
|
+
status = body.get("status")
|
|
2165
|
+
if status not in STATUS_TESTS:
|
|
2166
|
+
# conflicted/opposed оракул не исполняет (`flatten_body`):
|
|
2167
|
+
# правило у него пропускается с NON_EXECUTABLE_RULE.
|
|
2168
|
+
raise Refused(f"статусный тест вне среза генератора: {status}")
|
|
2169
|
+
if (body.get("formula") or {}).get("kind") != "literal":
|
|
2170
|
+
raise Refused("статусный тест над нелитералом вне среза генератора")
|
|
2171
|
+
out.append((status, body["formula"]))
|
|
2172
|
+
elif k == "literal":
|
|
2173
|
+
out.append(("established", body))
|
|
2174
|
+
elif k == "comparison":
|
|
2175
|
+
if body["op"] not in CMP:
|
|
2176
|
+
raise Refused(f"сравнение вне среза генератора: {body['op']}")
|
|
2177
|
+
out.append(("guard", body))
|
|
2178
|
+
elif k in ("and", "conjunction"):
|
|
2179
|
+
subs = body.get("items") or body.get("args") or body.get("operands")
|
|
2180
|
+
if not subs:
|
|
2181
|
+
raise Refused("конъюнкция без членов в CLIR")
|
|
2182
|
+
for s in subs:
|
|
2183
|
+
self.parts_l1(s, out)
|
|
2184
|
+
elif k == "boolean" and body.get("value") is True:
|
|
2185
|
+
return
|
|
2186
|
+
else:
|
|
2187
|
+
raise Refused(f"форма тела вне среза генератора: {k}")
|
|
2188
|
+
|
|
2189
|
+
def _rule_l1(self, i: int, rule: dict) -> dict:
|
|
2190
|
+
"""Правило L1: генераторы — положительные `established`/`monotone`
|
|
2191
|
+
литералы в порядке тела; прочие литералы и статусные тесты —
|
|
2192
|
+
проверки над уже связанными переменными (§190: отсутствие не
|
|
2193
|
+
связывает, §113); ground-тело без генератора допускается."""
|
|
2194
|
+
strength = rule.get("strength") or "strict"
|
|
2195
|
+
if strength not in ("strict", "defeasible", "defeater"):
|
|
2196
|
+
raise Refused(f"сила правила вне среза генератора: {strength}")
|
|
2197
|
+
conj: list = []
|
|
2198
|
+
self.parts_l1(rule["body"], conj)
|
|
2199
|
+
gens = [lit for st, lit in conj if st in GENERATING and lit.get("polarity") == "positive"]
|
|
2200
|
+
binds, joins, filters, plan_atoms = {}, [], [], []
|
|
2201
|
+
self._atoms = gens
|
|
2202
|
+
for ai, atom in enumerate(gens):
|
|
2203
|
+
params = self.types.get(atom["predicate"])
|
|
2204
|
+
if params is None:
|
|
2205
|
+
raise Refused(f"предикат тела не объявлен в пакете: {atom['predicate']}")
|
|
2206
|
+
if len(params) != len(atom["args"]):
|
|
2207
|
+
raise Refused("арность атома тела не совпадает с сигнатурой §189")
|
|
2208
|
+
status = next(st for st, lit in conj if lit is atom)
|
|
2209
|
+
plan_atoms.append({"pred": atom["predicate"], "accept": ACCEPT[(status, "positive")]})
|
|
2210
|
+
for j, arg in enumerate(atom["args"]):
|
|
2211
|
+
t = self.ts(params[j])
|
|
2212
|
+
if self._wild(plan_atoms[-1], arg, j, len(params)):
|
|
2213
|
+
continue
|
|
2214
|
+
if arg["kind"] != "var":
|
|
2215
|
+
filters.append({"row": ai, "col": j, "t": t, "term": self.body_const(arg, params[j])})
|
|
2216
|
+
continue
|
|
2217
|
+
cell = (ai, j, t)
|
|
2218
|
+
prev = binds.get(arg["var"])
|
|
2219
|
+
if prev is None:
|
|
2220
|
+
binds[arg["var"]] = cell
|
|
2221
|
+
else:
|
|
2222
|
+
joins.append([list(prev), list(cell)])
|
|
2223
|
+
checks, reads = [], []
|
|
2224
|
+
for st, lit in conj:
|
|
2225
|
+
if st == "guard":
|
|
2226
|
+
continue
|
|
2227
|
+
pol = lit.get("polarity")
|
|
2228
|
+
if pol not in ("positive", "negative"):
|
|
2229
|
+
raise Refused("полярность литерала тела вне среза генератора")
|
|
2230
|
+
reads.append((st, lit["predicate"], pol))
|
|
2231
|
+
if any(lit is g for g in gens):
|
|
2232
|
+
continue
|
|
2233
|
+
if self._has_wild(lit):
|
|
2234
|
+
# §98/E-0041: `_` законно только в положительном литерале тела;
|
|
2235
|
+
# под отрицанием и в статусном тесте компилятор его отвергает (LDC-E0207).
|
|
2236
|
+
raise Refused("подстановочный аргумент `_` §98 вне положительного литерала тела вне среза генератора")
|
|
2237
|
+
params = self.types.get(lit["predicate"])
|
|
2238
|
+
if params is None:
|
|
2239
|
+
raise Refused(f"предикат тела не объявлен в пакете: {lit['predicate']}")
|
|
2240
|
+
if len(params) != len(lit["args"]):
|
|
2241
|
+
raise Refused("арность атома тела не совпадает с сигнатурой §189")
|
|
2242
|
+
args = []
|
|
2243
|
+
for j, arg in enumerate(lit["args"]):
|
|
2244
|
+
if arg["kind"] == "var":
|
|
2245
|
+
if arg["var"] not in binds:
|
|
2246
|
+
raise Refused(f"переменная без позитивного генератора (§190/§113): {arg['var']}")
|
|
2247
|
+
args.append(self.term(arg, binds))
|
|
2248
|
+
else:
|
|
2249
|
+
args.append(self.body_const(arg, params[j]))
|
|
2250
|
+
checks.append({"pred": lit["predicate"], "accept": ACCEPT[(st, pol)], "args": args})
|
|
2251
|
+
head = rule["head"]
|
|
2252
|
+
if (head or {}).get("kind") != "literal":
|
|
2253
|
+
raise Refused(f"голова правила вне среза генератора: {(head or {}).get('kind')}")
|
|
2254
|
+
if head.get("polarity") not in ("positive", "negative"):
|
|
2255
|
+
raise Refused("полярность головы правила вне среза генератора")
|
|
2256
|
+
self.head_types(head, binds)
|
|
2257
|
+
window, dating = self._window(rule)
|
|
2258
|
+
guards = [g for st, g in conj if st == "guard"]
|
|
2259
|
+
for g in guards:
|
|
2260
|
+
self.guard_enums(g, binds)
|
|
2261
|
+
self.guard_instant(g, binds)
|
|
2262
|
+
out = {
|
|
2263
|
+
"name": f"{i}_{_slug(rule['id'])}", "id": rule["id"], "strength": strength, "window": window,
|
|
2264
|
+
"atoms": plan_atoms, "joins": joins, "filters": filters, "checks": checks,
|
|
2265
|
+
"guards": [{"op": g["op"], "l": self.term(g["left"], binds), "r": self.term(g["right"], binds)}
|
|
2266
|
+
for g in guards],
|
|
2267
|
+
"head": {"pred": head["predicate"], "polarity": head["polarity"],
|
|
2268
|
+
"args": [self.term(a, binds) for a in head["args"]]},
|
|
2269
|
+
# Идентичность применения (DECISION-0237 п. 8): StableId правила
|
|
2270
|
+
# плюс типизированная подстановка — значения всех переменных тела.
|
|
2271
|
+
"subst": [self.term({"kind": "var", "var": v}, binds) for v in binds],
|
|
2272
|
+
}
|
|
2273
|
+
self._remember(out, rule, strength, reads, dating)
|
|
2274
|
+
return out
|
|
2275
|
+
|
|
2276
|
+
# --- агрегаты §59 и замыкания §70 (DECISION-0239) ---------------------------
|
|
2277
|
+
|
|
2278
|
+
@staticmethod
|
|
2279
|
+
def _free(item, out: list | None = None) -> list:
|
|
2280
|
+
"""Свободные переменные терма или формулы в порядке встречи — порт
|
|
2281
|
+
`solver._term_vars`/`_formula_vars` для форм среза: у comprehension
|
|
2282
|
+
свои переменная и связки не свободны."""
|
|
2283
|
+
out = [] if out is None else out
|
|
2284
|
+
if isinstance(item, list):
|
|
2285
|
+
for x in item:
|
|
2286
|
+
Builder._free(x, out)
|
|
2287
|
+
return out
|
|
2288
|
+
if not isinstance(item, dict):
|
|
2289
|
+
return out
|
|
2290
|
+
k = item.get("kind")
|
|
2291
|
+
if k == "var":
|
|
2292
|
+
if item["var"] not in out:
|
|
2293
|
+
out.append(item["var"])
|
|
2294
|
+
elif k == "comprehension":
|
|
2295
|
+
inner = Builder._free([item.get("element"), item.get("generator")])
|
|
2296
|
+
bound = {(item.get("variable") or {}).get("id")}
|
|
2297
|
+
bound |= {b.get("id") for b in item.get("binders") or [] if b.get("id")}
|
|
2298
|
+
for v in inner:
|
|
2299
|
+
if v not in bound and v not in out:
|
|
2300
|
+
out.append(v)
|
|
2301
|
+
elif k == "aggregate":
|
|
2302
|
+
Builder._free(item.get("input"), out)
|
|
2303
|
+
elif k in ("literal",):
|
|
2304
|
+
Builder._free(item.get("args"), out)
|
|
2305
|
+
elif k == "status":
|
|
2306
|
+
Builder._free(item.get("formula"), out)
|
|
2307
|
+
elif k in ("and", "conjunction"):
|
|
2308
|
+
Builder._free(item.get("items") or item.get("args") or item.get("operands"), out)
|
|
2309
|
+
elif k in ("binary", "comparison"):
|
|
2310
|
+
Builder._free([item.get("left"), item.get("right")], out)
|
|
2311
|
+
elif k == "call":
|
|
2312
|
+
Builder._free(item.get("args"), out)
|
|
2313
|
+
elif k == "if":
|
|
2314
|
+
# §56: переменные условия и ОБЕИХ ветвей (`solver._term_vars`).
|
|
2315
|
+
Builder._free([item.get("condition"), item.get("then"), item.get("else")], out)
|
|
2316
|
+
elif k == "or":
|
|
2317
|
+
Builder._free(item.get("items"), out)
|
|
2318
|
+
elif k == "let":
|
|
2319
|
+
# DECISION-0273: переменная `let` связана в теле.
|
|
2320
|
+
Builder._free(item.get("value"), out)
|
|
2321
|
+
inner = Builder._free(item.get("body"))
|
|
2322
|
+
bound = str((item.get("variable") or {}).get("id"))
|
|
2323
|
+
for v in inner:
|
|
2324
|
+
if v != bound and v not in out:
|
|
2325
|
+
out.append(v)
|
|
2326
|
+
elif k == "match":
|
|
2327
|
+
Builder._free([item.get("subject")] + [a.get("value") for a in item.get("arms") or []], out)
|
|
2328
|
+
return out
|
|
2329
|
+
|
|
2330
|
+
def _body09(self, formula: dict, outer: dict) -> dict:
|
|
2331
|
+
"""Тело правила или генератор comprehension в порядке выбора
|
|
2332
|
+
`solver.solve`: сначала первый полностью связанный конъюнкт (гард или
|
|
2333
|
+
проверка статуса), иначе первый позитивный генератор. От порядка
|
|
2334
|
+
зависит, вычислится ли гард с ошибкой агрегата (DECISION-0239 п. 4)."""
|
|
2335
|
+
conj: list = []
|
|
2336
|
+
self.parts_l1(formula, conj)
|
|
2337
|
+
binds = dict(outer)
|
|
2338
|
+
atoms, joins, filters, checks, guards, steps, reads, gens = [], [], [], [], [], [], [], []
|
|
2339
|
+
saved = self._atoms
|
|
2340
|
+
self._atoms = gens
|
|
2341
|
+
remaining = list(conj)
|
|
2342
|
+
try:
|
|
2343
|
+
while remaining:
|
|
2344
|
+
idx = next((n for n, (_st, it) in enumerate(remaining)
|
|
2345
|
+
if set(self._free(it)) <= set(binds)), None)
|
|
2346
|
+
if idx is not None and self._has_wild(remaining[idx][1]):
|
|
2347
|
+
# §98 (DECISION-0268): полностью связанный литерал с `_` у
|
|
2348
|
+
# оракула — свидетель существования на том же месте; в
|
|
2349
|
+
# плане это генератор с дедупликацией, все столбцы которого
|
|
2350
|
+
# соединения либо фильтры, и он берётся на своём шаге.
|
|
2351
|
+
st, it = remaining[idx]
|
|
2352
|
+
if st not in GENERATING or it.get("polarity") != "positive":
|
|
2353
|
+
raise Refused("подстановочный аргумент `_` §98 вне положительного литерала тела вне среза генератора")
|
|
2354
|
+
remaining.pop(idx)
|
|
2355
|
+
remaining.insert(0, (st, it))
|
|
2356
|
+
idx = None
|
|
2357
|
+
if idx is not None:
|
|
2358
|
+
st, it = remaining.pop(idx)
|
|
2359
|
+
if st == "guard":
|
|
2360
|
+
self.guard_enums(it, binds)
|
|
2361
|
+
self.guard_kinds(it, binds)
|
|
2362
|
+
self.guard_instant(it, binds)
|
|
2363
|
+
steps.append({"guard": len(guards)})
|
|
2364
|
+
guards.append({"op": it["op"], "l": self.term(it["left"], binds),
|
|
2365
|
+
"r": self.term(it["right"], binds), "_node": it})
|
|
2366
|
+
continue
|
|
2367
|
+
pol = it.get("polarity")
|
|
2368
|
+
if pol not in ("positive", "negative"):
|
|
2369
|
+
raise Refused("полярность литерала тела вне среза генератора")
|
|
2370
|
+
reads.append((st, it["predicate"], pol))
|
|
2371
|
+
params = self.types.get(it["predicate"])
|
|
2372
|
+
if params is None:
|
|
2373
|
+
raise Refused(f"предикат тела не объявлен в пакете: {it['predicate']}")
|
|
2374
|
+
if len(params) != len(it["args"]):
|
|
2375
|
+
raise Refused("арность атома тела не совпадает с сигнатурой §189")
|
|
2376
|
+
args, err = [], False
|
|
2377
|
+
for j, a in enumerate(it["args"]):
|
|
2378
|
+
if a["kind"] == "var":
|
|
2379
|
+
args.append(self.term(a, binds))
|
|
2380
|
+
elif a["kind"] in ("binary", "call", "if", "let", "match"):
|
|
2381
|
+
# DECISION-0270: вычисляемый аргумент проверки статуса —
|
|
2382
|
+
# значение считается до проверки; ошибка значения — issue
|
|
2383
|
+
# правила по подстановкам, как у гарда (`err`).
|
|
2384
|
+
f = self._body_arg(0, j, self.ts(params[j]), a, params[j], binds)
|
|
2385
|
+
err = err or bool(f.get("err"))
|
|
2386
|
+
args.append(f["term"])
|
|
2387
|
+
else:
|
|
2388
|
+
args.append(self.body_const(a, params[j]))
|
|
2389
|
+
steps.append({"check": len(checks)})
|
|
2390
|
+
check = {"pred": it["predicate"], "accept": ACCEPT[(st, pol)], "args": args}
|
|
2391
|
+
if err:
|
|
2392
|
+
check["err"] = True
|
|
2393
|
+
checks.append(check)
|
|
2394
|
+
continue
|
|
2395
|
+
# `solver.solve` (pick): генератор с вычисляемым аргументом
|
|
2396
|
+
# берётся, лишь когда переменные аргумента связаны (E-0180).
|
|
2397
|
+
idx = next((n for n, (st, it) in enumerate(remaining)
|
|
2398
|
+
if st in GENERATING and it.get("polarity") == "positive"
|
|
2399
|
+
and all(a.get("kind") in ("var", "wildcard") or set(self._free(a)) <= set(binds)
|
|
2400
|
+
for a in it.get("args") or [])), None)
|
|
2401
|
+
if idx is None:
|
|
2402
|
+
missing = sorted(set(self._free(remaining[0][1])) - set(binds))
|
|
2403
|
+
raise Refused(f"переменная без позитивного генератора (§190/§113): {', '.join(missing)}")
|
|
2404
|
+
st, atom = remaining.pop(idx)
|
|
2405
|
+
reads.append((st, atom["predicate"], "positive"))
|
|
2406
|
+
params = self.types.get(atom["predicate"])
|
|
2407
|
+
if params is None:
|
|
2408
|
+
raise Refused(f"предикат тела не объявлен в пакете: {atom['predicate']}")
|
|
2409
|
+
if len(params) != len(atom["args"]):
|
|
2410
|
+
raise Refused("арность атома тела не совпадает с сигнатурой §189")
|
|
2411
|
+
ai = len(atoms)
|
|
2412
|
+
gens.append(atom)
|
|
2413
|
+
atoms.append({"pred": atom["predicate"], "accept": ACCEPT[(st, "positive")]})
|
|
2414
|
+
steps.append({"gen": ai})
|
|
2415
|
+
for j, arg in enumerate(atom["args"]):
|
|
2416
|
+
t = self.ts(params[j])
|
|
2417
|
+
if self._wild(atoms[-1], arg, j, len(params)):
|
|
2418
|
+
continue
|
|
2419
|
+
if arg["kind"] != "var":
|
|
2420
|
+
filters.append(self._body_arg(ai, j, t, arg, params[j], binds))
|
|
2421
|
+
continue
|
|
2422
|
+
prev = binds.get(arg["var"])
|
|
2423
|
+
if prev is None:
|
|
2424
|
+
binds[arg["var"]] = (ai, j, t)
|
|
2425
|
+
elif prev[0] == "p":
|
|
2426
|
+
filters.append({"row": ai, "col": j, "t": t,
|
|
2427
|
+
"term": {"k": "param", "i": prev[1], "t": prev[2]}})
|
|
2428
|
+
else:
|
|
2429
|
+
joins.append([list(prev), [ai, j, t]])
|
|
2430
|
+
finally:
|
|
2431
|
+
self._atoms = saved
|
|
2432
|
+
return {"atoms": atoms, "joins": joins, "filters": filters, "checks": checks, "guards": guards,
|
|
2433
|
+
"steps": steps, "reads": reads, "binds": binds, "gens": gens}
|
|
2434
|
+
|
|
2435
|
+
def _aggregate(self, node: dict, binds: dict) -> tuple:
|
|
2436
|
+
"""Агрегат §59 → подплан comprehension §52.1 с параметрами — внешними
|
|
2437
|
+
переменными. Возвращает (терм, URN вида результата, фоллибелен ли)."""
|
|
2438
|
+
key = id(node)
|
|
2439
|
+
if key in self._agg_cache:
|
|
2440
|
+
return self._agg_cache[key]
|
|
2441
|
+
op = node.get("op")
|
|
2442
|
+
if op not in AGG_OPS:
|
|
2443
|
+
raise Refused(f"агрегат вне среза генератора: {op}")
|
|
2444
|
+
if op == "only" and node.get("options"):
|
|
2445
|
+
# §51 (errata E-0256): у функционального lookup политики нет, и
|
|
2446
|
+
# оракул отвечает на `options` TYPE_ERROR — другой ответ, а не меньший.
|
|
2447
|
+
raise Refused("агрегат only с options вне среза генератора (§51, errata E-0256)")
|
|
2448
|
+
comp = node.get("input") or {}
|
|
2449
|
+
if comp.get("kind") != "comprehension" or "distinct" not in comp:
|
|
2450
|
+
raise Refused("вход агрегата не comprehension §52.1 вне среза генератора")
|
|
2451
|
+
free = self._free(comp)
|
|
2452
|
+
outer, ptypes = {}, []
|
|
2453
|
+
for n, v in enumerate(free):
|
|
2454
|
+
if v not in binds:
|
|
2455
|
+
raise Refused(f"несвязанная переменная в голове: {v}")
|
|
2456
|
+
b = binds[v]
|
|
2457
|
+
if b[0] == "p":
|
|
2458
|
+
urn, t = b[3], b[2]
|
|
2459
|
+
else:
|
|
2460
|
+
urn = self.types[self._atoms[b[0]]["predicate"]][b[1]]
|
|
2461
|
+
t = b[2]
|
|
2462
|
+
outer[v] = ("p", n, t, urn)
|
|
2463
|
+
ptypes.append(t)
|
|
2464
|
+
args = [self.term({"kind": "var", "var": v}, binds) for v in free]
|
|
2465
|
+
body = self._body09(comp["generator"], outer)
|
|
2466
|
+
if not body["atoms"]:
|
|
2467
|
+
raise Refused("comprehension без перебираемых атомов вне среза генератора")
|
|
2468
|
+
if any(f.get("err") for f in body["filters"]) or any(c.get("err") for c in body["checks"]):
|
|
2469
|
+
# DECISION-0270: у подплана агрегата нет кодов по подстановкам (G3 —
|
|
2470
|
+
# ошибка агрегата целиком); ошибка аргумента атома comprehension — отказ.
|
|
2471
|
+
raise Refused("ошибка значения в аргументе атома comprehension вне среза генератора (DECISION-0270)")
|
|
2472
|
+
element = comp.get("element") or {}
|
|
2473
|
+
saved = self._atoms
|
|
2474
|
+
self._atoms = body["gens"]
|
|
2475
|
+
try:
|
|
2476
|
+
if element.get("kind") not in ("var", "value", "entity_ref", "const_ref"):
|
|
2477
|
+
raise Refused("вычисляемый элемент comprehension вне среза генератора (§52.1)")
|
|
2478
|
+
el = self.term(element, body["binds"])
|
|
2479
|
+
kind = self.static_type(element, body["binds"])
|
|
2480
|
+
finally:
|
|
2481
|
+
self._atoms = saved
|
|
2482
|
+
if op != "count" and kind not in NUMERIC:
|
|
2483
|
+
raise Refused(f"агрегат {op} над нечисловым элементом вне среза генератора (§59)")
|
|
2484
|
+
# DECISION-0266: сумма и выбор над Natural у оракула — Integer (`Typed integer`).
|
|
2485
|
+
result = INT if op == "count" else _num(kind)
|
|
2486
|
+
empty = (node.get("options") or {}).get("empty")
|
|
2487
|
+
empty_term = None
|
|
2488
|
+
if empty is not None and op != "count":
|
|
2489
|
+
policy = _num(self.static_type(empty, {}))
|
|
2490
|
+
# errata E-0195: у оракула вид политики сверяется с видом элемента
|
|
2491
|
+
# ПУСТОЙ коллекции, а его несёт ОБЪЯВЛЕННЫЙ тип проекции §52.1
|
|
2492
|
+
# (`values.aggregate_element_type`), а не вид, выведенный планом из
|
|
2493
|
+
# терма элемента: в законной программе они совпадают, в рукописном
|
|
2494
|
+
# CLIR — нет, и там оракул даёт TYPE_ERROR. Сверяются ОБА носителя:
|
|
2495
|
+
# расхождение любого — отказ, потому что ответ у оракула другой,
|
|
2496
|
+
# а не меньший.
|
|
2497
|
+
declared = self._t(((comp.get("variable") or {}).get("type") or {}).get("name"))
|
|
2498
|
+
if policy != result or (declared in EMPTY_KIND_NAMES and policy != _num(declared)):
|
|
2499
|
+
raise Refused("вид empty-политики агрегата не совпадает с видом элемента коллекции "
|
|
2500
|
+
"(§59, errata E-0195)")
|
|
2501
|
+
empty_term = self.term(empty, {})
|
|
2502
|
+
name = f"agg_{len(self._aggs)}"
|
|
2503
|
+
for g in body["guards"]:
|
|
2504
|
+
g.pop("_node", None)
|
|
2505
|
+
self._aggs.append({"name": name, "op": op, "params": ptypes, "atoms": body["atoms"],
|
|
2506
|
+
"joins": body["joins"], "filters": body["filters"], "checks": body["checks"],
|
|
2507
|
+
"guards": body["guards"], "steps": body["steps"], "element": el,
|
|
2508
|
+
"distinct": bool(comp["distinct"]), "empty": empty_term})
|
|
2509
|
+
# E-0112: перечисление — ребро completion над всеми чтениями генератора.
|
|
2510
|
+
self._agg_reads += [("completion", pred, pol) for _st, pred, pol in body["reads"]]
|
|
2511
|
+
self._agg_inner += body["reads"]
|
|
2512
|
+
out = ({"k": "agg", "fn": name, "args": args, "t": TYPES[result]}, result,
|
|
2513
|
+
op != "count" and empty_term is None)
|
|
2514
|
+
if out[2]:
|
|
2515
|
+
# E-0105: ошибка агрегата правила-кандидата доезжает до статуса
|
|
2516
|
+
# вопроса `truth` — раннеру нужен обход от цели (`backchain`).
|
|
2517
|
+
self._agg_fallible = True
|
|
2518
|
+
self._agg_cache[key] = out
|
|
2519
|
+
return out
|
|
2520
|
+
|
|
2521
|
+
def _monotone_guard(self, g: dict):
|
|
2522
|
+
"""Агрегат монотонного гарда E-0116 либо None — порт
|
|
2523
|
+
`rules._monotone_guard_aggregate` с `_monotone_generator`."""
|
|
2524
|
+
op = g.get("op")
|
|
2525
|
+
for agg, threshold, effective in ((g.get("left") or {}, g.get("right") or {}, op),
|
|
2526
|
+
(g.get("right") or {}, g.get("left") or {}, MIRROR_OP.get(op))):
|
|
2527
|
+
if agg.get("kind") != "aggregate":
|
|
2528
|
+
continue
|
|
2529
|
+
if effective not in MONOTONE_GUARD_OPS.get(agg.get("op"), ()):
|
|
2530
|
+
return None
|
|
2531
|
+
comp = agg.get("input") or {}
|
|
2532
|
+
if comp.get("kind") != "comprehension" or '"comprehension"' in json.dumps(threshold):
|
|
2533
|
+
return None
|
|
2534
|
+
conj: list = []
|
|
2535
|
+
try:
|
|
2536
|
+
self.parts_l1(comp.get("generator") or {}, conj)
|
|
2537
|
+
except Refused:
|
|
2538
|
+
return None
|
|
2539
|
+
for st, lit in conj:
|
|
2540
|
+
if st == "guard" or st in ("monotone", "supported"):
|
|
2541
|
+
continue
|
|
2542
|
+
if st != "established" or lit.get("polarity") == "negative" \
|
|
2543
|
+
or lit.get("predicate") in self._derived:
|
|
2544
|
+
return None
|
|
2545
|
+
return agg
|
|
2546
|
+
return None
|
|
2547
|
+
|
|
2548
|
+
def _rule_v09(self, i: int, rule: dict) -> dict:
|
|
2549
|
+
"""Правило плана 0.9: шаги тела в порядке `solve`, агрегаты подпланами."""
|
|
2550
|
+
strength = rule.get("strength") or "strict"
|
|
2551
|
+
if strength not in ("strict", "defeasible", "defeater"):
|
|
2552
|
+
raise Refused(f"сила правила вне среза генератора: {strength}")
|
|
2553
|
+
body, fallible, addends, first_agg = self._rule_body09(rule)
|
|
2554
|
+
binds = body["binds"]
|
|
2555
|
+
head = rule["head"]
|
|
2556
|
+
if (head or {}).get("kind") != "literal":
|
|
2557
|
+
raise Refused(f"голова правила вне среза генератора: {(head or {}).get('kind')}")
|
|
2558
|
+
if head.get("polarity") not in ("positive", "negative"):
|
|
2559
|
+
raise Refused("полярность головы правила вне среза генератора")
|
|
2560
|
+
saved = self._atoms
|
|
2561
|
+
self._atoms = body["gens"]
|
|
2562
|
+
try:
|
|
2563
|
+
self.head_types(head, binds)
|
|
2564
|
+
kind_checks = self.kind_checks(head, binds) if self.v12 or self.v13 or self.v14 or self.v17 or self.v18 else []
|
|
2565
|
+
head_args = [self.term(a, binds) for a in head["args"]]
|
|
2566
|
+
subst = [self.term({"kind": "var", "var": v}, binds) for v in binds]
|
|
2567
|
+
head_value = (self.v12 or self.v13 or self.v14 or self.v17 or self.v18) and self._fallible_value(head["args"], binds)
|
|
2568
|
+
finally:
|
|
2569
|
+
self._atoms = saved
|
|
2570
|
+
# W5 (errata E-0265): агрегат `sum`/`min`/`max`/`only` без политики `empty`
|
|
2571
|
+
# в голове снимает только свою подстановку — issue одна на (правило, код),
|
|
2572
|
+
# остальные подстановки применяются. До errata ответ оракула зависел от
|
|
2573
|
+
# порядка подстановок, и здесь стоял отказ плана (DECISION-0239 п. 4).
|
|
2574
|
+
head_agg = any(self._agg_cache.get(id(x), (None, None, False))[2] for x in self._walk(head))
|
|
2575
|
+
head_fallible = self._fallible_calendar(head) or head_value or head_agg
|
|
2576
|
+
window, dating = self._window(rule)
|
|
2577
|
+
out = {
|
|
2578
|
+
"name": f"{i}_{_slug(rule['id'])}", "id": rule["id"], "strength": strength, "window": window,
|
|
2579
|
+
"atoms": body["atoms"], "joins": body["joins"], "filters": body["filters"], "checks": body["checks"],
|
|
2580
|
+
"guards": body["guards"], "steps": body["steps"],
|
|
2581
|
+
"head": {"pred": head["predicate"], "polarity": head["polarity"], "args": head_args},
|
|
2582
|
+
"subst": subst, "fallible": fallible, "addends": addends,
|
|
2583
|
+
}
|
|
2584
|
+
if head_fallible:
|
|
2585
|
+
# §175 (errata E-0265): ошибка головы снимает только свою подстановку;
|
|
2586
|
+
# issue — по каждому коду однажды, прошедшие подстановки применяются.
|
|
2587
|
+
out["headFallible"] = True
|
|
2588
|
+
if head_agg:
|
|
2589
|
+
# Ошибка агрегата головы (EMPTY_AGGREGATE, CARDINALITY_CONFLICT) ловится
|
|
2590
|
+
# на подстановке, а не всем правилом, как ошибка гарда (G3).
|
|
2591
|
+
out["headAgg"] = True
|
|
2592
|
+
if kind_checks:
|
|
2593
|
+
# DECISION-0252 п. 5: вид значения в слоте Decimal/Rational — E-0077.
|
|
2594
|
+
out["kindChecks"] = kind_checks
|
|
2595
|
+
self._remember(out, rule, strength, body["reads"] + self._agg_reads, dating)
|
|
2596
|
+
self._meta[out["name"]]["fallible"] = fallible or head_fallible
|
|
2597
|
+
if self._fallible_calendar(head) or any(g.get("cal") for g in body["guards"]):
|
|
2598
|
+
self._meta[out["name"]]["calendar"] = True
|
|
2599
|
+
if head_value or any(g.get("err") for g in body["guards"]) \
|
|
2600
|
+
or any(f.get("err") for f in body["filters"]) or any(c.get("err") for c in body["checks"]):
|
|
2601
|
+
self._meta[out["name"]]["value"] = True
|
|
2602
|
+
# Статусы чтений внутри генераторов: для замыкания важно, читает ли
|
|
2603
|
+
# перечисление негатив (§70/E-0113), а не сам факт перечисления.
|
|
2604
|
+
self._meta[out["name"]]["inner"] = list(self._agg_inner)
|
|
2605
|
+
self._meta[out["name"]]["aggregates"] = [a["name"] for a in self._aggs[first_agg:]]
|
|
2606
|
+
self._meta[out["name"]]["closure_reads"] = self._closure_reads(rule)
|
|
2607
|
+
return out
|
|
2608
|
+
|
|
2609
|
+
def _rule_body09(self, rule: dict) -> tuple:
|
|
2610
|
+
"""Тело правила плана 0.9: шаги `solve`, гарды E-0116 и фоллибельность —
|
|
2611
|
+
общее у правил с литеральной головой и у норм-правил (DECISION-0242)."""
|
|
2612
|
+
self._agg_reads = []
|
|
2613
|
+
self._agg_inner = []
|
|
2614
|
+
first_agg = len(self._aggs)
|
|
2615
|
+
body = self._body09(rule["body"], {})
|
|
2616
|
+
addends, fallible = [], False
|
|
2617
|
+
# DECISION-0270: ошибка значения вычисляемого аргумента атома (до перебора
|
|
2618
|
+
# таблицы) либо проверки статуса — та же механика кодов, что у гарда.
|
|
2619
|
+
if any(f.get("err") for f in body["filters"]) or any(c.get("err") for c in body["checks"]):
|
|
2620
|
+
fallible = True
|
|
2621
|
+
for g in body["guards"]:
|
|
2622
|
+
node = g.pop("_node")
|
|
2623
|
+
if self._fallible_calendar(node):
|
|
2624
|
+
# DECISION-0245 п. 6: ошибка календаря в гарде — issue и снятие
|
|
2625
|
+
# правила на проход; коды ловятся по подстановкам.
|
|
2626
|
+
g["cal"] = True
|
|
2627
|
+
fallible = True
|
|
2628
|
+
elif (self.v12 or self.v13 or self.v14 or self.v17 or self.v18) and self._fallible_value(node, body["binds"], body["gens"]):
|
|
2629
|
+
# DECISION-0252 п. 4: ошибка значения §58 в гарде — та же механика.
|
|
2630
|
+
g["err"] = True
|
|
2631
|
+
fallible = True
|
|
2632
|
+
mono = self._monotone_guard(node)
|
|
2633
|
+
if mono is not None:
|
|
2634
|
+
conj: list = []
|
|
2635
|
+
self.parts_l1(mono["input"]["generator"], conj)
|
|
2636
|
+
derived = sorted({lit["predicate"] for st, lit in conj
|
|
2637
|
+
if st != "guard" and lit["predicate"] in self._derived})
|
|
2638
|
+
if derived:
|
|
2639
|
+
raise Refused("монотонный агрегатный гард (E-0116) над выводимым предикатом "
|
|
2640
|
+
f"вне среза генератора (DECISION-0239 G2): {', '.join(derived)}")
|
|
2641
|
+
if mono.get("op") == "sum":
|
|
2642
|
+
# E-0116: знак слагаемых проверяется на исполнении гарда.
|
|
2643
|
+
addends.append(self._agg_cache[id(mono)][0])
|
|
2644
|
+
for side in (node.get("left") or {}, node.get("right") or {}):
|
|
2645
|
+
stack = [side]
|
|
2646
|
+
while stack:
|
|
2647
|
+
x = stack.pop()
|
|
2648
|
+
if isinstance(x, dict):
|
|
2649
|
+
if x.get("kind") == "aggregate" and id(x) in self._agg_cache and self._agg_cache[id(x)][2]:
|
|
2650
|
+
fallible = True
|
|
2651
|
+
stack.extend(v for v in x.values() if isinstance(v, (dict, list)))
|
|
2652
|
+
elif isinstance(x, list):
|
|
2653
|
+
stack.extend(x)
|
|
2654
|
+
return body, fallible, addends, first_agg
|
|
2655
|
+
|
|
2656
|
+
def _closure_reads(self, rule: dict) -> list:
|
|
2657
|
+
"""Чтения правила для ожидания замыкания §70 (E-0174) —
|
|
2658
|
+
`(статус, предикат, полярность, перечисление)`: порт разбора
|
|
2659
|
+
`rules.closure_waits`. Литералы тела; генераторы монотонного гарда
|
|
2660
|
+
E-0116 — не перечисление; генераторы прочих агрегатов тела и головы —
|
|
2661
|
+
перечисление (`_split_generator_literals`)."""
|
|
2662
|
+
conj: list = []
|
|
2663
|
+
self.parts_l1(rule["body"], conj)
|
|
2664
|
+
reads: list = []
|
|
2665
|
+
for st, item in conj:
|
|
2666
|
+
if st == "guard":
|
|
2667
|
+
mono = self._monotone_guard(item)
|
|
2668
|
+
if mono is not None:
|
|
2669
|
+
reads += [(s, lit, False) for s, lit in self._generator_literals(mono["input"])]
|
|
2670
|
+
continue
|
|
2671
|
+
else:
|
|
2672
|
+
reads.append((st, item, False))
|
|
2673
|
+
reads += [(s, lit, True) for s, lit in self._generator_literals(item)]
|
|
2674
|
+
reads += [(s, lit, True) for s, lit in self._generator_literals(rule["head"])]
|
|
2675
|
+
return [(s, lit["predicate"], lit.get("polarity", "positive"), enum) for s, lit, enum in reads]
|
|
2676
|
+
|
|
2677
|
+
def _generator_literals(self, formula) -> list:
|
|
2678
|
+
"""Порт `rules._aggregate_generator_literals`: литералы генераторов
|
|
2679
|
+
comprehension со статусом; внутрь генератора обход не спускается."""
|
|
2680
|
+
out: list = []
|
|
2681
|
+
stack = [formula]
|
|
2682
|
+
while stack:
|
|
2683
|
+
x = stack.pop(0)
|
|
2684
|
+
if isinstance(x, list):
|
|
2685
|
+
stack[:0] = x
|
|
2686
|
+
elif isinstance(x, dict):
|
|
2687
|
+
if x.get("kind") == "comprehension":
|
|
2688
|
+
conj: list = []
|
|
2689
|
+
self.parts_l1(x.get("generator") or {}, conj)
|
|
2690
|
+
out += [(st, lit) for st, lit in conj if st != "guard" and lit.get("predicate")]
|
|
2691
|
+
continue
|
|
2692
|
+
stack[:0] = [v for v in x.values() if isinstance(v, (dict, list))]
|
|
2693
|
+
return out
|
|
2694
|
+
|
|
2695
|
+
@staticmethod
|
|
2696
|
+
def _walk(x):
|
|
2697
|
+
stack = [x]
|
|
2698
|
+
while stack:
|
|
2699
|
+
y = stack.pop()
|
|
2700
|
+
if isinstance(y, dict):
|
|
2701
|
+
yield y
|
|
2702
|
+
stack.extend(v for v in y.values() if isinstance(v, (dict, list)))
|
|
2703
|
+
elif isinstance(y, list):
|
|
2704
|
+
stack.extend(y)
|
|
2705
|
+
|
|
2706
|
+
def _closure(self, n: dict) -> None:
|
|
2707
|
+
"""Замыкание §70 (E-0087, E-0104): негатив члену домена без
|
|
2708
|
+
strict/asserted positive-опоры. Неисполнимое у оракула замыкание
|
|
2709
|
+
(`NON_EXECUTABLE_CLOSURE`) — отказ среза."""
|
|
2710
|
+
for f in sorted(set(n) - CLOSURE_FIELDS):
|
|
2711
|
+
raise Refused(f"поле замыкания вне среза генератора: {f}")
|
|
2712
|
+
if not n.get("deriveExplicitNegative"):
|
|
2713
|
+
return # оракул такое замыкание пропускает молча
|
|
2714
|
+
pred, domain = n.get("predicate"), n.get("domain") or {}
|
|
2715
|
+
width = len(self.types.get(pred) or [])
|
|
2716
|
+
if not width or domain.get("kind") != "const_ref" or domain.get("id") not in self.types \
|
|
2717
|
+
or self.types[domain["id"]] != self.types[pred]:
|
|
2718
|
+
raise Refused(f"замыкание §70 вне среза генератора (NON_EXECUTABLE_CLOSURE): {n.get('id')}")
|
|
2719
|
+
eff = n.get("effective")
|
|
2720
|
+
window = None
|
|
2721
|
+
if eff:
|
|
2722
|
+
if eff.get("axis") != "legal_time":
|
|
2723
|
+
raise Refused(f"ось окна effective вне среза генератора: {eff.get('axis')}")
|
|
2724
|
+
start, end = eff.get("start"), eff.get("end")
|
|
2725
|
+
start = None if start in (None, "-infinity") else start
|
|
2726
|
+
end = None if end in (None, "infinity") else end
|
|
2727
|
+
# E-0087: окно замыкания проверяется `within_effective` с флагами
|
|
2728
|
+
# включения. Граница-день §2.15 переводится в полуинтервал
|
|
2729
|
+
# `[start, end)` сдвигом на следующий день; иная гранулярность — отказ.
|
|
2730
|
+
for bound, inclusive, shift in ((start, eff.get("startInclusive", True), False),
|
|
2731
|
+
(end, eff.get("endInclusive", False), True)):
|
|
2732
|
+
if bound is not None and not _is_date(bound) and inclusive is shift:
|
|
2733
|
+
raise Refused("полузакрытость окна effective вне среза генератора")
|
|
2734
|
+
if start is not None and eff.get("startInclusive") is False:
|
|
2735
|
+
start = _next_day(start)
|
|
2736
|
+
if end is not None and eff.get("endInclusive") is True:
|
|
2737
|
+
end = _next_day(end)
|
|
2738
|
+
window = {"start": start, "end": end}
|
|
2739
|
+
self._closures.append({"name": f"closure_{len(self._closures)}_{_slug(n['id'])}", "id": n["id"],
|
|
2740
|
+
"pred": pred, "domain": domain["id"], "window": window})
|
|
2741
|
+
|
|
2742
|
+
# --- нормативные позиции L2 (DECISION-0242) ---------------------------------
|
|
2743
|
+
|
|
2744
|
+
def _std_tables(self) -> list:
|
|
2745
|
+
"""Таблицы литералов §140, которые публикует жизненный цикл позиций.
|
|
2746
|
+
Позиция — сущность вида `urn:position:…`; шаблон во втором аргументе
|
|
2747
|
+
`instance_of` — `const_ref` (идентичность). Объявленный пакетом
|
|
2748
|
+
std-предикат остаётся объявлением пакета."""
|
|
2749
|
+
out = []
|
|
2750
|
+
self.entities |= {POSITION_TYPE, "urn:law:backend#Template"}
|
|
2751
|
+
for name, arity in STD_POSITION.items():
|
|
2752
|
+
pred = STD + name
|
|
2753
|
+
if pred in self.types:
|
|
2754
|
+
continue
|
|
2755
|
+
params = [POSITION_TYPE] + (["urn:law:backend#Template" if name == "instance_of" else POSITION_TYPE]
|
|
2756
|
+
if arity == 2 else [])
|
|
2757
|
+
if name in ("bearer_of", "beneficiary_of"):
|
|
2758
|
+
params[1] = POSITION_TYPE
|
|
2759
|
+
self.types[pred] = params
|
|
2760
|
+
out.append({"pred": pred, "name": self.table_name(pred), "arity": arity, "params": params,
|
|
2761
|
+
"keys": [], "kinds": ["text"] * arity})
|
|
2762
|
+
return out
|
|
2763
|
+
|
|
2764
|
+
def _deref(self, value):
|
|
2765
|
+
"""Разыменование констант §54 в payload шаблона — оракул делает его над
|
|
2766
|
+
программой до исполнения; члены перечислений остаются `const_ref`."""
|
|
2767
|
+
if isinstance(value, list):
|
|
2768
|
+
return [self._deref(v) for v in value]
|
|
2769
|
+
if not isinstance(value, dict):
|
|
2770
|
+
return value
|
|
2771
|
+
if value.get("kind") == "const_ref" and not self.is_member(value.get("id", "")):
|
|
2772
|
+
c = self.consts.get(value.get("id"))
|
|
2773
|
+
if c is not None and c.get("value") is not None:
|
|
2774
|
+
return c["value"]
|
|
2775
|
+
return {k: self._deref(v) for k, v in value.items()}
|
|
2776
|
+
|
|
2777
|
+
def _norm_v10(self, i: int, rule: dict) -> dict:
|
|
2778
|
+
"""Норм-правило §135 с шаблоном §122: подстановки тела создают позиции,
|
|
2779
|
+
статус §134 — лестница оракула (`norms._summary_at`), эффект power —
|
|
2780
|
+
`effects.materialize_effects`. Всё, что оракул исполняет иначе или
|
|
2781
|
+
не исполняет, — названный отказ."""
|
|
2782
|
+
strength = rule.get("strength") or "strict"
|
|
2783
|
+
if strength == "defeater":
|
|
2784
|
+
raise Refused("дефитер норм-правила вне среза генератора (DECISION-0242 K6)")
|
|
2785
|
+
if strength not in ("strict", "defeasible"):
|
|
2786
|
+
raise Refused(f"сила правила вне среза генератора: {strength}")
|
|
2787
|
+
tid = (rule.get("head") or {}).get("id")
|
|
2788
|
+
tpl = self.by_id.get(tid)
|
|
2789
|
+
if tpl is None or tpl.get("kind") != "norm_template":
|
|
2790
|
+
raise Refused(f"шаблон нормы не разрешается (§135): {tid}")
|
|
2791
|
+
# §92.3: проекция редакции снимает у оракула шаблон на дату права, и
|
|
2792
|
+
# ссылающееся правило позиций не создаёт (NON_EXECUTABLE_RULE — issue
|
|
2793
|
+
# вне сверки среза). То же даёт окно правила, пересечённое с окнами
|
|
2794
|
+
# шаблона; несколько окон — названный отказ, как у правил.
|
|
2795
|
+
rule_window, _dating = self._window(rule)
|
|
2796
|
+
tdating = self._dating(tpl)
|
|
2797
|
+
if tdating is not None and tdating != [(None, None)]:
|
|
2798
|
+
base = (rule_window or {}).get("start"), (rule_window or {}).get("end")
|
|
2799
|
+
cut = [w for w in (_intersect(base, d) for d in tdating) if w is not None]
|
|
2800
|
+
if not cut:
|
|
2801
|
+
raise _Excluded(rule["id"])
|
|
2802
|
+
if len(cut) > 1:
|
|
2803
|
+
raise Refused("прерывистая применимость редакции §92.3 вне среза генератора")
|
|
2804
|
+
rule_window = {"start": cut[0][0], "end": cut[0][1]}
|
|
2805
|
+
modality = tpl.get("modality")
|
|
2806
|
+
payload = self._deref(tpl.get("payload") or {})
|
|
2807
|
+
if modality not in MODALITIES or payload.get("kind") != modality:
|
|
2808
|
+
raise Refused(f"модальность шаблона вне среза генератора: {modality}")
|
|
2809
|
+
for f in sorted(set(payload) - PAYLOAD_FIELDS[modality]):
|
|
2810
|
+
raise Refused(f"поле payload шаблона вне среза генератора (DECISION-0242 п. 4): {f}")
|
|
2811
|
+
body, fallible, addends, first_agg = self._rule_body09(rule)
|
|
2812
|
+
if any(g.get("cal") for g in body["guards"]):
|
|
2813
|
+
raise Refused("календарная функция с ошибкой значения в теле норм-правила вне среза генератора "
|
|
2814
|
+
"(DECISION-0245)")
|
|
2815
|
+
if any(g.get("err") for g in body["guards"]):
|
|
2816
|
+
raise Refused("терм с ошибкой значения §58 в теле норм-правила вне среза генератора (DECISION-0252)")
|
|
2817
|
+
binds = body["binds"]
|
|
2818
|
+
free = set(_payload_vars(payload))
|
|
2819
|
+
if not free <= set(binds):
|
|
2820
|
+
raise Refused(f"переменная payload не связана телом норм-правила (DECISION-0242 п. 3): "
|
|
2821
|
+
f"{', '.join(sorted(free - set(binds)))}")
|
|
2822
|
+
saved = self._atoms
|
|
2823
|
+
self._atoms = body["gens"]
|
|
2824
|
+
reads_extra: list = []
|
|
2825
|
+
try:
|
|
2826
|
+
def literal(node, what):
|
|
2827
|
+
if node is None:
|
|
2828
|
+
return None
|
|
2829
|
+
if node.get("kind") == "status" and what in ("условие цели", "validWhen"):
|
|
2830
|
+
if what == "условие цели" and node.get("status") != "established":
|
|
2831
|
+
raise Refused(f"{what} §124 в форме status:{node.get('status')} вне среза генератора")
|
|
2832
|
+
node = node.get("formula") or {}
|
|
2833
|
+
if node.get("kind") == "call" and what == "exercise":
|
|
2834
|
+
node = {"kind": "literal", "polarity": "positive", "predicate": node.get("function"),
|
|
2835
|
+
"args": node.get("args") or []}
|
|
2836
|
+
if node.get("kind") != "literal" or node.get("polarity") not in ("positive", "negative"):
|
|
2837
|
+
raise Refused(f"{what} вне среза генератора (DECISION-0242 п. 4): {node.get('kind')}")
|
|
2838
|
+
params = self.types.get(node["predicate"])
|
|
2839
|
+
if params is None or len(params) != len(node.get("args") or []):
|
|
2840
|
+
raise Refused(f"{what}: предикат не объявлен либо арность не совпадает (§189)")
|
|
2841
|
+
reads_extra.append((what, node["predicate"]))
|
|
2842
|
+
return {"pred": node["predicate"], "polarity": node["polarity"],
|
|
2843
|
+
"args": [self.term(a, binds) for a in node["args"]]}
|
|
2844
|
+
|
|
2845
|
+
def actor(term):
|
|
2846
|
+
if term is None:
|
|
2847
|
+
return None
|
|
2848
|
+
k = term.get("kind")
|
|
2849
|
+
if k in ("entity_ref", "const_ref"):
|
|
2850
|
+
return {"k": "lit", "t": "text" if k == "entity_ref" else "enum", "v": term["id"]}
|
|
2851
|
+
if k == "var":
|
|
2852
|
+
t = self.term(term, binds)
|
|
2853
|
+
return t if t["t"] in ("text", "enum") else {"k": "fallback"}
|
|
2854
|
+
return {"k": "fallback"}
|
|
2855
|
+
|
|
2856
|
+
def bound(term):
|
|
2857
|
+
if term is None:
|
|
2858
|
+
return None
|
|
2859
|
+
if term.get("kind") == "value":
|
|
2860
|
+
tname = (term.get("type") or {}).get("name")
|
|
2861
|
+
if tname == "urn:law:std#Text" and term.get("value") in ("infinity", "+infinity", "-infinity"):
|
|
2862
|
+
return {"k": "inf", "v": term["value"]}
|
|
2863
|
+
if tname == "urn:law:std#Date":
|
|
2864
|
+
return {"k": "lit", "t": "date", "v": str(term["value"])}
|
|
2865
|
+
if term.get("kind") == "var":
|
|
2866
|
+
t = self.term(term, binds)
|
|
2867
|
+
if t["t"] == "date":
|
|
2868
|
+
return t
|
|
2869
|
+
if term.get("kind") == "call" and term.get("function") in CAL_FNS:
|
|
2870
|
+
# DECISION-0245 п. 8: граница — календарный терм над
|
|
2871
|
+
# переменными тела; ошибка — issue и граница «нет». Не
|
|
2872
|
+
# Date (`days_between`) — у оракула TYPE_ERROR на каждой
|
|
2873
|
+
# позиции (`norms._window_bounds`).
|
|
2874
|
+
compiled = self.term(term, binds)
|
|
2875
|
+
if self.static_type(term, binds) != "urn:law:std#Date":
|
|
2876
|
+
return {"k": "calbound", "error": "TYPE_ERROR"}
|
|
2877
|
+
return {"k": "calbound", "term": compiled}
|
|
2878
|
+
raise Refused("граница окна позиции вне среза генератора (DECISION-0242 п. 2): "
|
|
2879
|
+
f"{term.get('kind')}")
|
|
2880
|
+
|
|
2881
|
+
def window(w):
|
|
2882
|
+
if w is None:
|
|
2883
|
+
return None
|
|
2884
|
+
if w.get("kind") != "interval":
|
|
2885
|
+
raise Refused(f"окно позиции вне среза генератора: {w.get('kind')}")
|
|
2886
|
+
return {"start": bound(w.get("start")), "end": bound(w.get("end")),
|
|
2887
|
+
"startInclusive": bool(w.get("startInclusive", True)),
|
|
2888
|
+
"endInclusive": bool(w.get("endInclusive", True))}
|
|
2889
|
+
|
|
2890
|
+
goal = payload.get("goal")
|
|
2891
|
+
goal_kind, condition, goal_window, completeness = None, None, None, []
|
|
2892
|
+
if modality == "duty":
|
|
2893
|
+
if not isinstance(goal, dict) or goal.get("kind") not in ("achievement", "maintenance", "forbearance"):
|
|
2894
|
+
raise Refused("вид цели §124 вне среза генератора: "
|
|
2895
|
+
f"{(goal or {}).get('kind') if isinstance(goal, dict) else goal}")
|
|
2896
|
+
for f in sorted(set(goal) - GOAL_FIELDS):
|
|
2897
|
+
raise Refused(f"поле цели §124 вне среза генератора: {f}")
|
|
2898
|
+
goal_kind = goal["kind"]
|
|
2899
|
+
goal_window = window(goal.get("window"))
|
|
2900
|
+
if goal_kind == "forbearance":
|
|
2901
|
+
action = goal.get("action") or {}
|
|
2902
|
+
if action.get("kind") != "call":
|
|
2903
|
+
raise Refused("действие цели §124.3 вне среза генератора: требуется вызов отношения")
|
|
2904
|
+
condition = literal({"kind": "literal", "polarity": "negative",
|
|
2905
|
+
"predicate": action.get("function"), "args": action.get("args") or []},
|
|
2906
|
+
"условие цели")
|
|
2907
|
+
else:
|
|
2908
|
+
if goal.get("condition") is None:
|
|
2909
|
+
raise Refused("условие цели §124 отсутствует (NON_EXECUTABLE_GOAL)")
|
|
2910
|
+
condition = literal(goal.get("condition"), "условие цели")
|
|
2911
|
+
if goal_kind in ("maintenance", "forbearance"):
|
|
2912
|
+
for c in sorted((n for n in self.ir["nodes"] if n.get("kind") == "closure_policy"
|
|
2913
|
+
and n.get("deriveExplicitNegative") and n.get("predicate") == condition["pred"]),
|
|
2914
|
+
key=lambda n: n["id"]):
|
|
2915
|
+
dom = (c.get("domain") or {})
|
|
2916
|
+
if dom.get("kind") == "const_ref" and dom.get("id") in self.types:
|
|
2917
|
+
completeness.append({"domain": dom["id"],
|
|
2918
|
+
"completeAsOf": str(c.get("completeAsOf", ""))[:10]})
|
|
2919
|
+
elif goal is not None:
|
|
2920
|
+
raise Refused(f"цель у модальности {modality} вне среза генератора")
|
|
2921
|
+
holder_term = payload.get("bearer") if payload.get("bearer") is not None else payload.get("holder")
|
|
2922
|
+
out = {
|
|
2923
|
+
"name": f"{i}_{_slug(rule['id'])}", "id": rule["id"], "strength": strength,
|
|
2924
|
+
"window": rule_window, "template": tid, "modality": modality,
|
|
2925
|
+
"atoms": body["atoms"], "joins": body["joins"], "filters": body["filters"],
|
|
2926
|
+
"checks": body["checks"], "guards": body["guards"], "steps": body["steps"],
|
|
2927
|
+
"fallible": fallible, "addends": addends,
|
|
2928
|
+
"subst": [self.term({"kind": "var", "var": v}, binds) for v in binds],
|
|
2929
|
+
"holder": actor(holder_term),
|
|
2930
|
+
"beneficiary": actor(payload.get("beneficiary")),
|
|
2931
|
+
"goalKind": goal_kind, "condition": condition, "goalWindow": goal_window,
|
|
2932
|
+
"payloadWindow": window(payload.get("window")), "completeness": completeness,
|
|
2933
|
+
"payload": payload,
|
|
2934
|
+
"payloadVars": [{"var": v, "term": self.term({"kind": "var", "var": v}, binds),
|
|
2935
|
+
"pred": body["gens"][binds[v][0]]["predicate"], "col": binds[v][1]}
|
|
2936
|
+
for v in sorted(free)],
|
|
2937
|
+
}
|
|
2938
|
+
if modality == "power":
|
|
2939
|
+
effect = payload.get("effect") or {}
|
|
2940
|
+
if effect.get("kind") != "create" or not isinstance(effect.get("after"), dict):
|
|
2941
|
+
raise Refused("эффект полномочия §127 вне среза генератора: только create")
|
|
2942
|
+
out.update({"exercise": literal(payload.get("exercise"), "exercise"),
|
|
2943
|
+
"validWhen": literal(payload.get("validWhen"), "validWhen"),
|
|
2944
|
+
"effect": literal(effect["after"], "эффект"),
|
|
2945
|
+
"powerHolder": actor(payload.get("holder"))})
|
|
2946
|
+
if out["exercise"] is None:
|
|
2947
|
+
raise Refused("exercise полномочия §127 отсутствует")
|
|
2948
|
+
if modality == "immunity":
|
|
2949
|
+
protected = (payload.get("protectedEffect") or {}).get("after")
|
|
2950
|
+
out.update({"protected": literal(protected, "защищённый эффект") if protected else None,
|
|
2951
|
+
"against": actor(payload.get("against"))})
|
|
2952
|
+
finally:
|
|
2953
|
+
self._atoms = saved
|
|
2954
|
+
self._norm_meta[out["name"]] = {
|
|
2955
|
+
"id": rule["id"], "name": out["name"], "reads": body["reads"] + self._agg_reads,
|
|
2956
|
+
"extra": reads_extra, "modality": modality, "goal": goal_kind, "template": tid,
|
|
2957
|
+
"action": json.dumps((goal or {}).get("action") if modality == "duty" else payload.get("action"),
|
|
2958
|
+
sort_keys=True),
|
|
2959
|
+
"condition": (condition or {}).get("pred"),
|
|
2960
|
+
"effect": (out.get("effect") or {}).get("pred"), "aggregates": [a["name"] for a in self._aggs[first_agg:]]}
|
|
2961
|
+
return out
|
|
2962
|
+
|
|
2963
|
+
def _structure10(self, metas: list, strict: list, drules: list, reach: dict) -> dict:
|
|
2964
|
+
"""Отказы DECISION-0242: обратная связь L2 (п. 1), конфликт позиций
|
|
2965
|
+
§105.1 (K5); приоритеты норм-правил для power/immunity (п. 5)."""
|
|
2966
|
+
if "urn:law:std#incompatible" in json.dumps(self.ir["nodes"]):
|
|
2967
|
+
raise Refused("конфликт позиций §105.1 (incompatible) вне среза генератора (DECISION-0242 K5)")
|
|
2968
|
+
norms = list(self._norm_meta.values())
|
|
2969
|
+
libs = {n["action"] for n in norms if n["modality"] == "liberty" and n["action"] != "null"}
|
|
2970
|
+
forb = {n["action"] for n in norms if n["goal"] == "forbearance" and n["action"] != "null"}
|
|
2971
|
+
if libs & forb:
|
|
2972
|
+
raise Refused("конфликт позиций §105.1 (forbearance/liberty) вне среза генератора (DECISION-0242 K5)")
|
|
2973
|
+
keyed = sorted({n["condition"] for n in norms if n["goal"] in ("achievement", "maintenance")
|
|
2974
|
+
and n["condition"] and (self.by_id.get(n["condition"]) or {}).get("keys")})
|
|
2975
|
+
if keyed:
|
|
2976
|
+
raise Refused("конфликт позиций §105.1 по ключу §45.1 вне среза генератора "
|
|
2977
|
+
f"(DECISION-0242 K5, POSSIBLE_NORM_CONFLICT): {', '.join(keyed)}")
|
|
2978
|
+
# п. 1: выходы L2 и их strict-следствия.
|
|
2979
|
+
out = {STD + s for s in STD_POSITION} | {n["effect"] for n in norms if n["effect"]}
|
|
2980
|
+
changed = True
|
|
2981
|
+
while changed:
|
|
2982
|
+
changed = False
|
|
2983
|
+
for m in strict:
|
|
2984
|
+
if m["head"] not in out and any(pred in out for _st, pred, _p in m["reads"]):
|
|
2985
|
+
out.add(m["head"])
|
|
2986
|
+
changed = True
|
|
2987
|
+
for n in norms:
|
|
2988
|
+
hit = sorted({pred for _st, pred, _p in n["reads"] if pred in out}
|
|
2989
|
+
# Эффект и защищённый эффект — образцы, а не чтения хранилища.
|
|
2990
|
+
| {pred for _w, pred in n["extra"] if pred in out and _w not in ("эффект", "защищённый эффект")})
|
|
2991
|
+
if hit:
|
|
2992
|
+
raise Refused(f"{NORM_REFUSAL}: норм-правило {n['id']} читает {', '.join(hit)}")
|
|
2993
|
+
for m in drules:
|
|
2994
|
+
if m["head"] in out or any(pred in out for _st, pred, _p in m["reads"]):
|
|
2995
|
+
raise Refused(f"{NORM_REFUSAL}: поражаемое правило {m['id']} над выходом L2")
|
|
2996
|
+
for c in self._closures:
|
|
2997
|
+
if c["pred"] in out or c["domain"] in out:
|
|
2998
|
+
raise Refused(f"{NORM_REFUSAL}: замыкание {c['id']} над выходом L2")
|
|
2999
|
+
negative_heads = {m["head"] for m in strict if m["polarity"] == "negative"}
|
|
3000
|
+
for m in strict:
|
|
3001
|
+
for st, pred, pol in m["reads"] + m.get("inner", []):
|
|
3002
|
+
if pred not in out:
|
|
3003
|
+
continue
|
|
3004
|
+
if st not in ("established", "monotone", "supported") or pol == "negative" \
|
|
3005
|
+
or (st == "established" and pred in negative_heads):
|
|
3006
|
+
raise Refused(f"{NORM_REFUSAL}: strict-правило {m['id']} читает {st} {pred} "
|
|
3007
|
+
"до исполнения L2")
|
|
3008
|
+
names = {n["id"]: n["name"] for n in norms}
|
|
3009
|
+
return {"normPriorities": sorted([names[a], names[b]] for a in reach for b in reach[a]
|
|
3010
|
+
if a in names and b in names)}
|
|
3011
|
+
|
|
3012
|
+
def _priority(self, n: dict) -> None:
|
|
3013
|
+
"""Приоритет §106 в срезе: безусловное (`condition == true`)
|
|
3014
|
+
недатированное ребро между правилами. Условный §117/§118,
|
|
3015
|
+
датированный редакцией и неизвестные поля — названный отказ."""
|
|
3016
|
+
if n.get("condition") not in (None, TRUE_FORMULA) or n.get("priorityKind") not in (None, "direct"):
|
|
3017
|
+
raise Refused(f"условный приоритет (§117/§118) вне среза генератора: {n.get('id')}")
|
|
3018
|
+
for f in sorted(set(n) - PRIORITY_FIELDS):
|
|
3019
|
+
raise Refused(f"поле приоритета вне среза генератора: {f}")
|
|
3020
|
+
higher, lower = n.get("higher") or {}, n.get("lower") or {}
|
|
3021
|
+
if higher.get("kind") != "const_ref" or lower.get("kind") != "const_ref":
|
|
3022
|
+
raise Refused(f"сторона приоритета не правило (§106) вне среза генератора: {n.get('id')}")
|
|
3023
|
+
dating = self._dating(n)
|
|
3024
|
+
window = None
|
|
3025
|
+
if dating is not None and dating != [(None, None)]:
|
|
3026
|
+
if not dating:
|
|
3027
|
+
return # §92.3: узел снят проекцией на всей оси
|
|
3028
|
+
if len(dating) != 1:
|
|
3029
|
+
# DECISION-0269 п. 3: прерывистое окно исходного ребра — отказ первого среза.
|
|
3030
|
+
raise Refused(f"прерывистое окно приоритета (§92.3) вне среза генератора: {n.get('id')}")
|
|
3031
|
+
self._dated_prio_seen = True
|
|
3032
|
+
window = dating[0]
|
|
3033
|
+
self._prio.append((higher["id"], lower["id"], n["id"], window))
|
|
3034
|
+
|
|
3035
|
+
def _fact_l1(self, a: dict) -> dict:
|
|
3036
|
+
lit = a.get("literal") or {}
|
|
3037
|
+
pol = lit.get("polarity")
|
|
3038
|
+
if pol not in ("positive", "negative"):
|
|
3039
|
+
raise Refused("полярность собственного факта вне среза генератора")
|
|
3040
|
+
dating = self._dating(a)
|
|
3041
|
+
if dating is not None and dating != [(None, None)]:
|
|
3042
|
+
if not dating:
|
|
3043
|
+
raise _Excluded(a["id"])
|
|
3044
|
+
if len(dating) > 1:
|
|
3045
|
+
raise Refused("прерывистая применимость редакции §92.3 вне среза генератора")
|
|
3046
|
+
self.dated_facts.append({
|
|
3047
|
+
"name": f"dated_{len(self.dated_facts)}_{_slug(a['id'])}", "id": a["id"], "strength": "strict",
|
|
3048
|
+
"window": {"start": dating[0][0], "end": dating[0][1]},
|
|
3049
|
+
"atoms": [{"pred": ALWAYS, "accept": ["TRUE_ONLY"]}], "joins": [], "filters": [], "checks": [],
|
|
3050
|
+
"guards": [], "subst": [],
|
|
3051
|
+
"head": {"pred": lit["predicate"], "polarity": pol, "args": [self.term(x, {}) for x in lit["args"]]}})
|
|
3052
|
+
raise _Excluded(None)
|
|
3053
|
+
return {"pred": lit["predicate"], "polarity": pol, "args": [self.term(x, {}) for x in lit["args"]]}
|
|
3054
|
+
|
|
3055
|
+
def _structure(self, rules: list) -> dict:
|
|
3056
|
+
"""Структурный анализ программы: циклы §110, завершимость, а у L1 —
|
|
3057
|
+
барьер полноты §110 п. 5, приоритеты §106, ранги обоих слоёв, поздние
|
|
3058
|
+
производители §111 и расписание фаз. Всё за срезом — названный отказ."""
|
|
3059
|
+
metas = sorted((self._meta[r["name"]] for r in rules if r["name"] in self._meta),
|
|
3060
|
+
key=lambda m: m["id"])
|
|
3061
|
+
# §110 п. 1–2: цикл допускает только монотонные рёбра. Единственное
|
|
3062
|
+
# поражаемое правило, читающее свою голову, — тоже цикл: до errata
|
|
3063
|
+
# E-0173 оракул исполнял его одним проходом, теперь оба оценщика
|
|
3064
|
+
# отвечают NON_EXECUTABLE_UNSTRATIFIED, и срез отказывает с ними.
|
|
3065
|
+
edges = []
|
|
3066
|
+
for m in metas:
|
|
3067
|
+
for st, pred, _pol in m["reads"]:
|
|
3068
|
+
if st == "completion":
|
|
3069
|
+
kind = "completion"
|
|
3070
|
+
elif st in ("not_known", "unknown"):
|
|
3071
|
+
kind = "default"
|
|
3072
|
+
elif m["strength"] != "strict":
|
|
3073
|
+
kind = "defeasible"
|
|
3074
|
+
elif st in ("monotone", "supported"):
|
|
3075
|
+
kind = "monotone"
|
|
3076
|
+
else:
|
|
3077
|
+
kind = "status"
|
|
3078
|
+
edges.append((m["head"], pred, kind))
|
|
3079
|
+
# E-0113: замыкание — вершина `producers(P ∪ domain) → closure(P) →
|
|
3080
|
+
# readers(¬P)`; все его рёбра немонотонны.
|
|
3081
|
+
closed: dict = {}
|
|
3082
|
+
for c in self._closures:
|
|
3083
|
+
closed.setdefault(c["pred"], []).append(c)
|
|
3084
|
+
edges += [(c["id"], c["pred"], "closure"), (c["id"], c["domain"], "closure")]
|
|
3085
|
+
for m in metas:
|
|
3086
|
+
for st, pred, pol in m["reads"] + m.get("inner", []):
|
|
3087
|
+
if pred in closed and st != "completion" and (pol == "negative" or st in ("refuted", "not_known", "unknown")):
|
|
3088
|
+
edges += [(m["head"], c["id"], "closure") for c in closed[pred]]
|
|
3089
|
+
# §47.3 (DECISION-0274): чтение отношения суждения — обычное чтение
|
|
3090
|
+
# таблицы; запрос судье строит обход раннера по `backchain`.
|
|
3091
|
+
comp = _sccs(sorted({e[0] for e in edges} | {e[1] for e in edges}), [(a, b) for a, b, _ in edges])
|
|
3092
|
+
for a, b, kind in edges:
|
|
3093
|
+
if kind != "monotone" and comp[a] == comp[b]:
|
|
3094
|
+
raise Refused(f"{UNSTRATIFIED}: цикл через {a} → {b} с ребром [{kind}]")
|
|
3095
|
+
# DECISION-0237 п. 11: рекурсия, порождающая значения арифметикой, не
|
|
3096
|
+
# обязана завершиться, а частичный результат не выдаётся как ответ.
|
|
3097
|
+
for m in metas:
|
|
3098
|
+
if m["computed"] and any(comp.get(m["head"]) == comp.get(pred) for _st, pred, _p in m["reads"]):
|
|
3099
|
+
raise Refused(f"рекурсия с порождением значений арифметикой (DECISION-0237 п. 11) "
|
|
3100
|
+
f"вне среза генератора: {m['head']}")
|
|
3101
|
+
if not self.l1:
|
|
3102
|
+
return {}
|
|
3103
|
+
strict =[m for m in metas if m["strength"] == "strict"]
|
|
3104
|
+
drules = [m for m in metas if m["strength"] != "strict"]
|
|
3105
|
+
# §110 п. 5, §107.3: дефитер действует только при обычном кандидате
|
|
3106
|
+
# той же головы и полярности; у оракула набор узлов проецирован на
|
|
3107
|
+
# дату права §92.3, поэтому окна производителей обязаны покрыть окно
|
|
3108
|
+
# дефитера — иначе в непокрытую дату оракул гасит документ.
|
|
3109
|
+
# Считается по узлам CLIR, а не по скомпилированным правилам: обзор
|
|
3110
|
+
# продолжает после отказа правила, и производитель, отвергнутый своей
|
|
3111
|
+
# причиной, иначе делал бы дефитер «сиротой».
|
|
3112
|
+
heads = []
|
|
3113
|
+
for n in self.ir["nodes"]:
|
|
3114
|
+
head = n.get("head") or {}
|
|
3115
|
+
if n.get("kind") != "rule" or n.get("strength") not in ("defeasible", "defeater") \
|
|
3116
|
+
or head.get("kind") != "literal":
|
|
3117
|
+
continue
|
|
3118
|
+
dating = self._dating(n)
|
|
3119
|
+
if dating == []:
|
|
3120
|
+
continue # §92.3: узел снят проекцией на всей оси
|
|
3121
|
+
heads.append({"id": n["id"], "strength": n["strength"], "head": head.get("predicate"),
|
|
3122
|
+
"polarity": head.get("polarity"),
|
|
3123
|
+
"dating": None if dating == [(None, None)] else dating})
|
|
3124
|
+
for d in heads:
|
|
3125
|
+
if d["strength"] != "defeater":
|
|
3126
|
+
continue
|
|
3127
|
+
prods = [m for m in heads if m["strength"] == "defeasible"
|
|
3128
|
+
and (m["head"], m["polarity"]) == (d["head"], d["polarity"])]
|
|
3129
|
+
outer = None if any(m["dating"] is None for m in prods) else [w for m in prods for w in m["dating"]]
|
|
3130
|
+
if not prods or not _covered(d["dating"], outer):
|
|
3131
|
+
raise Refused("дефитер без обычного кандидата той же полярности "
|
|
3132
|
+
f"(§110 п. 5, NON_EXECUTABLE_STRATUM_BARRIER): {d['id']}")
|
|
3133
|
+
# §106: транзитивное замыкание безусловных рёбер; цикл — отказ среза
|
|
3134
|
+
# (у оракула PRIORITY_CONFLICT группы, DECISION-0237 п. 13).
|
|
3135
|
+
# DECISION-0269: у оракула проекция §92.3 сначала снимает неприменимые
|
|
3136
|
+
# узлы приоритета, потом строится замыкание (`editions.project_editions`,
|
|
3137
|
+
# `defeasible._priority_closure`), поэтому окно ребра участвует в
|
|
3138
|
+
# замыкании: по отрезкам оси между границами окон — пересечение вдоль
|
|
3139
|
+
# пути, объединение между путями; цикл на отрезке — отказ.
|
|
3140
|
+
pairs = self._priority_windows()
|
|
3141
|
+
reach: dict = {}
|
|
3142
|
+
for (a, b), ws in pairs.items():
|
|
3143
|
+
if ws == [(None, None)]:
|
|
3144
|
+
reach.setdefault(a, set()).add(b)
|
|
3145
|
+
dnames = {m["id"]: m["name"] for m in drules}
|
|
3146
|
+
priorities = sorted([dnames[a], dnames[b]] for a in reach for b in reach[a]
|
|
3147
|
+
if a in dnames and b in dnames)
|
|
3148
|
+
dated_priorities = sorted([dnames[a], dnames[b], [list(w) for w in ws]]
|
|
3149
|
+
for (a, b), ws in pairs.items()
|
|
3150
|
+
if ws != [(None, None)] and a in dnames and b in dnames)
|
|
3151
|
+
srank = self._strict_ranks(strict)
|
|
3152
|
+
drank = self._defeasible_ranks(drules)
|
|
3153
|
+
late = self._late(strict, drules, drank)
|
|
3154
|
+
levels_09, wait_level = (self._structure09(metas, strict, drules, srank, late, closed, edges)
|
|
3155
|
+
if self.v09 else ([], {}))
|
|
3156
|
+
strict_strata = [[m["name"] for m in strict if srank[m["id"]] == k]
|
|
3157
|
+
for k in sorted(set(srank.values()))]
|
|
3158
|
+
if self.dated_facts:
|
|
3159
|
+
# Датированный факт у оракула — вход, а не правило: его голова
|
|
3160
|
+
# полна до любого вывода, поэтому страта печатается первой.
|
|
3161
|
+
strict_strata.insert(0, [r["name"] for r in self.dated_facts])
|
|
3162
|
+
levels = sorted(set(drank.values()))
|
|
3163
|
+
dstrata = [[m["name"] for m in drules if drank[m["id"]] == k] for k in levels]
|
|
3164
|
+
|
|
3165
|
+
def strict_phase(done: int) -> dict:
|
|
3166
|
+
# E-0174: `waiting` — правила, чьи замыкания ещё не пройдены после
|
|
3167
|
+
# `done` уровней; strict-проход их не исполняет. Поле печатается
|
|
3168
|
+
# только непустым: у прочих планов расписание байтово прежнее.
|
|
3169
|
+
waiting = sorted(name for name, k in wait_level.items() if k > done)
|
|
3170
|
+
return {"phase": "strict", "waiting": waiting} if waiting else {"phase": "strict"}
|
|
3171
|
+
|
|
3172
|
+
schedule = [strict_phase(0)]
|
|
3173
|
+
if levels_09:
|
|
3174
|
+
for done, names in enumerate(levels_09, start=1):
|
|
3175
|
+
schedule += [{"phase": "closure", "policies": names}, strict_phase(done)]
|
|
3176
|
+
else:
|
|
3177
|
+
schedule.append({"phase": "closure", "policies": []})
|
|
3178
|
+
for k, names in zip(levels, dstrata):
|
|
3179
|
+
schedule += [{"phase": "candidates", "stratum": k, "rules": names},
|
|
3180
|
+
{"phase": "defeat", "stratum": k},
|
|
3181
|
+
{"phase": "surviving", "stratum": k},
|
|
3182
|
+
{"phase": "strict"},
|
|
3183
|
+
{"phase": "status", "stratum": k}]
|
|
3184
|
+
result = {"strata": {"strict": [s for s in strict_strata if s], "defeasible": dstrata},
|
|
3185
|
+
"priorities": priorities, "schedule": schedule}
|
|
3186
|
+
if self.v16:
|
|
3187
|
+
result["datedPriorities"] = dated_priorities
|
|
3188
|
+
if self.v10:
|
|
3189
|
+
# DECISION-0242 K3: один проход L2 после поражаемого слоя —
|
|
3190
|
+
# позиции §135, эффекты полномочий §127, strict-замыкание.
|
|
3191
|
+
result.update(self._structure10(metas, strict, drules, reach))
|
|
3192
|
+
if self.v16:
|
|
3193
|
+
names = {n["id"]: n["name"] for n in self._norm_meta.values()}
|
|
3194
|
+
result["datedNormPriorities"] = sorted(
|
|
3195
|
+
[names[a], names[b], [list(w) for w in ws]] for (a, b), ws in pairs.items()
|
|
3196
|
+
if ws != [(None, None)] and a in names and b in names)
|
|
3197
|
+
schedule += [{"phase": "norms"}, {"phase": "effects"}, {"phase": "strict"}]
|
|
3198
|
+
return result
|
|
3199
|
+
|
|
3200
|
+
def _priority_windows(self) -> dict:
|
|
3201
|
+
"""Транзитивное замыкание приоритетов §106 с окнами §92.3
|
|
3202
|
+
(DECISION-0269): пара «выше, ниже» → объединение окон `[start, end)`,
|
|
3203
|
+
на которых она достижима; `[(None, None)]` — вся ось (недатированная
|
|
3204
|
+
пара, как прежде). Отрезки оси — между границами окон датированных
|
|
3205
|
+
рёбер; на каждом отрезке замыкание Уоршелла по активным рёбрам, цикл
|
|
3206
|
+
на отрезке — названный отказ (у оракула PRIORITY_CONFLICT на дату)."""
|
|
3207
|
+
edges = [(hi, lo, w) for hi, lo, _pid, w in self._prio]
|
|
3208
|
+
bounds = sorted({b for _hi, _lo, w in edges if w is not None for b in w if b is not None})
|
|
3209
|
+
segments = []
|
|
3210
|
+
prev = None
|
|
3211
|
+
for b in bounds:
|
|
3212
|
+
segments.append((prev, b))
|
|
3213
|
+
prev = b
|
|
3214
|
+
segments.append((prev, None))
|
|
3215
|
+
pairs: dict = {}
|
|
3216
|
+
for seg in segments:
|
|
3217
|
+
active = [(hi, lo) for hi, lo, w in edges if w is None or _contains(w, seg)]
|
|
3218
|
+
reach: dict = {}
|
|
3219
|
+
for hi, lo in active:
|
|
3220
|
+
reach.setdefault(hi, set()).add(lo)
|
|
3221
|
+
changed = True
|
|
3222
|
+
while changed:
|
|
3223
|
+
changed = False
|
|
3224
|
+
for a in sorted(reach):
|
|
3225
|
+
for b in sorted(reach[a]):
|
|
3226
|
+
extra = reach.get(b, set()) - reach[a]
|
|
3227
|
+
if extra:
|
|
3228
|
+
reach[a] |= extra
|
|
3229
|
+
changed = True
|
|
3230
|
+
cyc = sorted(a for a in reach if a in reach[a])
|
|
3231
|
+
if cyc:
|
|
3232
|
+
if seg == (None, None):
|
|
3233
|
+
raise Refused(f"цикл приоритетов (§106, PRIORITY_CONFLICT) вне среза генератора: {', '.join(cyc)}")
|
|
3234
|
+
raise Refused("цикл приоритетов (§106, PRIORITY_CONFLICT) на отрезке "
|
|
3235
|
+
f"[{seg[0] or '-∞'}, {seg[1] or '∞'}) вне среза генератора (DECISION-0269): {', '.join(cyc)}")
|
|
3236
|
+
for a in reach:
|
|
3237
|
+
for b in reach[a]:
|
|
3238
|
+
pairs.setdefault((a, b), []).append(seg)
|
|
3239
|
+
return {pair: _union(segs) for pair, segs in pairs.items()}
|
|
3240
|
+
|
|
3241
|
+
def _structure09(self, metas, strict, drules, srank, late, closed, edges) -> list:
|
|
3242
|
+
"""Структура плана 0.9: отказы DECISION-0239 и уровни замыканий E-0113."""
|
|
3243
|
+
dp = {m["head"] for m in drules}
|
|
3244
|
+
# п. 8 (G5): замыкание над предикатом или доменом, чей производитель
|
|
3245
|
+
# поражаем либо поздний через строгую цепочку, — отказ: оракул
|
|
3246
|
+
# материализует замыкания до поражаемого слоя, проза §70 — после
|
|
3247
|
+
# производителей.
|
|
3248
|
+
for c in self._closures:
|
|
3249
|
+
for pred in (c["pred"], c["domain"]):
|
|
3250
|
+
if pred in dp or late.get(pred):
|
|
3251
|
+
raise Refused(f"замыкание §70 над поражаемым производителем вне среза генератора "
|
|
3252
|
+
f"(DECISION-0239 G5): {c['id']}")
|
|
3253
|
+
# п. 7 (errata E-0174): strict-читатель, чей тест опровергается
|
|
3254
|
+
# негативом замыкания, ждёт замыкание — подъём над вершиной, а не отказ.
|
|
3255
|
+
waits = _closure_waits(strict, closed)
|
|
3256
|
+
# п. 4: фоллибельный гард strict-правила обязан читать только
|
|
3257
|
+
# завершённое ниже его страты — иначе момент ошибки зависел бы от хода
|
|
3258
|
+
# фикспойнта.
|
|
3259
|
+
producers: dict = {}
|
|
3260
|
+
for m in strict:
|
|
3261
|
+
producers.setdefault(m["head"], []).append(m)
|
|
3262
|
+
for m in strict:
|
|
3263
|
+
if not m.get("fallible"):
|
|
3264
|
+
continue
|
|
3265
|
+
cal = m.get("calendar")
|
|
3266
|
+
value = m.get("value")
|
|
3267
|
+
for _st, pred, _pol in m["reads"]:
|
|
3268
|
+
if pred in dp or late.get(pred) or pred in closed or any(
|
|
3269
|
+
srank[p["id"]] >= srank[m["id"]] for p in producers.get(pred, [])
|
|
3270
|
+
if p["id"] != m["id"] or cal or value):
|
|
3271
|
+
if cal:
|
|
3272
|
+
raise Refused("календарная функция с ошибкой значения над незавершённым входом вне "
|
|
3273
|
+
f"среза генератора (DECISION-0245 п. 7): {m['id']} читает {pred}")
|
|
3274
|
+
if value:
|
|
3275
|
+
# DECISION-0252 п. 4: момент ошибки значения зависел бы от хода
|
|
3276
|
+
# фикспойнта — правило ошибку не ловит, и она становится
|
|
3277
|
+
# названным отказом раннера, а не issue и не отказом плана.
|
|
3278
|
+
self._value_refuse.add(m["name"])
|
|
3279
|
+
break
|
|
3280
|
+
raise Refused("агрегат с ошибкой значения над незавершённым входом вне среза генератора "
|
|
3281
|
+
f"(DECISION-0239 п. 4): {m['id']} читает {pred}")
|
|
3282
|
+
if not self._closures:
|
|
3283
|
+
return [], {}
|
|
3284
|
+
# E-0113: уровень замыкания — на единицу выше уровней замыканий,
|
|
3285
|
+
# достижимых из его вершины по зависимостям.
|
|
3286
|
+
adj: dict = {}
|
|
3287
|
+
for a, b, _k in edges:
|
|
3288
|
+
adj.setdefault(a, set()).add(b)
|
|
3289
|
+
ids = [c["id"] for c in self._closures]
|
|
3290
|
+
deps = {}
|
|
3291
|
+
for cid in ids:
|
|
3292
|
+
seen, stack = {cid}, [cid]
|
|
3293
|
+
while stack:
|
|
3294
|
+
for nxt in adj.get(stack.pop(), ()):
|
|
3295
|
+
if nxt not in seen:
|
|
3296
|
+
seen.add(nxt)
|
|
3297
|
+
stack.append(nxt)
|
|
3298
|
+
deps[cid] = [o for o in ids if o != cid and o in seen]
|
|
3299
|
+
level: dict = {}
|
|
3300
|
+
for _ in range(len(ids) + 1):
|
|
3301
|
+
changed = False
|
|
3302
|
+
for cid in ids:
|
|
3303
|
+
need = 1 + max((level.get(o, 1) for o in deps[cid]), default=0)
|
|
3304
|
+
if need > level.get(cid, 0):
|
|
3305
|
+
level[cid] = need
|
|
3306
|
+
changed = True
|
|
3307
|
+
if not changed:
|
|
3308
|
+
break
|
|
3309
|
+
else:
|
|
3310
|
+
raise Refused(f"{UNSTRATIFIED}: цикл между замыканиями")
|
|
3311
|
+
groups = [[c["name"] for c in sorted(self._closures, key=lambda c: c["id"]) if level[c["id"]] == k]
|
|
3312
|
+
for k in sorted(set(level.values()))]
|
|
3313
|
+
# Ждущее правило исполняется в strict-проходе после уровня своего
|
|
3314
|
+
# старшего замыкания: замыкания материализуются по уровням.
|
|
3315
|
+
ranks = sorted(set(level.values()))
|
|
3316
|
+
wait_level = {m["name"]: 1 + ranks.index(max(level[c] for c in waits[m["id"]]))
|
|
3317
|
+
for m in strict if waits.get(m["id"])}
|
|
3318
|
+
return groups, wait_level
|
|
3319
|
+
|
|
3320
|
+
def _strict_ranks(self, strict: list) -> dict:
|
|
3321
|
+
"""Порт `strict.strict_ranks`: ранги §110 строгого слоя (DECISION-0088)."""
|
|
3322
|
+
rank = {m["id"]: 0 for m in strict}
|
|
3323
|
+
heads = {m["head"] for m in strict}
|
|
3324
|
+
needs = any(m["polarity"] == "negative" or any(st in ("not_known", "unknown", "refuted")
|
|
3325
|
+
or (st == "completion" and p in heads)
|
|
3326
|
+
for st, p, _q in m["reads"]) for m in strict)
|
|
3327
|
+
if not needs:
|
|
3328
|
+
return rank
|
|
3329
|
+
producers: dict = {}
|
|
3330
|
+
for m in strict:
|
|
3331
|
+
producers.setdefault(m["head"], []).append(m["id"])
|
|
3332
|
+
for _ in range(len(strict) + 1):
|
|
3333
|
+
changed = False
|
|
3334
|
+
for m in strict:
|
|
3335
|
+
need = 0
|
|
3336
|
+
for st, pred, _pol in m["reads"]:
|
|
3337
|
+
bump = 0 if st in ("supported", "monotone") else 1
|
|
3338
|
+
for pid in producers.get(pred, []):
|
|
3339
|
+
if pid != m["id"]:
|
|
3340
|
+
need = max(need, rank[pid] + bump)
|
|
3341
|
+
if need > rank[m["id"]]:
|
|
3342
|
+
rank[m["id"]] = need
|
|
3343
|
+
changed = True
|
|
3344
|
+
head_max: dict = {}
|
|
3345
|
+
for m in strict:
|
|
3346
|
+
head_max[m["head"]] = max(head_max.get(m["head"], 0), rank[m["id"]])
|
|
3347
|
+
for m in strict:
|
|
3348
|
+
if head_max[m["head"]] > rank[m["id"]]:
|
|
3349
|
+
rank[m["id"]] = head_max[m["head"]]
|
|
3350
|
+
changed = True
|
|
3351
|
+
if not changed:
|
|
3352
|
+
return rank
|
|
3353
|
+
raise Refused(f"{UNSTRATIFIED}: ранги строгого слоя не сходятся")
|
|
3354
|
+
|
|
3355
|
+
def _defeasible_ranks(self, drules: list) -> dict:
|
|
3356
|
+
"""Порт `defeasible.stratify`: ранг §110 поражаемого правила — глубина
|
|
3357
|
+
через поражаемые головы; правила одной головы обеих полярностей и
|
|
3358
|
+
дефитеры — в одной страте (E-0078/E-0080)."""
|
|
3359
|
+
heads: dict = {}
|
|
3360
|
+
for m in drules:
|
|
3361
|
+
heads.setdefault(m["head"], []).append(m)
|
|
3362
|
+
rank = {m["id"]: 0 for m in drules}
|
|
3363
|
+
for _ in range(len(drules) + 1):
|
|
3364
|
+
changed = False
|
|
3365
|
+
for m in drules:
|
|
3366
|
+
deps = [(pred, 1) for _st, pred, _pol in m["reads"]]
|
|
3367
|
+
if m["strength"] == "defeater":
|
|
3368
|
+
deps.append((m["head"], 1))
|
|
3369
|
+
need = 0
|
|
3370
|
+
for pred, bump in deps:
|
|
3371
|
+
same = m["strength"] == "defeater" and pred == m["head"]
|
|
3372
|
+
for p in heads.get(pred, []):
|
|
3373
|
+
if p["id"] != m["id"]:
|
|
3374
|
+
need = max(need, rank[p["id"]] + (0 if same else bump))
|
|
3375
|
+
if need > rank[m["id"]]:
|
|
3376
|
+
rank[m["id"]] = need
|
|
3377
|
+
changed = True
|
|
3378
|
+
head_max: dict = {}
|
|
3379
|
+
for m in drules:
|
|
3380
|
+
head_max[m["head"]] = max(head_max.get(m["head"], 0), rank[m["id"]])
|
|
3381
|
+
for m in drules:
|
|
3382
|
+
if head_max[m["head"]] > rank[m["id"]]:
|
|
3383
|
+
rank[m["id"]] = head_max[m["head"]]
|
|
3384
|
+
changed = True
|
|
3385
|
+
if not changed:
|
|
3386
|
+
return rank
|
|
3387
|
+
raise Refused(f"{UNSTRATIFIED}: цикл через поражаемые зависимости")
|
|
3388
|
+
|
|
3389
|
+
def _late(self, strict: list, drules: list, drank: dict) -> None:
|
|
3390
|
+
"""Поздние производители §111 (LDC-E4103). Strict-замыкание повторяется
|
|
3391
|
+
после каждой поражаемой страты, поэтому предикат со строгим
|
|
3392
|
+
производителем, читающим поражаемый вывод, полон только после страты
|
|
3393
|
+
этого вывода. С errata E-0172 оба оценщика поднимают поражаемого
|
|
3394
|
+
читателя цепочки рангом, а строгого status-sensitive читателя
|
|
3395
|
+
пропускают (lawc — E4103); срез печатает страты без этого подъёма и
|
|
3396
|
+
потому по-прежнему отказывает — снятие отказа есть расширение среза."""
|
|
3397
|
+
dp = {m["head"] for m in drules}
|
|
3398
|
+
level = {}
|
|
3399
|
+
for m in drules:
|
|
3400
|
+
level[m["head"]] = max(level.get(m["head"], 0), drank[m["id"]])
|
|
3401
|
+
late: dict = {m["head"]: set() for m in strict}
|
|
3402
|
+
per_rule: dict = {}
|
|
3403
|
+
changed = True
|
|
3404
|
+
while changed:
|
|
3405
|
+
changed = False
|
|
3406
|
+
for m in strict:
|
|
3407
|
+
acc = set()
|
|
3408
|
+
for _st, pred, _pol in m["reads"]:
|
|
3409
|
+
if pred in dp:
|
|
3410
|
+
acc.add(pred)
|
|
3411
|
+
acc |= late.get(pred, set())
|
|
3412
|
+
per_rule[m["id"]] = acc
|
|
3413
|
+
if not acc <= late[m["head"]]:
|
|
3414
|
+
late[m["head"]] |= acc
|
|
3415
|
+
changed = True
|
|
3416
|
+
for m in strict:
|
|
3417
|
+
for st, pred, pol in m["reads"]:
|
|
3418
|
+
if st in ("monotone", "supported"):
|
|
3419
|
+
continue
|
|
3420
|
+
if pred in dp:
|
|
3421
|
+
raise Refused(f"{LATE}: strict-правило {m['id']} читает {st} {pred} "
|
|
3422
|
+
"с поражаемым производителем")
|
|
3423
|
+
if not late.get(pred):
|
|
3424
|
+
continue
|
|
3425
|
+
opposite = [s for s in strict if s["head"] == pred and s["polarity"] != pol and per_rule[s["id"]]]
|
|
3426
|
+
if st != "established" or opposite:
|
|
3427
|
+
raise Refused(f"{LATE}: strict-правило {m['id']} читает {st} {pred}, "
|
|
3428
|
+
"производимый строгой цепочкой над поражаемым выводом")
|
|
3429
|
+
for m in drules:
|
|
3430
|
+
for pred in [p for _st, p, _pol in m["reads"]] + [m["head"]]:
|
|
3431
|
+
if late.get(pred) and max(level[q] for q in late[pred]) >= drank[m["id"]]:
|
|
3432
|
+
raise Refused(f"{LATE}: правило {m['id']} страты {drank[m['id']]} читает {pred}, "
|
|
3433
|
+
"полный лишь после страты своего поражаемого источника")
|
|
3434
|
+
return {pred: qs | ({pred} if pred in dp else set()) for pred, qs in late.items()} | \
|
|
3435
|
+
{pred: {pred} for pred in dp if pred not in late}
|
|
3436
|
+
|
|
3437
|
+
def _api(self, rels: list, rules: list) -> dict:
|
|
3438
|
+
"""Поверхность API §api (DECISION-0234): вопросы и входы модуля.
|
|
3439
|
+
|
|
3440
|
+
Правила те же, что у `@arxo/law-forms`, и пакет их не объявляет:
|
|
3441
|
+
ВОПРОС — `pub`-отношение, у которого есть производящее правило,
|
|
3442
|
+
читающее в теле ДРУГОЙ предикат (самозамыкание вроде симметрии
|
|
3443
|
+
вопросом не делает); ВХОД — предикат, достижимый из вопросов по
|
|
3444
|
+
телам правил и либо эмпирический, либо без производителя. Печатается
|
|
3445
|
+
только это; остальные отношения остаются таблицами модели."""
|
|
3446
|
+
# DECISION-0237 п. 9: дефитер опоры не создаёт и производителем
|
|
3447
|
+
# вопроса не становится, но его входы и входы отрицательных и
|
|
3448
|
+
# статусных чтений (`checks`) — входы вопроса.
|
|
3449
|
+
producers: dict = {}
|
|
3450
|
+
reads: dict = {}
|
|
3451
|
+
for r in rules:
|
|
3452
|
+
body = {a["pred"] for a in r["atoms"]} | {c["pred"] for c in r.get("checks", [])}
|
|
3453
|
+
if self.v09 and r["name"] in self._meta:
|
|
3454
|
+
# DECISION-0239: входы генераторов агрегатов — входы вопроса.
|
|
3455
|
+
body |= {pred for _st, pred, _pol in self._meta[r["name"]]["reads"]}
|
|
3456
|
+
reads.setdefault(r["head"]["pred"], set()).update(body)
|
|
3457
|
+
if r.get("strength") != "defeater":
|
|
3458
|
+
producers.setdefault(r["head"]["pred"], set()).update(body)
|
|
3459
|
+
questions = sorted(p for p in self.public
|
|
3460
|
+
if (self.by_id.get(p) or {}).get("package", self.root_ns) == self.root_ns
|
|
3461
|
+
and any(q != p for q in producers.get(p, ())))
|
|
3462
|
+
reach: set = set()
|
|
3463
|
+
stack = list(questions)
|
|
3464
|
+
while stack:
|
|
3465
|
+
p = stack.pop()
|
|
3466
|
+
for q in reads.get(p, ()):
|
|
3467
|
+
if q not in reach:
|
|
3468
|
+
reach.add(q)
|
|
3469
|
+
stack.append(q)
|
|
3470
|
+
inputs = sorted(p for p in reach
|
|
3471
|
+
if p != ALWAYS and (p not in producers or self.rel_kind.get(p) == "empirical"))
|
|
3472
|
+
names: dict = {}
|
|
3473
|
+
|
|
3474
|
+
def unique(pred: str) -> str:
|
|
3475
|
+
base = _api_slug(pred)
|
|
3476
|
+
name, n = base, 1
|
|
3477
|
+
while name in names and names[name] != pred:
|
|
3478
|
+
n += 1
|
|
3479
|
+
name = f"{base}_{n}"
|
|
3480
|
+
names[name] = pred
|
|
3481
|
+
return name
|
|
3482
|
+
|
|
3483
|
+
# Источники вопроса: производящие правила с их метками и якорями
|
|
3484
|
+
# §194 — то, что печатается комментарием над функцией.
|
|
3485
|
+
by_head: dict = {}
|
|
3486
|
+
for r in rules:
|
|
3487
|
+
if r.get("strength") != "defeater":
|
|
3488
|
+
by_head.setdefault(r["head"]["pred"], []).append(r["id"])
|
|
3489
|
+
|
|
3490
|
+
def sources(pred: str) -> list:
|
|
3491
|
+
out = []
|
|
3492
|
+
for rid in by_head.get(pred, []):
|
|
3493
|
+
node = self.rule_nodes.get(rid, {})
|
|
3494
|
+
anchors = [dict(self.fragments.get(a) or {"locator": None, "edition": None}, id=a)
|
|
3495
|
+
for a in (node.get("anchors") or [])]
|
|
3496
|
+
out.append({"rule": _slug(rid), "labels": _labels(node), "anchors": anchors})
|
|
3497
|
+
return out
|
|
3498
|
+
|
|
3499
|
+
return {"questions": [{"pred": p, "name": unique(p), "params": list(self.types[p]),
|
|
3500
|
+
"kinds": [self.ts(t) for t in self.types[p]],
|
|
3501
|
+
"labels": self.rel_labels.get(p, []),
|
|
3502
|
+
"inputs": sorted(q for q in inputs if q in self._deps(p, reads)),
|
|
3503
|
+
"sources": sources(p)}
|
|
3504
|
+
for p in questions],
|
|
3505
|
+
"inputs": [{"pred": p, "name": unique(p), "params": list(self.types[p]),
|
|
3506
|
+
"kinds": [self.ts(t) for t in self.types[p]],
|
|
3507
|
+
"labels": self.rel_labels.get(p, [])} for p in inputs]}
|
|
3508
|
+
|
|
3509
|
+
@staticmethod
|
|
3510
|
+
def _deps(pred: str, producers: dict) -> set:
|
|
3511
|
+
out: set = set()
|
|
3512
|
+
stack = [pred]
|
|
3513
|
+
while stack:
|
|
3514
|
+
p = stack.pop()
|
|
3515
|
+
for q in producers.get(p, ()):
|
|
3516
|
+
if q not in out:
|
|
3517
|
+
out.add(q)
|
|
3518
|
+
stack.append(q)
|
|
3519
|
+
return out
|
|
3520
|
+
|
|
3521
|
+
def _fact(self, a: dict) -> dict:
|
|
3522
|
+
lit = a.get("literal") or {}
|
|
3523
|
+
if lit.get("polarity") != "positive":
|
|
3524
|
+
raise Refused("отрицательный собственный факт вне L0-среза")
|
|
3525
|
+
dating = self._dating(a)
|
|
3526
|
+
if dating is not None and dating != [(None, None)]:
|
|
3527
|
+
if not dating:
|
|
3528
|
+
raise _Excluded(a["id"])
|
|
3529
|
+
if len(dating) > 1:
|
|
3530
|
+
raise Refused("прерывистая применимость редакции §92.3 вне среза генератора")
|
|
3531
|
+
# Датированный факт печатается правилом с окном над служебной
|
|
3532
|
+
# таблицей `always` (одна строка): та же механика окна §31, что у
|
|
3533
|
+
# правил, и ни один эмиттер не узнаёт о редакциях.
|
|
3534
|
+
self.dated_facts.append({
|
|
3535
|
+
"name": f"dated_{len(self.dated_facts)}_{_slug(a['id'])}", "id": a["id"],
|
|
3536
|
+
"window": {"start": dating[0][0], "end": dating[0][1]},
|
|
3537
|
+
"atoms": [{"pred": ALWAYS}], "joins": [], "filters": [], "guards": [],
|
|
3538
|
+
"head": {"pred": lit["predicate"], "args": [self.term(x, {}) for x in lit["args"]]}})
|
|
3539
|
+
raise _Excluded(None)
|
|
3540
|
+
return {"pred": lit["predicate"], "args": [self.term(x, {}) for x in lit["args"]]}
|
|
3541
|
+
|
|
3542
|
+
def _setup(self) -> None:
|
|
3543
|
+
self.caps = self._capabilities()
|
|
3544
|
+
# DECISION-0245: `days_between` модели не меняет; фоллибельные
|
|
3545
|
+
# календарные функции требуют накопления выводов модели 0.9.
|
|
3546
|
+
self.l1 = bool(self.caps - {"calendar:days_between", "judgment"} - V15_CAPS)
|
|
3547
|
+
self.v10 = "norm" in self.caps
|
|
3548
|
+
self.v09 = bool(self.caps & {"aggregate", "closure", "norm", "calendar:period", "calendar:business",
|
|
3549
|
+
"value:rational", "value:quantity", "value:round", "value:div_round"}
|
|
3550
|
+
or self.caps & TERM_CAPS or self.caps & FN_CAPS)
|
|
3551
|
+
self.v11 = bool(self.caps & CAL_CAPS)
|
|
3552
|
+
# DECISION-0252 п. 1: ошибка значения §58 требует накопления выводов 0.9.
|
|
3553
|
+
self.v12 = "value:rational" in self.caps
|
|
3554
|
+
self.v13 = "value:quantity" in self.caps
|
|
3555
|
+
# DECISION-0259 п. 1: политика округления для рантайма.
|
|
3556
|
+
self.v14 = bool(self.caps & {"value:round", "value:div_round"})
|
|
3557
|
+
# DECISION-0266–0268: значения и формы, не меняющие модель.
|
|
3558
|
+
self.v15 = bool(self.caps & V15_CAPS)
|
|
3559
|
+
# DECISION-0269: датированные приоритеты §92.3.
|
|
3560
|
+
self.v16 = "priority:dated" in self.caps
|
|
3561
|
+
# DECISION-0270: вычисляемые аргументы тела и условные термы.
|
|
3562
|
+
self.v17 = bool(self.caps & TERM_CAPS)
|
|
3563
|
+
# DECISION-0273: функции, параметры A3, `let`, `match`.
|
|
3564
|
+
self.v18 = bool(self.caps & FN_CAPS)
|
|
3565
|
+
# DECISION-0274: канал суждения §47.3.
|
|
3566
|
+
self.v19 = "judgment" in self.caps
|
|
3567
|
+
self._derived = {(n.get("head") or {}).get("predicate") for n in self.ir["nodes"]
|
|
3568
|
+
if n.get("kind") == "rule" and (n.get("head") or {}).get("kind") == "literal"}
|
|
3569
|
+
|
|
3570
|
+
def build(self) -> dict:
|
|
3571
|
+
ir = self.ir
|
|
3572
|
+
if ir.get("imports") and not self.linked:
|
|
3573
|
+
raise Refused("импорт чужого пакета без мира зависимостей вне среза генератора")
|
|
3574
|
+
self._setup()
|
|
3575
|
+
for n in ir["nodes"]:
|
|
3576
|
+
self._node(n)
|
|
3577
|
+
rels = self._index()
|
|
3578
|
+
self._export_functions()
|
|
3579
|
+
tables = [self._table(r) for r in rels]
|
|
3580
|
+
if self.v10:
|
|
3581
|
+
tables += self._std_tables()
|
|
3582
|
+
rules, excluded = [], []
|
|
3583
|
+
compile_rule = self._rule_v09 if self.v09 else self._rule_l1 if self.l1 else self._rule
|
|
3584
|
+
for i, rule in enumerate([n for n in ir["nodes"] if n.get("kind") == "rule"]):
|
|
3585
|
+
try:
|
|
3586
|
+
if self.v10 and (rule.get("head") or {}).get("kind") == "norm_template_ref":
|
|
3587
|
+
self._norms.append(self._norm_v10(i, rule))
|
|
3588
|
+
else:
|
|
3589
|
+
rules.append(compile_rule(i, rule))
|
|
3590
|
+
except _Excluded as exc:
|
|
3591
|
+
excluded.append(str(exc))
|
|
3592
|
+
if self.l1:
|
|
3593
|
+
for n in ir["nodes"]:
|
|
3594
|
+
if n.get("kind") == "priority_rule":
|
|
3595
|
+
self._priority(n)
|
|
3596
|
+
if n.get("kind") == "closure_policy" and self.v09:
|
|
3597
|
+
self._closure(n)
|
|
3598
|
+
facts = []
|
|
3599
|
+
compile_fact = self._fact_l1 if self.l1 else self._fact
|
|
3600
|
+
for a in ir["nodes"]:
|
|
3601
|
+
if a.get("kind") != "assertion":
|
|
3602
|
+
continue
|
|
3603
|
+
try:
|
|
3604
|
+
facts.append(compile_fact(a))
|
|
3605
|
+
except _Excluded as exc:
|
|
3606
|
+
if str(exc) != "None":
|
|
3607
|
+
excluded.append(str(exc))
|
|
3608
|
+
structure = self._structure(rules)
|
|
3609
|
+
for r in rules:
|
|
3610
|
+
if r["name"] in self._value_refuse:
|
|
3611
|
+
if r.get("headAgg"):
|
|
3612
|
+
# Без перехвата ошибка агрегата головы ушла бы в обработчик G3
|
|
3613
|
+
# и сняла правило целиком — иной ответ, чем у оракула.
|
|
3614
|
+
raise Refused("агрегат в голове с ошибкой значения над незавершённым входом "
|
|
3615
|
+
"вне среза генератора (DECISION-0252 п. 4)")
|
|
3616
|
+
# DECISION-0252 п. 4: без перехвата ошибка значения — отказ раннера.
|
|
3617
|
+
r.pop("headFallible", None)
|
|
3618
|
+
for g in r["guards"]:
|
|
3619
|
+
g.pop("err", None)
|
|
3620
|
+
if self.dated_facts:
|
|
3621
|
+
tables.append({"pred": ALWAYS, "name": "always", "arity": 1, "params": ["urn:law:std#Text"],
|
|
3622
|
+
"keys": [], "kinds": ["text"]})
|
|
3623
|
+
always = {"pred": ALWAYS, "args": [{"k": "lit", "t": "text", "v": "on"}]}
|
|
3624
|
+
facts.append(dict(always, polarity="positive") if self.l1 else always)
|
|
3625
|
+
rules += self.dated_facts
|
|
3626
|
+
plan = {"planVersion": PLAN_VERSION, "package": ir.get("package"),
|
|
3627
|
+
"world": self.world_passport,
|
|
3628
|
+
"tables": tables, "rules": rules, "facts": facts,
|
|
3629
|
+
"excluded": sorted(excluded),
|
|
3630
|
+
"consts": self._consts(), "bases": dict(sorted(self.bases.items())),
|
|
3631
|
+
"api": self._api(rels, rules)}
|
|
3632
|
+
if not self.l1:
|
|
3633
|
+
if self.v11 or self.v15 or self._fn_plan or self.v19:
|
|
3634
|
+
# DECISION-0245 п. 1: `days_between` в модели L0; DECISION-0266–0268
|
|
3635
|
+
# тоже не меняют модели — версия 0.15 и возможности при тех же полях.
|
|
3636
|
+
# DECISION-0273: функции §46 для вопросов `term` — модель L0 версии 0.18.
|
|
3637
|
+
out = dict({"planVersion": PLAN_VERSION_19 if self.v19 else PLAN_VERSION_18 if self._fn_plan
|
|
3638
|
+
else PLAN_VERSION_15 if self.v15 else PLAN_VERSION_11,
|
|
3639
|
+
"capabilities": sorted(self.caps)},
|
|
3640
|
+
**{k: v for k, v in plan.items() if k != "planVersion"})
|
|
3641
|
+
if self.v11:
|
|
3642
|
+
out.update(self._calendar_env())
|
|
3643
|
+
if self._fn_plan:
|
|
3644
|
+
out["functions"] = [self._fn_plan[k] for k in sorted(self._fn_plan)]
|
|
3645
|
+
if self.v19:
|
|
3646
|
+
# DECISION-0274: канал суждения у модели L0 — обход раннера
|
|
3647
|
+
# по правилам с силой и гардами, органы по отношению.
|
|
3648
|
+
out["judgments"] = dict(sorted(self.judgments.items()))
|
|
3649
|
+
out["backchain"] = self._backchain(rules)
|
|
3650
|
+
out["backchainConsts"] = {cid: c["value"] for cid, c in sorted(self.consts.items())
|
|
3651
|
+
if c["value"] is not None}
|
|
3652
|
+
return out
|
|
3653
|
+
return plan
|
|
3654
|
+
if self.v09:
|
|
3655
|
+
structure = dict(structure, aggregates=self._aggs, closures=self._closures)
|
|
3656
|
+
if self.v10:
|
|
3657
|
+
structure = dict(structure, norms=self._norms)
|
|
3658
|
+
if self.v18 or self._fn_plan or self._a3params:
|
|
3659
|
+
# DECISION-0273: функции §46 и параметры A3 в порядке идентификаторов.
|
|
3660
|
+
structure = dict(structure, functions=[self._fn_plan[k] for k in sorted(self._fn_plan)])
|
|
3661
|
+
if self._a3params:
|
|
3662
|
+
structure["a3Params"] = [self._a3params[k] for k in sorted(self._a3params)]
|
|
3663
|
+
self.v18 = True
|
|
3664
|
+
if self.v11:
|
|
3665
|
+
structure = dict(structure, **self._calendar_env())
|
|
3666
|
+
if self.v19:
|
|
3667
|
+
structure = dict(structure, judgments=dict(sorted(self.judgments.items())))
|
|
3668
|
+
if self.caps & {"calendar:period", "calendar:business"} or self.v12 or self.v13 or self.v14 or self.v17 \
|
|
3669
|
+
or self.v18 or self.v19 or self._agg_fallible:
|
|
3670
|
+
# E-0105: обход ошибок терма от цели вопроса (DECISION-0245, 0252, 0270);
|
|
3671
|
+
# DECISION-0274: тот же обход — запрос судье §47.3.
|
|
3672
|
+
structure["backchain"] = self._backchain(rules)
|
|
3673
|
+
# Разыменование констант §54 аргументов вопроса для обхода.
|
|
3674
|
+
structure["backchainConsts"] = {cid: c["value"] for cid, c in sorted(self.consts.items())
|
|
3675
|
+
if c["value"] is not None}
|
|
3676
|
+
return dict({"planVersion": PLAN_VERSION_19 if self.v19 else PLAN_VERSION_18 if self.v18
|
|
3677
|
+
else PLAN_VERSION_17 if self.v17 else PLAN_VERSION_16 if self.v16
|
|
3678
|
+
else PLAN_VERSION_15 if self.v15
|
|
3679
|
+
else PLAN_VERSION_14 if self.v14
|
|
3680
|
+
else PLAN_VERSION_13 if self.v13
|
|
3681
|
+
else PLAN_VERSION_12 if self.v12
|
|
3682
|
+
else PLAN_VERSION_11 if self.v11
|
|
3683
|
+
else PLAN_VERSION_10 if self.v10 else PLAN_VERSION_09 if self.v09 else PLAN_VERSION_L1,
|
|
3684
|
+
"capabilities": sorted(self.caps)},
|
|
3685
|
+
**{k: v for k, v in plan.items() if k != "planVersion"}, **structure)
|
|
3686
|
+
|
|
3687
|
+
def _export_functions(self) -> None:
|
|
3688
|
+
"""DECISION-0273: функции §46 модуля доступны вопросу `term` раннера и
|
|
3689
|
+
без правил (T071), поэтому компилируются заранее. Функция вне среза
|
|
3690
|
+
(запись, список, рекурсия) пакету не отказывает, пока её не читает
|
|
3691
|
+
правило: она не попадает в план, вопрос к ней — названный отказ
|
|
3692
|
+
раннера. Признаки значений неиспользуемой функции не двигают версию."""
|
|
3693
|
+
flags = ("_rational_seen", "_quantity_seen", "_round_seen", "_div_round_seen", "_boolean_seen",
|
|
3694
|
+
"_natural_seen", "_month_seen", "_wildcard_seen", "_if_seen", "_let_seen", "_match_seen",
|
|
3695
|
+
"_body_term_seen", "_function_seen")
|
|
3696
|
+
saved = {f: getattr(self, f) for f in flags}
|
|
3697
|
+
for fn in sorted(self.fdecls):
|
|
3698
|
+
if self._is_a3(fn):
|
|
3699
|
+
continue
|
|
3700
|
+
try:
|
|
3701
|
+
self._fn_sig(fn)
|
|
3702
|
+
except Refused:
|
|
3703
|
+
continue
|
|
3704
|
+
for f, v in saved.items():
|
|
3705
|
+
setattr(self, f, v)
|
|
3706
|
+
|
|
3707
|
+
def _backchain(self, rules: list) -> list:
|
|
3708
|
+
"""Обход ошибок терма E-0105 (`whynot.term_error_blockers`): правила
|
|
3709
|
+
с литеральной головой по id — окно, голова и литеральные конъюнкты
|
|
3710
|
+
тела с разыменованными константами §54, как их видит оракул. Раннер
|
|
3711
|
+
по нему называет статусом NEITHER, за которым стоит ошибка терма."""
|
|
3712
|
+
compiled = {r["id"]: r for r in rules if "id" in r}
|
|
3713
|
+
out = []
|
|
3714
|
+
for n in sorted((n for n in self.ir["nodes"] if n.get("kind") == "rule"
|
|
3715
|
+
and (n.get("head") or {}).get("kind") == "literal" and n["id"] in compiled),
|
|
3716
|
+
key=lambda n: n["id"]):
|
|
3717
|
+
conj: list = []
|
|
3718
|
+
|
|
3719
|
+
def walk(f):
|
|
3720
|
+
k = f.get("kind") if isinstance(f, dict) else None
|
|
3721
|
+
if k == "status" and (f.get("formula") or {}).get("kind") == "literal":
|
|
3722
|
+
conj.append([f.get("status"), self._deref(f["formula"])])
|
|
3723
|
+
elif k == "literal":
|
|
3724
|
+
conj.append(["established", self._deref(f)])
|
|
3725
|
+
elif k == "comparison":
|
|
3726
|
+
# DECISION-0274: гард §57–§58 — живость правила на пути
|
|
3727
|
+
# запроса судье (E-0101, `_rule_is_dead`).
|
|
3728
|
+
conj.append(["guard", self._deref(f)])
|
|
3729
|
+
elif k in ("and", "conjunction"):
|
|
3730
|
+
for x in f.get("items") or f.get("args") or f.get("operands") or []:
|
|
3731
|
+
walk(x)
|
|
3732
|
+
|
|
3733
|
+
walk(n.get("body") or {})
|
|
3734
|
+
r = compiled[n["id"]]
|
|
3735
|
+
out.append({"id": n["id"], "name": r["name"], "window": r["window"],
|
|
3736
|
+
"strength": n.get("strength") or "strict",
|
|
3737
|
+
"head": self._deref(n["head"]), "body": conj})
|
|
3738
|
+
return out
|
|
3739
|
+
|
|
3740
|
+
def _calendar_env(self) -> dict:
|
|
3741
|
+
"""Окружение календаря для раннера (DECISION-0245 п. 3): версия
|
|
3742
|
+
семантики CLIR (у 0.1 политика — объект контекста), политики
|
|
3743
|
+
`deadline_policy` по id и снимки `calendar_snapshot` с датасетом либо
|
|
3744
|
+
кодом ошибки загрузки — порт `calendar.load_calendar`."""
|
|
3745
|
+
import hashlib
|
|
3746
|
+
from pathlib import Path
|
|
3747
|
+
policies, calendars = {}, []
|
|
3748
|
+
for n in self.ir["nodes"]:
|
|
3749
|
+
if n.get("kind") == "deadline_policy" and isinstance(n.get("policy"), dict):
|
|
3750
|
+
policies[n["id"]] = n["policy"]
|
|
3751
|
+
if n.get("kind") != "calendar_snapshot":
|
|
3752
|
+
continue
|
|
3753
|
+
uri = str(n.get("resourceUri", ""))
|
|
3754
|
+
data = self.resources.get(uri)
|
|
3755
|
+
entry = {"id": n["id"]}
|
|
3756
|
+
if data is None:
|
|
3757
|
+
entry["error"] = "MISSING_CALENDAR"
|
|
3758
|
+
elif "sha256:" + hashlib.sha256(data).hexdigest() != n.get("datasetHash", ""):
|
|
3759
|
+
entry["error"] = "CALENDAR_DATASET_HASH_MISMATCH"
|
|
3760
|
+
else:
|
|
3761
|
+
entry.update(_calendar_dataset(data))
|
|
3762
|
+
calendars.append(entry)
|
|
3763
|
+
return {"semanticVersion": self.ir["semanticVersion"],
|
|
3764
|
+
"deadlinePolicies": dict(sorted(policies.items())),
|
|
3765
|
+
"calendars": sorted(calendars, key=lambda c: c["id"])}
|
|
3766
|
+
|
|
3767
|
+
def _consts(self) -> dict:
|
|
3768
|
+
"""Константы со значением для раннера: разыменование §54 аргументов
|
|
3769
|
+
дела и запроса и тип константы для проверки §189. Константа с типом
|
|
3770
|
+
вне среза не отказывает пакету, пока на неё никто не ссылается:
|
|
3771
|
+
она просто не попадает в план."""
|
|
3772
|
+
out = {}
|
|
3773
|
+
for cid, c in sorted(self.consts.items()):
|
|
3774
|
+
if c["value"] is None:
|
|
3775
|
+
continue
|
|
3776
|
+
if c["type"] == RAT and not self.v12:
|
|
3777
|
+
continue # DECISION-0252: значение Rational несёт только план 0.12
|
|
3778
|
+
if c["type"] == QTY and not self.v13:
|
|
3779
|
+
continue # DECISION-0256: значение Quantity несёт только план 0.13
|
|
3780
|
+
try:
|
|
3781
|
+
out[cid] = {"type": c["type"], "term": self.term(c["value"], {})}
|
|
3782
|
+
except Refused:
|
|
3783
|
+
continue
|
|
3784
|
+
return out
|
|
3785
|
+
|
|
3786
|
+
def survey(self) -> list[str]:
|
|
3787
|
+
"""Все отказы пакета по единицам проверки; пустой список — план строится."""
|
|
3788
|
+
ir = self.ir
|
|
3789
|
+
out: list[str] = []
|
|
3790
|
+
|
|
3791
|
+
def attempt(fn, *args) -> None:
|
|
3792
|
+
try:
|
|
3793
|
+
fn(*args)
|
|
3794
|
+
except Refused as exc:
|
|
3795
|
+
out.append(str(exc))
|
|
3796
|
+
except _Excluded:
|
|
3797
|
+
pass
|
|
3798
|
+
|
|
3799
|
+
if ir.get("imports") and not self.linked:
|
|
3800
|
+
out.append("импорт чужого пакета без мира зависимостей вне среза генератора")
|
|
3801
|
+
self._setup()
|
|
3802
|
+
for n in ir["nodes"]:
|
|
3803
|
+
attempt(self._node, n)
|
|
3804
|
+
rels = self._index()
|
|
3805
|
+
for r in rels:
|
|
3806
|
+
attempt(self._table, r)
|
|
3807
|
+
if self.v10:
|
|
3808
|
+
attempt(self._std_tables)
|
|
3809
|
+
rules: list = []
|
|
3810
|
+
compile_rule = self._rule_v09 if self.v09 else self._rule_l1 if self.l1 else self._rule
|
|
3811
|
+
for i, rule in enumerate([n for n in ir["nodes"] if n.get("kind") == "rule"]):
|
|
3812
|
+
if self.v10 and (rule.get("head") or {}).get("kind") == "norm_template_ref":
|
|
3813
|
+
attempt(lambda i=i, rule=rule: self._norms.append(self._norm_v10(i, rule)))
|
|
3814
|
+
else:
|
|
3815
|
+
attempt(lambda i=i, rule=rule: rules.append(compile_rule(i, rule)))
|
|
3816
|
+
if self.l1:
|
|
3817
|
+
for n in ir["nodes"]:
|
|
3818
|
+
if n.get("kind") == "priority_rule":
|
|
3819
|
+
attempt(self._priority, n)
|
|
3820
|
+
if n.get("kind") == "closure_policy" and self.v09:
|
|
3821
|
+
attempt(self._closure, n)
|
|
3822
|
+
for a in [n for n in ir["nodes"] if n.get("kind") == "assertion"]:
|
|
3823
|
+
attempt(self._fact_l1 if self.l1 else self._fact, a)
|
|
3824
|
+
# Структура — одна единица над правилами, прошедшими свою проверку:
|
|
3825
|
+
# цикл или поздний производитель — свойство программы, не правила.
|
|
3826
|
+
attempt(self._structure, rules)
|
|
3827
|
+
return out
|
|
3828
|
+
|
|
3829
|
+
def capabilities(self) -> list[str]:
|
|
3830
|
+
"""Возможности программы без построения плана (для обзора)."""
|
|
3831
|
+
return sorted(self._capabilities())
|
|
3832
|
+
|
|
3833
|
+
|
|
3834
|
+
def _closure_read_dependencies(status: str, predicate: str, polarity: str, enumeration: bool) -> tuple:
|
|
3835
|
+
"""Порт `rules._closure_read_dependencies` (E-0174): зависимости одного
|
|
3836
|
+
чтения — (монотонные, немонотонные). Элемент `("rule", P, полярность)` —
|
|
3837
|
+
опоры строгих производителей головы `P`, `("closure", P)` — негативы
|
|
3838
|
+
замыкания над `P`. `established` опровергает противоположная опора,
|
|
3839
|
+
`not_known` и `refuted` — опора той же полярности, `unknown` — любая;
|
|
3840
|
+
перечисление немонотонно по всему, от чего зависит его тест."""
|
|
3841
|
+
negative = polarity == "negative"
|
|
3842
|
+
same = ("rule", predicate, polarity)
|
|
3843
|
+
other = ("rule", predicate, "positive" if negative else "negative")
|
|
3844
|
+
closure = ("closure", predicate)
|
|
3845
|
+
if status in ("supported", "monotone"):
|
|
3846
|
+
monotone, non_monotone = [same] + ([closure] if negative else []), []
|
|
3847
|
+
elif status == "not_known":
|
|
3848
|
+
monotone, non_monotone = [], [same] + ([closure] if negative else [])
|
|
3849
|
+
elif status == "refuted":
|
|
3850
|
+
monotone, non_monotone = [other] + ([] if negative else [closure]), [same]
|
|
3851
|
+
elif status == "unknown":
|
|
3852
|
+
monotone, non_monotone = [], [same, other, closure]
|
|
3853
|
+
else:
|
|
3854
|
+
monotone, non_monotone = [same] + ([closure] if negative else []), [other]
|
|
3855
|
+
if enumeration:
|
|
3856
|
+
return [], monotone + non_monotone
|
|
3857
|
+
return monotone, non_monotone
|
|
3858
|
+
|
|
3859
|
+
|
|
3860
|
+
def _closure_waits(strict: list, closed: dict) -> dict:
|
|
3861
|
+
"""Порт `rules.closure_waits` (errata E-0174): id strict-правила →
|
|
3862
|
+
id замыканий, до материализации которых оно не исполняется. Ожидание
|
|
3863
|
+
транзитивно через строгих производителей: производитель завершён, когда
|
|
3864
|
+
завершены все его чтения. Пустой словарь — прежнее расписание."""
|
|
3865
|
+
if not closed:
|
|
3866
|
+
return {}
|
|
3867
|
+
reads_of = []
|
|
3868
|
+
for m in strict:
|
|
3869
|
+
monotone, non_monotone = [], []
|
|
3870
|
+
for st, pred, pol, enumeration in m.get("closure_reads", []):
|
|
3871
|
+
mono, non = _closure_read_dependencies(st, pred, pol, enumeration)
|
|
3872
|
+
monotone += mono
|
|
3873
|
+
non_monotone += non
|
|
3874
|
+
reads_of.append((m["id"], (m["head"], m["polarity"]), monotone, non_monotone))
|
|
3875
|
+
ready: dict = {}
|
|
3876
|
+
|
|
3877
|
+
def value(dep: tuple) -> frozenset:
|
|
3878
|
+
if dep[0] == "closure":
|
|
3879
|
+
return frozenset(c["id"] for c in closed.get(dep[1], []))
|
|
3880
|
+
return ready.get((dep[1], dep[2]), frozenset())
|
|
3881
|
+
|
|
3882
|
+
waits = {rule_id: frozenset() for rule_id, *_ in reads_of}
|
|
3883
|
+
changed = True
|
|
3884
|
+
while changed:
|
|
3885
|
+
changed = False
|
|
3886
|
+
for rule_id, head, monotone, non_monotone in reads_of:
|
|
3887
|
+
wait = frozenset().union(*(value(d) for d in non_monotone))
|
|
3888
|
+
complete = wait.union(*(value(d) for d in monotone))
|
|
3889
|
+
if wait != waits[rule_id]:
|
|
3890
|
+
waits[rule_id] = wait
|
|
3891
|
+
changed = True
|
|
3892
|
+
if not complete <= ready.get(head, frozenset()):
|
|
3893
|
+
ready[head] = ready.get(head, frozenset()) | complete
|
|
3894
|
+
changed = True
|
|
3895
|
+
return {rule_id: wait for rule_id, wait in waits.items() if wait}
|
|
3896
|
+
|
|
3897
|
+
|
|
3898
|
+
def build_plan(ir: dict, world: list | None = None, want: set | None = None,
|
|
3899
|
+
resources: dict | None = None) -> dict:
|
|
3900
|
+
"""План программы. Rational §35 и значение-зависимое частное §58 видны
|
|
3901
|
+
только при компиляции термов, а возможность `value:rational` меняет модель
|
|
3902
|
+
(DECISION-0252 п. 1): встретив их, план строится заново с возможностью.
|
|
3903
|
+
Отказ первого прохода проверяется обзором: он доходит до всех правил."""
|
|
3904
|
+
_admit_current_slice(ir, world)
|
|
3905
|
+
builder = Builder(ir, world, want, resources)
|
|
3906
|
+
try:
|
|
3907
|
+
plan = builder.build()
|
|
3908
|
+
needed = value_caps(builder) - builder.caps
|
|
3909
|
+
except Refused:
|
|
3910
|
+
probe = Builder(ir, world, want, resources)
|
|
3911
|
+
probe.survey()
|
|
3912
|
+
needed = value_caps(probe) - probe.caps
|
|
3913
|
+
if not needed:
|
|
3914
|
+
raise
|
|
3915
|
+
want = set(want or ())
|
|
3916
|
+
# DECISION-0273: параметр A3, функция, `let`, `match` внутри вычисляемого
|
|
3917
|
+
# аргумента тела видны только проходу с `term:body` — возможности
|
|
3918
|
+
# добираются до неподвижной точки (каждый проход добавляет хотя бы одну).
|
|
3919
|
+
while needed:
|
|
3920
|
+
want |= needed
|
|
3921
|
+
builder = Builder(ir, world, want, resources)
|
|
3922
|
+
plan = builder.build()
|
|
3923
|
+
needed = value_caps(builder) - builder.caps
|
|
3924
|
+
return dict(plan, planVersion="law-backend-plan/0.20", basePlanVersion=plan["planVersion"],
|
|
3925
|
+
capabilities=sorted(set(plan.get("capabilities") or []) | {"admission:current"}),
|
|
3926
|
+
admission={"reservedAssertionIds": sorted({n["id"] for document in [ir, *(world or [])]
|
|
3927
|
+
for n in document.get("nodes") or [] if n.get("id") and n.get("kind") != "assertion"})})
|
|
3928
|
+
|
|
3929
|
+
|
|
3930
|
+
def value_caps(builder: Builder) -> set:
|
|
3931
|
+
"""Возможности значений, встреченные проходом компиляции: Rational
|
|
3932
|
+
(DECISION-0252), Quantity (DECISION-0256), политика округления
|
|
3933
|
+
(DECISION-0259). Признаки отмечаются и без возможности, поэтому второго
|
|
3934
|
+
прохода достаточно."""
|
|
3935
|
+
return ({"value:rational"} if builder._rational_seen else set()) | \
|
|
3936
|
+
({"value:quantity"} if builder._quantity_seen else set()) | \
|
|
3937
|
+
({"value:instant"} if builder._instant_seen else set()) | \
|
|
3938
|
+
({"value:round"} if builder._round_seen else set()) | \
|
|
3939
|
+
({"value:div_round"} if builder._div_round_seen else set()) | \
|
|
3940
|
+
({"value:boolean"} if builder._boolean_seen else set()) | \
|
|
3941
|
+
({"value:natural"} if builder._natural_seen else set()) | \
|
|
3942
|
+
({"calendar:month_bound"} if builder._month_seen else set()) | \
|
|
3943
|
+
({"wildcard"} if builder._wildcard_seen else set()) | \
|
|
3944
|
+
({"priority:dated"} if builder._dated_prio_seen else set()) | \
|
|
3945
|
+
({"term:body"} if builder._body_term_seen else set()) | \
|
|
3946
|
+
({"term:if"} if builder._if_seen else set()) | \
|
|
3947
|
+
({"function"} if builder._function_seen else set()) | \
|
|
3948
|
+
({"param:a3"} if builder._a3_seen else set()) | \
|
|
3949
|
+
({"term:let"} if builder._let_seen else set()) | \
|
|
3950
|
+
({"term:match"} if builder._match_seen else set())
|
|
3951
|
+
|
|
3952
|
+
|
|
3953
|
+
def survey_plan(ir: dict, world: list | None = None) -> list[str]:
|
|
3954
|
+
_admit_current_slice(ir, world)
|
|
3955
|
+
builder = Builder(ir, world)
|
|
3956
|
+
reasons = builder.survey()
|
|
3957
|
+
needed = value_caps(builder) - builder.caps
|
|
3958
|
+
if needed:
|
|
3959
|
+
return Builder(ir, world, needed).survey()
|
|
3960
|
+
return reasons
|
|
3961
|
+
|
|
3962
|
+
|
|
3963
|
+
WEEKDAYS = ("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")
|
|
3964
|
+
|
|
3965
|
+
|
|
3966
|
+
def _calendar_dataset(data: bytes) -> dict:
|
|
3967
|
+
"""Датасет снимка §85 для модуля: покрытие, недельный график, исключения —
|
|
3968
|
+
инварианты `calendar.load_calendar`; нарушение — код ошибки загрузки,
|
|
3969
|
+
который оракул выпускает при первом обращении к календарю."""
|
|
3970
|
+
import datetime
|
|
3971
|
+
from pathlib import Path
|
|
3972
|
+
import jsonschema
|
|
3973
|
+
invalid = {"error": "CALENDAR_DATASET_INVALID"}
|
|
3974
|
+
try:
|
|
3975
|
+
dataset = json.loads(data.decode("utf-8"))
|
|
3976
|
+
# Схема датасета §85 — та же, что у `schemas.validate("calendar-dataset")`.
|
|
3977
|
+
from core_support import calendar_schema
|
|
3978
|
+
schema = calendar_schema()
|
|
3979
|
+
if any(True for _ in jsonschema.Draft202012Validator(schema).iter_errors(dataset)):
|
|
3980
|
+
return invalid
|
|
3981
|
+
valid = dataset["valid"]
|
|
3982
|
+
start = datetime.date.fromisoformat(valid["start"])
|
|
3983
|
+
end = datetime.date.fromisoformat(valid["endExclusive"])
|
|
3984
|
+
weekly = {e["day"]: bool(e["working"]) for e in dataset["weeklySchedule"]}
|
|
3985
|
+
if any(d not in weekly for d in WEEKDAYS):
|
|
3986
|
+
return invalid
|
|
3987
|
+
overrides = {}
|
|
3988
|
+
for e in dataset.get("overrides", []):
|
|
3989
|
+
day = datetime.date.fromisoformat(e["date"])
|
|
3990
|
+
if not (start <= day < end):
|
|
3991
|
+
return invalid
|
|
3992
|
+
overrides[day.isoformat()] = bool(e["working"])
|
|
3993
|
+
except (ValueError, KeyError, TypeError, AttributeError):
|
|
3994
|
+
return invalid
|
|
3995
|
+
return {"start": start.isoformat(), "endExclusive": end.isoformat(),
|
|
3996
|
+
"weekly": [weekly[d] for d in WEEKDAYS], "overrides": dict(sorted(overrides.items()))}
|
|
3997
|
+
|
|
3998
|
+
|
|
3999
|
+
# --- мир зависимостей ---------------------------------------------------------
|
|
4000
|
+
|
|
4001
|
+
def world_index(root) -> dict:
|
|
4002
|
+
"""Снимки CLIR по имени пакета: `corpus/clir` и собственные снимки
|
|
4003
|
+
`packs/**/clir` (без `deps/` и `.build/` — копий чужих)."""
|
|
4004
|
+
from pathlib import Path
|
|
4005
|
+
root = Path(root)
|
|
4006
|
+
index = {}
|
|
4007
|
+
paths = sorted((root / "corpus" / "clir").glob("*.lawir.json"))
|
|
4008
|
+
for p in sorted((root / "packs").rglob("*.lawir.json")):
|
|
4009
|
+
if "deps" in p.parts or ".build" in p.parts:
|
|
4010
|
+
continue
|
|
4011
|
+
paths.append(p)
|
|
4012
|
+
for p in paths:
|
|
4013
|
+
try:
|
|
4014
|
+
ir = json.loads(p.read_text(encoding="utf-8"))
|
|
4015
|
+
except (OSError, ValueError):
|
|
4016
|
+
continue
|
|
4017
|
+
name = (ir.get("package") or {}).get("name")
|
|
4018
|
+
if name and name not in index:
|
|
4019
|
+
index[name] = p
|
|
4020
|
+
return index
|
|
4021
|
+
|
|
4022
|
+
|
|
4023
|
+
def resolve_world(ir: dict, index: dict, extra: list | None = None) -> list:
|
|
4024
|
+
"""Замыкание импортов корня по индексу снимков; `extra` — CLIR, поданные
|
|
4025
|
+
явно (зависимости вектора). Импорт без снимка — отказ плана."""
|
|
4026
|
+
from pathlib import Path
|
|
4027
|
+
from inputs import dependency_index
|
|
4028
|
+
have = dependency_index(extra or [])
|
|
4029
|
+
out, queue = [], [i.get("package", {}).get("name") for i in ir.get("imports") or []]
|
|
4030
|
+
seen = set()
|
|
4031
|
+
while queue:
|
|
4032
|
+
name = queue.pop(0)
|
|
4033
|
+
if not name or name in seen:
|
|
4034
|
+
continue
|
|
4035
|
+
seen.add(name)
|
|
4036
|
+
dep = have.get(name)
|
|
4037
|
+
if dep is None:
|
|
4038
|
+
path = index.get(name)
|
|
4039
|
+
if path is None:
|
|
4040
|
+
raise Refused(f"импорт без снимка CLIR в мире: {name}")
|
|
4041
|
+
dep = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
4042
|
+
out.append(dep)
|
|
4043
|
+
queue += [i.get("package", {}).get("name") for i in dep.get("imports") or []]
|
|
4044
|
+
return out
|
|
4045
|
+
|
|
4046
|
+
|
|
4047
|
+
if __name__ == "__main__":
|
|
4048
|
+
import sys
|
|
4049
|
+
from pathlib import Path
|
|
4050
|
+
plan = build_plan(json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")))
|
|
4051
|
+
out = sys.argv[2] if len(sys.argv) > 2 else "-"
|
|
4052
|
+
text = json.dumps(plan, ensure_ascii=False, indent=1)
|
|
4053
|
+
print(text) if out == "-" else Path(out).write_text(text, encoding="utf-8")
|