arxo-codegen 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arxo_codegen/__init__.py +5 -0
- arxo_codegen/__main__.py +7 -0
- arxo_codegen/active-outcomes.json +6837 -0
- arxo_codegen/check.py +158 -0
- arxo_codegen/check_active_canaries.py +262 -0
- arxo_codegen/check_api.py +76 -0
- arxo_codegen/check_conformance.py +414 -0
- arxo_codegen/check_matrix.py +131 -0
- arxo_codegen/check_packages.py +281 -0
- arxo_codegen/check_semantic_probes.py +293 -0
- arxo_codegen/check_xlsx.py +320 -0
- arxo_codegen/consumer_runner.py +316 -0
- arxo_codegen/core_support.py +203 -0
- arxo_codegen/coverage.py +341 -0
- arxo_codegen/deliver.py +180 -0
- arxo_codegen/examples/xlsx/pricing.lawir.json +409 -0
- arxo_codegen/examples/xlsx/pricing.xlsx.json +52 -0
- arxo_codegen/excel_plan.py +602 -0
- arxo_codegen/harness/verify/ci/gates/backends/check_backends.py +1248 -0
- arxo_codegen/harness/verify/ci/gates/backends/l1_fixtures.py +463 -0
- arxo_codegen/inert-nodes.json +62 -0
- arxo_codegen/inert.py +91 -0
- arxo_codegen/inputs.py +59 -0
- arxo_codegen/package.py +565 -0
- arxo_codegen/plan.py +4053 -0
- arxo_codegen/recertify_registry.py +100 -0
- arxo_codegen/runtime-cert.json +5259 -0
- arxo_codegen/runtime_cert.py +233 -0
- arxo_codegen/survey.py +272 -0
- arxo_codegen/target_runner.py +101 -0
- arxo_codegen/targets/bsl/cert.bsl +113 -0
- arxo_codegen/targets/bsl/emit.py +356 -0
- arxo_codegen/targets/bsl/json.bsl +243 -0
- arxo_codegen/targets/bsl/runner.bsl +210 -0
- arxo_codegen/targets/bsl/runtime.bsl +858 -0
- arxo_codegen/targets/cs/Cert.cs +90 -0
- arxo_codegen/targets/cs/Runner.cs +154 -0
- arxo_codegen/targets/cs/Runtime.cs +243 -0
- arxo_codegen/targets/cs/emit.py +223 -0
- arxo_codegen/targets/dart/cert.dart +99 -0
- arxo_codegen/targets/dart/emit.py +206 -0
- arxo_codegen/targets/dart/json.dart +166 -0
- arxo_codegen/targets/dart/runner.dart +154 -0
- arxo_codegen/targets/dart/runtime.dart +293 -0
- arxo_codegen/targets/erl/cert.erl +64 -0
- arxo_codegen/targets/erl/emit.py +216 -0
- arxo_codegen/targets/erl/rt.erl +238 -0
- arxo_codegen/targets/erl/runner.erl +130 -0
- arxo_codegen/targets/ex/cert.exs +62 -0
- arxo_codegen/targets/ex/emit.py +204 -0
- arxo_codegen/targets/ex/runner.exs +124 -0
- arxo_codegen/targets/ex/runtime.ex +227 -0
- arxo_codegen/targets/go/apiBase.go +483 -0
- arxo_codegen/targets/go/apicheck.go +97 -0
- arxo_codegen/targets/go/cert.go +218 -0
- arxo_codegen/targets/go/emit.py +1806 -0
- arxo_codegen/targets/go/evaluate.go +1826 -0
- arxo_codegen/targets/go/runner.go +38 -0
- arxo_codegen/targets/go/runtime.go +2612 -0
- arxo_codegen/targets/java/Cert.java +91 -0
- arxo_codegen/targets/java/Json.java +148 -0
- arxo_codegen/targets/java/Runner.java +151 -0
- arxo_codegen/targets/java/Runtime.java +243 -0
- arxo_codegen/targets/java/emit.py +198 -0
- arxo_codegen/targets/kt/Cert.kt +67 -0
- arxo_codegen/targets/kt/Json.kt +103 -0
- arxo_codegen/targets/kt/Runner.kt +126 -0
- arxo_codegen/targets/kt/Runtime.kt +196 -0
- arxo_codegen/targets/kt/emit.py +195 -0
- arxo_codegen/targets/lua/cert.lua +82 -0
- arxo_codegen/targets/lua/emit.py +250 -0
- arxo_codegen/targets/lua/json.lua +133 -0
- arxo_codegen/targets/lua/runner.lua +167 -0
- arxo_codegen/targets/lua/runtime.lua +541 -0
- arxo_codegen/targets/php/cert.php +84 -0
- arxo_codegen/targets/php/emit.py +229 -0
- arxo_codegen/targets/php/json.php +144 -0
- arxo_codegen/targets/php/runner.php +189 -0
- arxo_codegen/targets/php/runtime.php +660 -0
- arxo_codegen/targets/py/api_base.py +334 -0
- arxo_codegen/targets/py/apicheck.py +51 -0
- arxo_codegen/targets/py/cert.py +140 -0
- arxo_codegen/targets/py/emit.py +1140 -0
- arxo_codegen/targets/py/evaluate.py +980 -0
- arxo_codegen/targets/py/runner.py +38 -0
- arxo_codegen/targets/py/runtime.py +1634 -0
- arxo_codegen/targets/rb/cert.rb +73 -0
- arxo_codegen/targets/rb/emit.py +203 -0
- arxo_codegen/targets/rb/runner.rb +130 -0
- arxo_codegen/targets/rb/runtime.rb +255 -0
- arxo_codegen/targets/rs/Cargo.lock +142 -0
- arxo_codegen/targets/rs/Cargo.toml +16 -0
- arxo_codegen/targets/rs/cert.rs +106 -0
- arxo_codegen/targets/rs/emit.py +233 -0
- arxo_codegen/targets/rs/prefetch.rs +1 -0
- arxo_codegen/targets/rs/runner.rs +181 -0
- arxo_codegen/targets/rs/runtime.rs +448 -0
- arxo_codegen/targets/scala/Cert.scala +77 -0
- arxo_codegen/targets/scala/Json.scala +140 -0
- arxo_codegen/targets/scala/Runner.scala +121 -0
- arxo_codegen/targets/scala/Runtime.scala +269 -0
- arxo_codegen/targets/scala/emit.py +193 -0
- arxo_codegen/targets/swift/cert.swift +105 -0
- arxo_codegen/targets/swift/emit.py +231 -0
- arxo_codegen/targets/swift/json.swift +208 -0
- arxo_codegen/targets/swift/runner.swift +133 -0
- arxo_codegen/targets/swift/runtime.swift +528 -0
- arxo_codegen/targets/ts/apiBase.ts +250 -0
- arxo_codegen/targets/ts/apicheck.ts +37 -0
- arxo_codegen/targets/ts/cert.ts +96 -0
- arxo_codegen/targets/ts/emit.py +1214 -0
- arxo_codegen/targets/ts/evaluate.ts +894 -0
- arxo_codegen/targets/ts/package-lock.json +45 -0
- arxo_codegen/targets/ts/package.json +9 -0
- arxo_codegen/targets/ts/runner.ts +31 -0
- arxo_codegen/targets/ts/runtime.ts +1369 -0
- arxo_codegen/targets/xlsx/README.ru.md +176 -0
- arxo_codegen/targets/xlsx/emit.py +198 -0
- arxo_codegen/temporal.py +59 -0
- arxo_codegen/test_alias_types.py +171 -0
- arxo_codegen/test_api_contract.py +89 -0
- arxo_codegen/test_calendar_policy.py +48 -0
- arxo_codegen/test_check.py +49 -0
- arxo_codegen/test_check_active_canaries.py +96 -0
- arxo_codegen/test_check_conformance.py +132 -0
- arxo_codegen/test_check_semantic_probes.py +85 -0
- arxo_codegen/test_check_xlsx.py +52 -0
- arxo_codegen/test_consumer_runner.py +175 -0
- arxo_codegen/test_core_support.py +80 -0
- arxo_codegen/test_e0212_decision_time.py +63 -0
- arxo_codegen/test_head_aggregate.py +71 -0
- arxo_codegen/test_inert.py +143 -0
- arxo_codegen/test_instant_types.py +113 -0
- arxo_codegen/test_key_conflict_scope.py +92 -0
- arxo_codegen/test_key_conflict_targets.py +17 -0
- arxo_codegen/test_package_layout.py +163 -0
- arxo_codegen/test_plan_language_version.py +44 -0
- arxo_codegen/test_positions_refusal.py +43 -0
- arxo_codegen/test_recertify_registry.py +77 -0
- arxo_codegen/test_ruby_target.py +41 -0
- arxo_codegen/test_runtime_cert.py +83 -0
- arxo_codegen/test_secondary_key_conflict_scope.py +17 -0
- arxo_codegen/test_stage_r.py +108 -0
- arxo_codegen/test_standalone_inputs.py +71 -0
- arxo_codegen/test_support.py +150 -0
- arxo_codegen/test_survey_inputs.py +115 -0
- arxo_codegen-0.4.0.dist-info/LICENSE +202 -0
- arxo_codegen-0.4.0.dist-info/METADATA +1673 -0
- arxo_codegen-0.4.0.dist-info/RECORD +152 -0
- arxo_codegen-0.4.0.dist-info/WHEEL +5 -0
- arxo_codegen-0.4.0.dist-info/entry_points.txt +13 -0
- arxo_codegen-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1634 @@
|
|
|
1
|
+
"""Рантайм L0 для Python. Тот же контракт, что у runtime.ts и runtime.go:
|
|
2
|
+
точная десятичная арифметика §58, деньги с валютой, названный режим
|
|
3
|
+
округления, календарный день, член перечисления."""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import builtins as _b
|
|
7
|
+
import contextvars as _cv
|
|
8
|
+
import re
|
|
9
|
+
import datetime as _dt
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Refused(Exception):
|
|
13
|
+
"""Названный отказ рантайма: то, что срез не считает, а не сбой.
|
|
14
|
+
|
|
15
|
+
Раннер печатает его как JSON с кодом выхода 3. Любое другое исключение —
|
|
16
|
+
крах, и ворота обязаны его отличать: молчаливое «не смог» и «упал»
|
|
17
|
+
неразличимы по одному ненулевому коду."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Dec:
|
|
21
|
+
__slots__ = ("u", "s")
|
|
22
|
+
|
|
23
|
+
def __init__(self, u: int, s: int):
|
|
24
|
+
self.u, self.s = u, s
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Money(Dec):
|
|
28
|
+
__slots__ = ("cur",)
|
|
29
|
+
|
|
30
|
+
def __init__(self, u: int, s: int, cur: str):
|
|
31
|
+
super().__init__(u, s)
|
|
32
|
+
self.cur = cur
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Qty(Dec):
|
|
36
|
+
"""Quantity §49 v1 (DECISION-0256 п. 5): точное десятичное и тег единицы —
|
|
37
|
+
байты записи; реестра единиц и алиасов §49.2 здесь нет. Не Money: у ключа
|
|
38
|
+
и у сравнения свой вид, 1 KZT денег и величина 1 [KZT] — разные значения."""
|
|
39
|
+
__slots__ = ("unit",)
|
|
40
|
+
|
|
41
|
+
def __init__(self, u: int, s: int, unit: str):
|
|
42
|
+
super().__init__(u, s)
|
|
43
|
+
self.unit = unit
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Rat:
|
|
47
|
+
"""Rational §35 (DECISION-0252 п. 6): несократимая дробь на целых
|
|
48
|
+
произвольной длины, знаменатель положителен, ноль — 0/1. Не Dec: у ключа
|
|
49
|
+
строки и у сравнения свой вид, 2/1 и Decimal 2 — разные значения §48."""
|
|
50
|
+
__slots__ = ("n", "d")
|
|
51
|
+
|
|
52
|
+
def __init__(self, n: int, d: int):
|
|
53
|
+
if d == 0:
|
|
54
|
+
raise TermError("DIVISION_BY_ZERO")
|
|
55
|
+
if d < 0:
|
|
56
|
+
n, d = -n, -d
|
|
57
|
+
g = _gcd(n, d) or 1
|
|
58
|
+
self.n, self.d = n // g, d // g
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_DATE_RE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}\Z")
|
|
62
|
+
_INSTANT_RE = re.compile(r"([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.([0-9]+))?(Z|[+-][0-9]{2}:[0-9]{2})\Z")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _date_parts(raw):
|
|
66
|
+
if not isinstance(raw, str) or not _DATE_RE.fullmatch(raw):
|
|
67
|
+
raise TermError("TYPE_ERROR")
|
|
68
|
+
try:
|
|
69
|
+
return _dt.date.fromisoformat(raw)
|
|
70
|
+
except ValueError as exc:
|
|
71
|
+
raise TermError("TYPE_ERROR") from exc
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Date:
|
|
75
|
+
"""Strict ASCII Gregorian Date (§82.2)."""
|
|
76
|
+
__slots__ = ("v",)
|
|
77
|
+
|
|
78
|
+
def __init__(self, v: str):
|
|
79
|
+
_date_parts(v)
|
|
80
|
+
self.v = v
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def date(lit: str) -> Date:
|
|
84
|
+
return Date(lit)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Instant:
|
|
88
|
+
"""Keep the record for atom identity; compare the exact moment (§82.2)."""
|
|
89
|
+
__slots__ = ("v",)
|
|
90
|
+
|
|
91
|
+
def __init__(self, v: str):
|
|
92
|
+
_moment_text(v)
|
|
93
|
+
self.v = v
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def instant(lit: str) -> Instant:
|
|
97
|
+
return Instant(lit)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _moment_text(raw):
|
|
101
|
+
m = _INSTANT_RE.fullmatch(raw) if isinstance(raw, str) else None
|
|
102
|
+
if not m:
|
|
103
|
+
raise TermError("TYPE_ERROR")
|
|
104
|
+
day = _date_parts(m[1])
|
|
105
|
+
h, mi, sec = map(int, (m[2], m[3], m[4]))
|
|
106
|
+
off = m[6]
|
|
107
|
+
if h > 23 or mi > 59 or sec > 59:
|
|
108
|
+
raise TermError("TYPE_ERROR")
|
|
109
|
+
offset = 0
|
|
110
|
+
if off != "Z":
|
|
111
|
+
oh, om = int(off[1:3]), int(off[4:6])
|
|
112
|
+
if oh > 23 or om > 59:
|
|
113
|
+
raise TermError("TYPE_ERROR")
|
|
114
|
+
offset = (-1 if off[0] == "-" else 1) * (oh * 3600 + om * 60)
|
|
115
|
+
seconds = (day.toordinal() - _dt.date(1970, 1, 1).toordinal()) * 86400 + h * 3600 + mi * 60 + sec - offset
|
|
116
|
+
return seconds, (m[5] or "").rstrip("0")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _moment(value: Instant):
|
|
120
|
+
return _moment_text(value.v)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _moment_pair(op: str, a: Instant, b: Instant):
|
|
124
|
+
return _moment(a), _moment(b)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def validate_temporal_case(case):
|
|
128
|
+
for i, assertion in enumerate(case.get("assertions") or []):
|
|
129
|
+
if "valid" not in assertion:
|
|
130
|
+
continue
|
|
131
|
+
interval = assertion["valid"]
|
|
132
|
+
if not isinstance(interval, dict):
|
|
133
|
+
raise Refused(f"TEMPORAL_BOUND_INVALID: request:/case/assertions/{i}/valid")
|
|
134
|
+
for side in ("start", "end"):
|
|
135
|
+
if side not in interval:
|
|
136
|
+
continue
|
|
137
|
+
value = interval[side]
|
|
138
|
+
if value == ("-infinity" if side == "start" else "infinity"):
|
|
139
|
+
continue
|
|
140
|
+
try:
|
|
141
|
+
_date_parts(value) if isinstance(value, str) and _DATE_RE.fullmatch(value) else _moment_text(value)
|
|
142
|
+
except TermError as exc:
|
|
143
|
+
raise Refused(f"TEMPORAL_BOUND_INVALID: request:/case/assertions/{i}/valid/{side}") from exc
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Sym:
|
|
147
|
+
"""Член перечисления §38 либо иная ссылка `const_ref` без объявления:
|
|
148
|
+
значение — stable qualified identity, равенство — по ней (§57), порядок не
|
|
149
|
+
определён. В ключе атома у оракула это узел `const_ref`, не сущность."""
|
|
150
|
+
__slots__ = ("id",)
|
|
151
|
+
|
|
152
|
+
def __init__(self, id: str): # noqa: A002
|
|
153
|
+
self.id = id
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def sym(id: str) -> Sym: # noqa: A002
|
|
157
|
+
return Sym(str(id))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def dec(lit: str) -> Dec:
|
|
161
|
+
return lit_dec(lit)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def money(lit: str, cur: str) -> Money:
|
|
165
|
+
d = dec(lit)
|
|
166
|
+
return Money(d.u, d.s, cur)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def rat_term(lit) -> Rat:
|
|
170
|
+
"""Литерал Rational в вычислимом терме вопроса `term`: разбор оракула
|
|
171
|
+
(`values.from_literal_term`) — запись `n/d` с целыми частями по лексике
|
|
172
|
+
§208 (E-0203), сократимая и со знаком в знаменателе нормализуется; не
|
|
173
|
+
`n/d` — TYPE_ERROR, знаменатель 0 — DIVISION_BY_ZERO. Каноничность §168.1
|
|
174
|
+
требуется от записей дела и вопроса `truth`, не от операнда (ERR-E0005)."""
|
|
175
|
+
return lit_rat_term(lit)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def qty(lit: str, unit: str) -> Qty:
|
|
179
|
+
"""Величина §49: значение и тег единицы как записаны."""
|
|
180
|
+
d = dec(lit)
|
|
181
|
+
return Qty(d.u, d.s, str(unit))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def rat(lit) -> Rat:
|
|
185
|
+
"""Каноническая запись `n/d` (§168.1): иная — названный отказ, а не молчаливое
|
|
186
|
+
сокращение — оракул хранит запись как есть, и сокращённая дала бы другой ответ."""
|
|
187
|
+
text = lit_text(lit)
|
|
188
|
+
num, sep, den = text.partition("/")
|
|
189
|
+
if not sep or not (_FRACTION_PART_LEXIS.match(num) and _FRACTION_PART_LEXIS.match(den)):
|
|
190
|
+
raise Refused("§168.1: запись Rational не n/d: " + text)
|
|
191
|
+
n, d = int(num), int(den)
|
|
192
|
+
if d <= 0 or _gcd(n, d) != 1 or f"{n}/{d}" != text:
|
|
193
|
+
raise Refused("§168.1: неканоническая запись Rational: " + text)
|
|
194
|
+
return Rat(n, d)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# --- сырое поле литерала §208 ----------------------------------------------
|
|
198
|
+
# Правило одно на три цели (errata E-0197, E-0202, E-0203): сырое поле литерала
|
|
199
|
+
# читается НЕ средствами языка цели, а по грамматике §208. `int()`, `Decimal()`
|
|
200
|
+
# и `Fraction()` Python принимают пробелы, `_`, юникодную цифру и `Infinity`;
|
|
201
|
+
# `Number()`/`parseFloat` JavaScript принимают `1e2` и `Infinity` и теряют
|
|
202
|
+
# точность на i64; `strconv`/`math/big` Go имеют свои допуски. Норма у всех
|
|
203
|
+
# одна, поэтому алфавит назван явно и одинаково.
|
|
204
|
+
|
|
205
|
+
#: §18/§208 (errata E-0203): лексика ЗАПИСИ десятичного значения — дословный
|
|
206
|
+
#: порт `lawref/values.py::_DECIMAL_LEXIS`. Экспонента, ведущие и хвостовые
|
|
207
|
+
#: нули, знак `+`, `.5` и `5.` остаются законной записью входа и нормализуются;
|
|
208
|
+
#: сужены ровно пробельный символ, `_`, не-ASCII цифра и `Infinity`/`NaN`.
|
|
209
|
+
#: Якорь `\Z`, а не `$`: Python-овый `$` пропускает хвостовой перевод строки.
|
|
210
|
+
_DECIMAL_LEXIS = re.compile(r"^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?\Z")
|
|
211
|
+
|
|
212
|
+
#: §35/§208 (errata E-0203): лексика ЦЕЛОГО внутри записи `<n>/<d>`.
|
|
213
|
+
_FRACTION_PART_LEXIS = re.compile(r"^[+-]?[0-9]+\Z")
|
|
214
|
+
|
|
215
|
+
#: §208 (errata E-0202): знаковый 64-битный носитель JSON-числа канона.
|
|
216
|
+
I64_MIN, I64_MAX = -(2 ** 63), 2 ** 63 - 1
|
|
217
|
+
|
|
218
|
+
#: Канон §208 отвергает JSON-float и целое вне i64 ОТКАЗОМ СОБРАТЬ ДОКУМЕНТ
|
|
219
|
+
#: (error-объект §172), а не issue. У порождённого модуля канала error-объекта
|
|
220
|
+
#: нет — профиль `core-executable` документа не строит, — поэтому тот же вход
|
|
221
|
+
#: даёт названный отказ раннера: «меньше ответа», а не другой ответ.
|
|
222
|
+
CANON_NUMBER = ("JSON-число вне канона §208 (только целое знакового 64-битного "
|
|
223
|
+
"диапазона) вне среза раннера")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def raw_lit(raw):
|
|
227
|
+
"""Вид JSON-носителя сырого поля литерала и его текст: `s` — JSON-строка,
|
|
228
|
+
`i` — целое JSON-число канона, `x` — любая другая форма (в том числе
|
|
229
|
+
отсутствие поля: схема `LiteralTerm` не требует `value`, E-0197)."""
|
|
230
|
+
if isinstance(raw, str):
|
|
231
|
+
return "s", raw
|
|
232
|
+
if isinstance(raw, bool):
|
|
233
|
+
return "x", ""
|
|
234
|
+
if isinstance(raw, int):
|
|
235
|
+
if not (I64_MIN <= raw <= I64_MAX):
|
|
236
|
+
raise Refused(CANON_NUMBER)
|
|
237
|
+
return "i", str(raw)
|
|
238
|
+
if isinstance(raw, float):
|
|
239
|
+
raise Refused(CANON_NUMBER)
|
|
240
|
+
return "x", ""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def lit_text(raw) -> str:
|
|
244
|
+
"""Поле, чей канон §208 — JSON-строка (`Text`, `Date`, `Instant`,
|
|
245
|
+
`Rational`, валюта, единица). Порт `values._json_string`: до E-0197 цели
|
|
246
|
+
приводили к строке ЛЮБОЙ JSON, и литерал без `value` выходил значением
|
|
247
|
+
`"undefined"` у ts и `""` у go — расхождение в результате, не в тексте."""
|
|
248
|
+
kind, text = raw_lit(raw)
|
|
249
|
+
if kind != "s":
|
|
250
|
+
raise TermError("TYPE_ERROR")
|
|
251
|
+
return text
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def lit_int_text(raw) -> str:
|
|
255
|
+
"""Поле, чей канон §208 — целое JSON-число (`Integer`). Порт `values._json_int`."""
|
|
256
|
+
kind, text = raw_lit(raw)
|
|
257
|
+
if kind != "i":
|
|
258
|
+
raise TermError("TYPE_ERROR")
|
|
259
|
+
return text
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def lit_dec_text(raw) -> str:
|
|
263
|
+
"""Поле `Decimal`/`Money`/`Quantity`: строка по лексике §208 либо целое
|
|
264
|
+
JSON-число (запись числом остаётся законной — E-0203, долг п. 3)."""
|
|
265
|
+
kind, text = raw_lit(raw)
|
|
266
|
+
if kind == "i":
|
|
267
|
+
return text
|
|
268
|
+
if kind != "s" or not _DECIMAL_LEXIS.match(text):
|
|
269
|
+
raise TermError("TYPE_ERROR")
|
|
270
|
+
return text
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def dec_lexis(text: str) -> Dec:
|
|
274
|
+
"""Точное десятичное из записи, уже прошедшей `_DECIMAL_LEXIS`: знак,
|
|
275
|
+
необязательная экспонента, `.5` и `5.`. Нормализация — как у
|
|
276
|
+
`Decimal(text)` оракула, чей канон §208 печатает non-exponent форму."""
|
|
277
|
+
body = text
|
|
278
|
+
neg = body[:1] == "-"
|
|
279
|
+
if body[:1] in ("+", "-"):
|
|
280
|
+
body = body[1:]
|
|
281
|
+
mant, sep, exp_text = body.partition("e")
|
|
282
|
+
if not sep:
|
|
283
|
+
mant, sep, exp_text = body.partition("E")
|
|
284
|
+
exp = int(exp_text) if sep else 0
|
|
285
|
+
int_part, _, frac = mant.partition(".")
|
|
286
|
+
digits = (int_part + frac) or "0"
|
|
287
|
+
scale = len(frac) - exp
|
|
288
|
+
unscaled = int(digits)
|
|
289
|
+
if unscaled == 0 and _b.abs(scale) > 4096:
|
|
290
|
+
return Dec(0, 0)
|
|
291
|
+
canonical_scale = scale - (len(digits) - len(digits.rstrip("0")))
|
|
292
|
+
if unscaled != 0 and (canonical_scale > 4096 or scale < -4096):
|
|
293
|
+
raise Refused("DECIMAL_EXPANSION_UNAVAILABLE: E-0320 limit 4096")
|
|
294
|
+
if scale < 0:
|
|
295
|
+
unscaled *= 10 ** (-scale)
|
|
296
|
+
scale = 0
|
|
297
|
+
return Dec(-unscaled if neg else unscaled, scale)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def lit_dec(raw) -> Dec:
|
|
301
|
+
return dec_lexis(lit_dec_text(raw))
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def lit_int(raw) -> Dec:
|
|
305
|
+
return Dec(int(lit_int_text(raw)), 0)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def lit_money(raw, cur) -> Money:
|
|
309
|
+
d = lit_dec(raw)
|
|
310
|
+
return Money(d.u, d.s, lit_text(cur))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def lit_qty(raw, unit) -> Qty:
|
|
314
|
+
d = lit_dec(raw)
|
|
315
|
+
return Qty(d.u, d.s, lit_text(unit))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def lit_rat_term(raw) -> Rat:
|
|
319
|
+
"""Литерал `Rational` вычислимого терма: порт ветви `RATIONAL`
|
|
320
|
+
`values.from_literal_term` в редакции E-0203 — `<n>/<d>` с частями по
|
|
321
|
+
`_FRACTION_PART_LEXIS`, сократимая запись нормализуется."""
|
|
322
|
+
parts = lit_text(raw).split("/")
|
|
323
|
+
if len(parts) != 2 or not (_FRACTION_PART_LEXIS.match(parts[0])
|
|
324
|
+
and _FRACTION_PART_LEXIS.match(parts[1])):
|
|
325
|
+
raise TermError("TYPE_ERROR")
|
|
326
|
+
n, d = int(parts[0]), int(parts[1])
|
|
327
|
+
if d == 0:
|
|
328
|
+
raise TermError("DIVISION_BY_ZERO")
|
|
329
|
+
return Rat(n, d)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# --- виды ------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
def _is_num(v) -> bool:
|
|
335
|
+
return isinstance(v, Dec)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _same(a, b, op: str) -> None:
|
|
339
|
+
"""§48/§58: сложение, вычитание, min/max требуют совпадения видов;
|
|
340
|
+
у денег — и валюты, у величин — и единицы (DECISION-0256: ошибка значения)."""
|
|
341
|
+
if isinstance(a, Qty) or isinstance(b, Qty):
|
|
342
|
+
if not (isinstance(a, Qty) and isinstance(b, Qty)):
|
|
343
|
+
raise TermError("TYPE_ERROR")
|
|
344
|
+
if a.unit != b.unit:
|
|
345
|
+
raise TermError("DIMENSION_MISMATCH")
|
|
346
|
+
return
|
|
347
|
+
if not (_is_num(a) and _is_num(b)) or isinstance(a, Money) != isinstance(b, Money):
|
|
348
|
+
raise Refused(f"§58: виды операндов не совпадают: {op}")
|
|
349
|
+
if isinstance(a, Money) and a.cur != b.cur:
|
|
350
|
+
raise Refused(f"§58: разные валюты: {op}")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _bounded_coefficient(u, s=0):
|
|
354
|
+
if u < -(1 << 127) or u >= (1 << 127) or (u and s > 4096):
|
|
355
|
+
raise Refused("NUMERIC_LIMIT_UNAVAILABLE: current i64/i128 arithmetic boundary")
|
|
356
|
+
return u
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _pair(a, b):
|
|
360
|
+
s = _b.max(a.s, b.s)
|
|
361
|
+
x, y = a.u * 10 ** (s - a.s), b.u * 10 ** (s - b.s)
|
|
362
|
+
_bounded_coefficient(x, s); _bounded_coefficient(y, s)
|
|
363
|
+
return x, y, s
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _make(u: int, s: int, like) -> Dec:
|
|
367
|
+
_bounded_coefficient(u, s)
|
|
368
|
+
if isinstance(like, Qty):
|
|
369
|
+
return Qty(u, s, like.unit)
|
|
370
|
+
return Money(u, s, like.cur) if isinstance(like, Money) else Dec(u, s)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _integral(v) -> bool:
|
|
374
|
+
"""Целое значение: дробная часть нулевая. Статика плана гарантирует вид
|
|
375
|
+
Integer у операндов mod/pow, здесь — защита в глубину."""
|
|
376
|
+
return _is_num(v) and not isinstance(v, (Money, Qty)) and v.u % (10 ** v.s) == 0
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _int(v) -> int:
|
|
380
|
+
return v.u // (10 ** v.s)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# --- арифметика §58 ---------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
def add(a, b) -> Dec:
|
|
386
|
+
if isinstance(a, Rat) or isinstance(b, Rat):
|
|
387
|
+
return _rat_pair(a, b, "add")
|
|
388
|
+
_same(a, b, "add")
|
|
389
|
+
x, y, s = _pair(a, b)
|
|
390
|
+
return _make(x + y, s, a)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def sub(a, b) -> Dec:
|
|
394
|
+
if isinstance(a, Rat) or isinstance(b, Rat):
|
|
395
|
+
return _rat_pair(a, b, "sub")
|
|
396
|
+
_same(a, b, "sub")
|
|
397
|
+
x, y, s = _pair(a, b)
|
|
398
|
+
return _make(x - y, s, a)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def mul(a, b) -> Dec:
|
|
402
|
+
"""Правило скаляра §49: безразмерное масштабирует размерное; масштаб
|
|
403
|
+
результата — сумма масштабов. Два размерных не перемножаются."""
|
|
404
|
+
if not (_is_num(a) and _is_num(b)):
|
|
405
|
+
raise Refused("§58: виды операндов не совпадают: mul")
|
|
406
|
+
if isinstance(a, (Money, Qty)) and isinstance(b, (Money, Qty)):
|
|
407
|
+
raise Refused("§58: произведение двух размерных величин не определено")
|
|
408
|
+
like = a if isinstance(a, (Money, Qty)) else b
|
|
409
|
+
return _make(a.u * b.u, a.s + b.s, like)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _gcd(a: int, b: int) -> int:
|
|
413
|
+
a, b = _b.abs(a), _b.abs(b)
|
|
414
|
+
while b:
|
|
415
|
+
a, b = b, a % b
|
|
416
|
+
return a
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _exact(num: int, den: int):
|
|
420
|
+
"""Точное десятичное частное num/den, если знаменатель 10-гладкий (§18),
|
|
421
|
+
иначе None. Масштаб — наименьший точный."""
|
|
422
|
+
g = _gcd(num, den) or 1
|
|
423
|
+
num, den = num // g, den // g
|
|
424
|
+
if den < 0:
|
|
425
|
+
num, den = -num, -den
|
|
426
|
+
twos = fives = 0
|
|
427
|
+
while den % 2 == 0:
|
|
428
|
+
den //= 2
|
|
429
|
+
twos += 1
|
|
430
|
+
while den % 5 == 0:
|
|
431
|
+
den //= 5
|
|
432
|
+
fives += 1
|
|
433
|
+
if den != 1:
|
|
434
|
+
return None
|
|
435
|
+
k = _b.max(twos, fives)
|
|
436
|
+
return num * 2 ** (k - twos) * 5 ** (k - fives), k
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def div(a, b) -> Dec:
|
|
440
|
+
"""Таблица делимых §58 в срезе: Decimal/Decimal, Money/Decimal,
|
|
441
|
+
Money/Money одной валюты. Integer/Integer даёт Rational — вне среза,
|
|
442
|
+
отвергается планом статически."""
|
|
443
|
+
if not (_is_num(a) and _is_num(b)):
|
|
444
|
+
raise Refused("§58: деление вне таблицы делимых")
|
|
445
|
+
if b.u == 0:
|
|
446
|
+
raise Refused("деление на ноль — evaluation error (§58)")
|
|
447
|
+
if isinstance(a, Money) and isinstance(b, Money):
|
|
448
|
+
if a.cur != b.cur:
|
|
449
|
+
raise Refused("CURRENCY_MISMATCH: §58 делит Money на Money только при тождественной валюте")
|
|
450
|
+
like = None
|
|
451
|
+
elif isinstance(a, Money):
|
|
452
|
+
like = a
|
|
453
|
+
elif isinstance(b, Money):
|
|
454
|
+
raise Refused("§58: деление вне таблицы делимых")
|
|
455
|
+
else:
|
|
456
|
+
like = None
|
|
457
|
+
q = _exact(a.u * 10 ** b.s, b.u * 10 ** a.s)
|
|
458
|
+
if q is None:
|
|
459
|
+
if like is not None:
|
|
460
|
+
raise Refused("INEXACT_DIVISION: округление только явной политикой (§50/§18)")
|
|
461
|
+
raise Refused("§58: частное не 10-гладкое — Rational вне среза")
|
|
462
|
+
u, s = q
|
|
463
|
+
return Money(u, s, like.cur) if like is not None else Dec(u, s)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def mod(a, b) -> Dec:
|
|
467
|
+
"""Евклидов остаток §58: 0 <= r < |b| при любых знаках операндов."""
|
|
468
|
+
if not (_integral(a) and _integral(b)):
|
|
469
|
+
raise Refused("§58: mod определён только для Integer")
|
|
470
|
+
y = _int(b)
|
|
471
|
+
if y == 0:
|
|
472
|
+
raise Refused("остаток от деления на ноль — evaluation error (§58)")
|
|
473
|
+
return Dec(_int(a) % _b.abs(y), 0)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# --- Rational §35/§58 и арифметика по видам (DECISION-0252) ------------------
|
|
477
|
+
|
|
478
|
+
def _rat_pair(a, b, op: str):
|
|
479
|
+
"""add/sub/min/max над Rational: оба операнда Rational, иначе TYPE_ERROR §48."""
|
|
480
|
+
if not (isinstance(a, Rat) and isinstance(b, Rat)):
|
|
481
|
+
raise TermError("TYPE_ERROR")
|
|
482
|
+
if op == "add":
|
|
483
|
+
return Rat(a.n * b.d + b.n * a.d, a.d * b.d)
|
|
484
|
+
if op == "sub":
|
|
485
|
+
return Rat(a.n * b.d - b.n * a.d, a.d * b.d)
|
|
486
|
+
if op == "min":
|
|
487
|
+
return a if a.n * b.d <= b.n * a.d else b
|
|
488
|
+
return a if a.n * b.d >= b.n * a.d else b
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def kind_of(v) -> str:
|
|
492
|
+
"""Вид значения динамического терма (DECISION-0252 п. 5): `r` Rational, `d` Decimal."""
|
|
493
|
+
return "r" if isinstance(v, Rat) else "d"
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
_KINDS = ("i", "d", "m", "r", "q")
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _resolve(v, k: str) -> str:
|
|
500
|
+
"""Статический вид плана → вид значения; `n` — Decimal либо Rational по классу."""
|
|
501
|
+
if k == "n":
|
|
502
|
+
k = "r" if isinstance(v, Rat) else "d"
|
|
503
|
+
if k not in _KINDS or (k == "r") != isinstance(v, Rat) or (k == "m") != isinstance(v, Money) \
|
|
504
|
+
or (k == "q") != isinstance(v, Qty):
|
|
505
|
+
raise Refused("вид операнда расходится с планом: " + str(k))
|
|
506
|
+
return k
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _exact_of(num: int, den: int, like):
|
|
510
|
+
"""Точное десятичное num/den (§18) видом `like` (Money либо Dec) или None."""
|
|
511
|
+
q = _exact(num, den)
|
|
512
|
+
if q is None:
|
|
513
|
+
return None
|
|
514
|
+
if isinstance(like, Qty):
|
|
515
|
+
return Qty(q[0], q[1], like.unit)
|
|
516
|
+
return Money(q[0], q[1], like.cur) if isinstance(like, Money) else Dec(q[0], q[1])
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _arith(op: str, a, b, lk: str, rk: str):
|
|
520
|
+
"""Таблица §58 `values.binary` по видам операндов (DECISION-0252 п. 3–4).
|
|
521
|
+
|
|
522
|
+
Виды — из плана: `i` Integer, `d` Decimal, `m` Money, `r` Rational, `n` —
|
|
523
|
+
Decimal либо Rational по значению. Integer и Decimal рантайм по значению
|
|
524
|
+
не различает, поэтому пара решается видами, а не классами. Ошибка
|
|
525
|
+
значения — TermError с кодом оракула, в его порядке проверок."""
|
|
526
|
+
l, r = _resolve(a, lk), _resolve(b, rk)
|
|
527
|
+
pair = {l, r}
|
|
528
|
+
if op in ("add", "sub"):
|
|
529
|
+
if l == r == "m" and a.cur != b.cur:
|
|
530
|
+
raise TermError("CURRENCY_MISMATCH")
|
|
531
|
+
if l == r == "q" and a.unit != b.unit:
|
|
532
|
+
raise TermError("DIMENSION_MISMATCH") # §49, T023
|
|
533
|
+
if l != r:
|
|
534
|
+
raise TermError("TYPE_ERROR")
|
|
535
|
+
if l == "r":
|
|
536
|
+
return _rat_pair(a, b, op)
|
|
537
|
+
return add(a, b) if op == "add" else sub(a, b)
|
|
538
|
+
if op == "mul":
|
|
539
|
+
if l == r == "i" or ("i" in pair and pair & {"m", "d", "q"}):
|
|
540
|
+
return mul(a, b)
|
|
541
|
+
if pair == {"i", "r"}:
|
|
542
|
+
q, k = (a, b) if l == "r" else (b, a)
|
|
543
|
+
return Rat(q.n * _int(k), q.d)
|
|
544
|
+
if "i" in pair:
|
|
545
|
+
raise TermError("TYPE_ERROR")
|
|
546
|
+
if l == r == "r":
|
|
547
|
+
return Rat(a.n * b.n, a.d * b.d)
|
|
548
|
+
if pair == {"m", "r"}:
|
|
549
|
+
# E-0085: доля масштабирует деньги точно; 10-гладкость — после сокращения.
|
|
550
|
+
m, q = (a, b) if l == "m" else (b, a)
|
|
551
|
+
out = _exact_of(m.u * q.n, 10 ** m.s * q.d, m)
|
|
552
|
+
if out is None:
|
|
553
|
+
raise TermError("INEXACT_DIVISION")
|
|
554
|
+
return out
|
|
555
|
+
if "r" in pair or pair in ({"m"}, {"q"}, {"m", "q"}):
|
|
556
|
+
raise TermError("TYPE_ERROR")
|
|
557
|
+
return mul(a, b)
|
|
558
|
+
if op == "div":
|
|
559
|
+
if l == r == "r":
|
|
560
|
+
if b.n == 0:
|
|
561
|
+
raise TermError("DIVISION_BY_ZERO")
|
|
562
|
+
return Rat(a.n * b.d, a.d * b.n)
|
|
563
|
+
if l == r == "i":
|
|
564
|
+
if _int(b) == 0:
|
|
565
|
+
raise TermError("DIVISION_BY_ZERO")
|
|
566
|
+
return Rat(_int(a), _int(b))
|
|
567
|
+
if "r" in pair:
|
|
568
|
+
raise TermError("TYPE_ERROR")
|
|
569
|
+
num, den = a.u * 10 ** b.s, b.u * 10 ** a.s
|
|
570
|
+
if l == r == "m":
|
|
571
|
+
# E-0083: частное сумм одной валюты безразмерно — Decimal либо Rational.
|
|
572
|
+
if a.cur != b.cur:
|
|
573
|
+
raise TermError("CURRENCY_MISMATCH")
|
|
574
|
+
if b.u == 0:
|
|
575
|
+
raise TermError("DIVISION_BY_ZERO")
|
|
576
|
+
out = _exact_of(num, den, None)
|
|
577
|
+
return out if out is not None else Rat(num, den)
|
|
578
|
+
if l == r == "q":
|
|
579
|
+
# E-0068: частное одной единицы безразмерно; разные единицы — TYPE_ERROR раньше нуля.
|
|
580
|
+
if a.unit != b.unit:
|
|
581
|
+
raise TermError("TYPE_ERROR")
|
|
582
|
+
if b.u == 0:
|
|
583
|
+
raise TermError("DIVISION_BY_ZERO")
|
|
584
|
+
out = _exact_of(num, den, None)
|
|
585
|
+
return out if out is not None else Rat(num, den)
|
|
586
|
+
if (l, r) not in (("d", "d"), ("m", "d"), ("q", "d")):
|
|
587
|
+
raise TermError("TYPE_ERROR")
|
|
588
|
+
if b.u == 0:
|
|
589
|
+
raise TermError("DIVISION_BY_ZERO")
|
|
590
|
+
out = _exact_of(num, den, a)
|
|
591
|
+
if out is not None:
|
|
592
|
+
return out
|
|
593
|
+
if l == "d":
|
|
594
|
+
return Rat(num, den)
|
|
595
|
+
raise TermError("INEXACT_DIVISION")
|
|
596
|
+
if op == "mod":
|
|
597
|
+
if l == r == "i":
|
|
598
|
+
y = _int(b)
|
|
599
|
+
if y == 0:
|
|
600
|
+
raise TermError("DIVISION_BY_ZERO")
|
|
601
|
+
return Dec(_int(a) % _b.abs(y), 0)
|
|
602
|
+
raise TermError("TYPE_ERROR")
|
|
603
|
+
raise Refused("операция вне среза рантайма: " + str(op))
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def arith(op: str, a, b, lk: str, rk: str):
|
|
607
|
+
for v in (a, b):
|
|
608
|
+
if isinstance(v, Dec):
|
|
609
|
+
_bounded_coefficient(v.u, v.s)
|
|
610
|
+
out = _arith(op, a, b, lk, rk)
|
|
611
|
+
if isinstance(out, Dec):
|
|
612
|
+
_bounded_coefficient(out.u, out.s)
|
|
613
|
+
if lk == rk == "i" and op in ("add", "sub", "mul", "mod") and not (-(1 << 63) <= out.u < (1 << 63)):
|
|
614
|
+
raise Refused("NUMERIC_LIMIT_UNAVAILABLE: current i64/i128 arithmetic boundary")
|
|
615
|
+
return out
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def min(a, b) -> Dec: # noqa: A001
|
|
619
|
+
if isinstance(a, Rat) or isinstance(b, Rat):
|
|
620
|
+
return _rat_pair(a, b, "min")
|
|
621
|
+
_same(a, b, "min")
|
|
622
|
+
x, y, _ = _pair(a, b)
|
|
623
|
+
return a if x <= y else b
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def max(a, b) -> Dec: # noqa: A001
|
|
627
|
+
if isinstance(a, Rat) or isinstance(b, Rat):
|
|
628
|
+
return _rat_pair(a, b, "max")
|
|
629
|
+
_same(a, b, "max")
|
|
630
|
+
x, y, _ = _pair(a, b)
|
|
631
|
+
return a if x >= y else b
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def abs(v) -> Dec: # noqa: A001
|
|
635
|
+
if isinstance(v, Dec) and v.s == 0 and v.u == -(1 << 63):
|
|
636
|
+
raise Refused("NUMERIC_LIMIT_UNAVAILABLE: current i64/i128 arithmetic boundary")
|
|
637
|
+
if isinstance(v, Rat):
|
|
638
|
+
return Rat(_b.abs(v.n), v.d)
|
|
639
|
+
if not _is_num(v):
|
|
640
|
+
raise Refused("§58: abs — не числовой вид")
|
|
641
|
+
return _make(v.u if v.u >= 0 else -v.u, v.s, v)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
POW_EXP_LIMIT = 4096
|
|
645
|
+
POW_MIN, POW_MAX = -(2 ** 63), 2 ** 63 - 1
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def pow(base, exp) -> Dec: # noqa: A001
|
|
649
|
+
"""§58 (E-0075): только Integer pow Integer; порядок проверок — знак,
|
|
650
|
+
предел, вычисление, диапазон — часть контракта."""
|
|
651
|
+
if not (_integral(base) and _integral(exp)):
|
|
652
|
+
raise Refused("§58: pow определён только для Integer")
|
|
653
|
+
e = _int(exp)
|
|
654
|
+
if e < 0:
|
|
655
|
+
raise Refused("§58: pow — показатель отрицателен")
|
|
656
|
+
if e > POW_EXP_LIMIT:
|
|
657
|
+
raise Refused("RESOURCE_LIMIT: pow — показатель превышает 4096")
|
|
658
|
+
r = _int(base) ** e
|
|
659
|
+
if r < POW_MIN or r > POW_MAX:
|
|
660
|
+
raise Refused("RESOURCE_LIMIT: pow — результат вне 64-битного диапазона")
|
|
661
|
+
return Dec(r, 0)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def round(m, digits, mode: str) -> Dec: # noqa: A001
|
|
665
|
+
if mode != "HALF_UP":
|
|
666
|
+
raise Refused("§50: режим округления не реализован: " + mode)
|
|
667
|
+
if not _is_num(m):
|
|
668
|
+
raise Refused("§50: round — не числовой вид")
|
|
669
|
+
n = digits if isinstance(digits, int) else digits.u // 10 ** digits.s
|
|
670
|
+
if m.s <= n:
|
|
671
|
+
return _make(m.u * 10 ** (n - m.s), n, m)
|
|
672
|
+
drop = 10 ** (m.s - n)
|
|
673
|
+
neg = m.u < 0
|
|
674
|
+
a = -m.u if neg else m.u
|
|
675
|
+
q, r = divmod(a, drop)
|
|
676
|
+
up = q + 1 if r * 2 >= drop else q # HALF_UP — от нуля
|
|
677
|
+
return _make(-up if neg else up, n, m)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
# --- политика округления §50 (DECISION-0259) ---------------------------------
|
|
681
|
+
|
|
682
|
+
ROUNDING_MODES = ("CEILING", "DOWN", "FLOOR", "HALF_DOWN", "HALF_EVEN", "HALF_UP", "UP")
|
|
683
|
+
DIV_ROUND_MAX_PRECISION = 34
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _round_int(num: int, den: int, mode: str) -> int:
|
|
687
|
+
"""Порт `values._round_fraction` на целых: num/den (den > 0) к целому по
|
|
688
|
+
режиму §50; `2*tail` против знаменателя, ветви в порядке оракула."""
|
|
689
|
+
negative = num < 0
|
|
690
|
+
head, tail = divmod(-num if negative else num, den)
|
|
691
|
+
up = {
|
|
692
|
+
"HALF_UP": 2 * tail >= den,
|
|
693
|
+
"HALF_EVEN": 2 * tail > den or (2 * tail == den and head % 2 == 1),
|
|
694
|
+
"HALF_DOWN": 2 * tail > den,
|
|
695
|
+
"UP": tail > 0,
|
|
696
|
+
"DOWN": False,
|
|
697
|
+
"CEILING": tail > 0 and not negative,
|
|
698
|
+
"FLOOR": tail > 0 and negative,
|
|
699
|
+
}[mode]
|
|
700
|
+
if up:
|
|
701
|
+
head += 1
|
|
702
|
+
return -head if negative else head
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _policy_scale(p, pk: str) -> int:
|
|
706
|
+
"""Точность политики: Integer либо целый Decimal, вид — из плана. Дробная
|
|
707
|
+
точность — TYPE_ERROR; проверка на целом до любого приведения."""
|
|
708
|
+
if pk not in ("i", "d"):
|
|
709
|
+
raise Refused("вид точности расходится с планом: " + str(pk))
|
|
710
|
+
_resolve(p, pk)
|
|
711
|
+
if p.u % 10 ** p.s:
|
|
712
|
+
raise TermError("TYPE_ERROR")
|
|
713
|
+
return p.u // 10 ** p.s
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _policy_mode(mode) -> str:
|
|
717
|
+
if not isinstance(mode, str) or mode not in ROUNDING_MODES:
|
|
718
|
+
raise TermError("TYPE_ERROR")
|
|
719
|
+
return mode
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def round_mode(v, p, mode, vk: str, pk: str):
|
|
723
|
+
"""§50 `round` с проверкой политики (DECISION-0259 п. 4, 7): порядок
|
|
724
|
+
`values.round_value` — вид, точность (целое >= 0), режим. Integer — тождество
|
|
725
|
+
(E-0110); точность не короче дробной части — значение то же (E-0184)."""
|
|
726
|
+
k = _resolve(v, vk)
|
|
727
|
+
if k == "r":
|
|
728
|
+
raise TermError("TYPE_ERROR")
|
|
729
|
+
scale = _policy_scale(p, pk)
|
|
730
|
+
if scale < 0:
|
|
731
|
+
raise TermError("TYPE_ERROR")
|
|
732
|
+
mode = _policy_mode(mode)
|
|
733
|
+
if k == "i" or scale >= v.s:
|
|
734
|
+
return v
|
|
735
|
+
return _make(_round_int(v.u, 10 ** (v.s - scale), mode), scale, v)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def div_round(a, b, p, mode, ak: str, bk: str, pk: str):
|
|
739
|
+
"""§50/§58 `div_round` (E-0036, E-0170; DECISION-0259 п. 4, 7): пара →
|
|
740
|
+
валюта → ноль → точность 0..34 → режим. Частное точное, округление всегда;
|
|
741
|
+
Money/Money — доля Decimal без тега."""
|
|
742
|
+
l, r = _resolve(a, ak), _resolve(b, bk)
|
|
743
|
+
if (l, r) not in (("m", "d"), ("q", "d"), ("d", "d"), ("m", "m")):
|
|
744
|
+
raise TermError("TYPE_ERROR")
|
|
745
|
+
if l == r == "m" and a.cur != b.cur:
|
|
746
|
+
raise TermError("CURRENCY_MISMATCH")
|
|
747
|
+
if b.u == 0:
|
|
748
|
+
raise TermError("DIVISION_BY_ZERO")
|
|
749
|
+
scale = _policy_scale(p, pk)
|
|
750
|
+
if not 0 <= scale <= DIV_ROUND_MAX_PRECISION:
|
|
751
|
+
raise TermError("TYPE_ERROR")
|
|
752
|
+
mode = _policy_mode(mode)
|
|
753
|
+
num, den = a.u * 10 ** (b.s + scale), b.u * 10 ** a.s
|
|
754
|
+
if den < 0:
|
|
755
|
+
num, den = -num, -den
|
|
756
|
+
q = _round_int(num, den, mode)
|
|
757
|
+
return Dec(q, scale) if r == "m" else _make(q, scale, a)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
# --- сравнение и равенство ---------------------------------------------------
|
|
761
|
+
|
|
762
|
+
def cmp(a, op: str, b) -> bool: # noqa: A001
|
|
763
|
+
if isinstance(a, bool) or isinstance(b, bool):
|
|
764
|
+
# §57 (DECISION-0266): Boolean сравнимо только на равенство и только с Boolean.
|
|
765
|
+
same_bool = isinstance(a, bool) and isinstance(b, bool) and a == b
|
|
766
|
+
if op == "eq":
|
|
767
|
+
return same_bool
|
|
768
|
+
if op == "ne":
|
|
769
|
+
return not same_bool
|
|
770
|
+
raise Refused("§57: вид Boolean не упорядочен")
|
|
771
|
+
if isinstance(a, Rat) or isinstance(b, Rat):
|
|
772
|
+
# §57/§48 (DECISION-0252 п. 6): Rational сравнивается только с Rational;
|
|
773
|
+
# иной вид — TYPE_ERROR оракула, а не false.
|
|
774
|
+
if not (isinstance(a, Rat) and isinstance(b, Rat)):
|
|
775
|
+
raise TermError("TYPE_ERROR")
|
|
776
|
+
x, y = a.n * b.d, b.n * a.d
|
|
777
|
+
table = {"lt": x < y, "le": x <= y, "gt": x > y, "ge": x >= y,
|
|
778
|
+
"eq": x == y, "ne": x != y}
|
|
779
|
+
if op not in table:
|
|
780
|
+
raise Refused("сравнение вне среза рантайма: " + op)
|
|
781
|
+
return table[op]
|
|
782
|
+
if isinstance(a, Qty) or isinstance(b, Qty):
|
|
783
|
+
# §49/§57 (DECISION-0256 п. 5): величина — только с величиной той же единицы.
|
|
784
|
+
_same(a, b, "cmp")
|
|
785
|
+
x, y, _ = _pair(a, b)
|
|
786
|
+
table = {"lt": x < y, "le": x <= y, "gt": x > y, "ge": x >= y,
|
|
787
|
+
"eq": x == y, "ne": x != y}
|
|
788
|
+
if op not in table:
|
|
789
|
+
raise Refused("сравнение вне среза рантайма: " + op)
|
|
790
|
+
return table[op]
|
|
791
|
+
if isinstance(a, Sym) or isinstance(b, Sym):
|
|
792
|
+
if op == "eq":
|
|
793
|
+
return isinstance(a, Sym) and isinstance(b, Sym) and a.id == b.id
|
|
794
|
+
if op == "ne":
|
|
795
|
+
return not (isinstance(a, Sym) and isinstance(b, Sym) and a.id == b.id)
|
|
796
|
+
raise Refused("§57: вид enum не упорядочен")
|
|
797
|
+
if isinstance(a, Instant) or isinstance(b, Instant):
|
|
798
|
+
if isinstance(a, Instant) and isinstance(b, Instant):
|
|
799
|
+
x, y = _moment_pair(op, a, b)
|
|
800
|
+
table = {"lt": x < y, "le": x <= y, "gt": x > y, "ge": x >= y, "eq": x == y, "ne": x != y}
|
|
801
|
+
if op not in table:
|
|
802
|
+
raise Refused("сравнение вне среза рантайма: " + op)
|
|
803
|
+
return table[op]
|
|
804
|
+
# §57/§2.15: Date и Instant не сравниваются (DATE_INSTANT_MISMATCH оракула).
|
|
805
|
+
raise Refused("§57: Instant сравнивается только с Instant")
|
|
806
|
+
if isinstance(a, Date) or isinstance(b, Date):
|
|
807
|
+
if isinstance(a, Date) and isinstance(b, Date):
|
|
808
|
+
table = {"lt": a.v < b.v, "le": a.v <= b.v, "gt": a.v > b.v, "ge": a.v >= b.v,
|
|
809
|
+
"eq": a.v == b.v, "ne": a.v != b.v}
|
|
810
|
+
if op not in table:
|
|
811
|
+
raise Refused("сравнение вне среза рантайма: " + op)
|
|
812
|
+
return table[op]
|
|
813
|
+
if op == "eq":
|
|
814
|
+
return False
|
|
815
|
+
if op == "ne":
|
|
816
|
+
return True
|
|
817
|
+
raise Refused("§57: Date сравнивается только с Date")
|
|
818
|
+
if isinstance(a, str) or isinstance(b, str):
|
|
819
|
+
if op == "eq":
|
|
820
|
+
return a == b
|
|
821
|
+
if op == "ne":
|
|
822
|
+
return a != b
|
|
823
|
+
raise Refused("порядок на Text вне среза рантайма: " + op)
|
|
824
|
+
if isinstance(a, Money) != isinstance(b, Money):
|
|
825
|
+
raise Refused("§49: сравнение денег с безразмерным")
|
|
826
|
+
if isinstance(a, Money) and a.cur != b.cur:
|
|
827
|
+
raise Refused("§49: разные валюты")
|
|
828
|
+
x, y, _ = _pair(a, b)
|
|
829
|
+
table = {"lt": x < y, "le": x <= y, "gt": x > y, "ge": x >= y,
|
|
830
|
+
"eq": x == y, "ne": x != y}
|
|
831
|
+
if op not in table:
|
|
832
|
+
raise Refused("сравнение вне среза рантайма: " + op)
|
|
833
|
+
return table[op]
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def eq(a, b) -> bool:
|
|
837
|
+
if isinstance(a, bool) or isinstance(b, bool):
|
|
838
|
+
# DECISION-0266: Boolean равен только Boolean того же значения; bool —
|
|
839
|
+
# подкласс int в Python, поэтому ветвь стоит первой.
|
|
840
|
+
return isinstance(a, bool) and isinstance(b, bool) and a == b
|
|
841
|
+
if isinstance(a, Rat) or isinstance(b, Rat):
|
|
842
|
+
return isinstance(a, Rat) and isinstance(b, Rat) and a.n == b.n and a.d == b.d
|
|
843
|
+
if isinstance(a, Qty) or isinstance(b, Qty):
|
|
844
|
+
if not (isinstance(a, Qty) and isinstance(b, Qty)) or a.unit != b.unit:
|
|
845
|
+
return False
|
|
846
|
+
x, y, _ = _pair(a, b)
|
|
847
|
+
return x == y
|
|
848
|
+
if isinstance(a, Sym) or isinstance(b, Sym):
|
|
849
|
+
return isinstance(a, Sym) and isinstance(b, Sym) and a.id == b.id
|
|
850
|
+
if isinstance(a, Date) or isinstance(b, Date):
|
|
851
|
+
return isinstance(a, Date) and isinstance(b, Date) and a.v == b.v
|
|
852
|
+
if isinstance(a, Instant) or isinstance(b, Instant):
|
|
853
|
+
if not (isinstance(a, Instant) and isinstance(b, Instant)):
|
|
854
|
+
return False
|
|
855
|
+
x, y = _moment_pair("eq", a, b)
|
|
856
|
+
return x == y
|
|
857
|
+
if isinstance(a, str) or isinstance(b, str):
|
|
858
|
+
return a == b
|
|
859
|
+
if isinstance(a, Money) != isinstance(b, Money):
|
|
860
|
+
return False
|
|
861
|
+
if isinstance(a, Money) and a.cur != b.cur:
|
|
862
|
+
return False
|
|
863
|
+
x, y, _ = _pair(a, b)
|
|
864
|
+
return x == y
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _norm(d: Dec) -> str:
|
|
868
|
+
u, s = d.u, d.s
|
|
869
|
+
if u == 0:
|
|
870
|
+
return "0e-0"
|
|
871
|
+
while s > 0 and u % 10 == 0:
|
|
872
|
+
u //= 10
|
|
873
|
+
s -= 1
|
|
874
|
+
return f"{u}e-{s}"
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _esc(text: str) -> str:
|
|
878
|
+
"""Разделитель и экран внутри текста экранируются: иначе ["a|e:b","c"] и
|
|
879
|
+
["a","b|e:c"] склеивались в один ключ, и второй факт терялся."""
|
|
880
|
+
return text.replace("\\", "\\\\").replace("|", "\\|")
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def key(row) -> str:
|
|
884
|
+
out = []
|
|
885
|
+
for v in row:
|
|
886
|
+
if isinstance(v, str):
|
|
887
|
+
out.append("e:" + _esc(v))
|
|
888
|
+
elif isinstance(v, bool):
|
|
889
|
+
out.append("b:" + ("true" if v else "false"))
|
|
890
|
+
elif isinstance(v, Date):
|
|
891
|
+
out.append("t:" + v.v)
|
|
892
|
+
elif isinstance(v, Instant):
|
|
893
|
+
out.append("i:" + _esc(v.v))
|
|
894
|
+
elif isinstance(v, Sym):
|
|
895
|
+
out.append("c:" + _esc(v.id))
|
|
896
|
+
elif isinstance(v, Rat):
|
|
897
|
+
out.append(f"r:{v.n}/{v.d}")
|
|
898
|
+
elif isinstance(v, Qty):
|
|
899
|
+
out.append("q:" + _esc(v.unit) + ":" + _norm(v))
|
|
900
|
+
elif isinstance(v, Money):
|
|
901
|
+
out.append("m:" + v.cur + ":" + _norm(v))
|
|
902
|
+
else:
|
|
903
|
+
out.append("d:" + _norm(v))
|
|
904
|
+
return "|".join(out)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
class _Context:
|
|
908
|
+
"""Контекст одного вычисления (DECISION-0423 §2.3): правовое время дела,
|
|
909
|
+
политика срока §86, снимок календаря §85 и загруженный календарь. Живёт в
|
|
910
|
+
`contextvars`: `evaluate` открывает новый контекст на каждый вызов, и
|
|
911
|
+
параллельные вызовы из потоков его не делят."""
|
|
912
|
+
__slots__ = ("legal_time", "policy", "snapshot", "cal")
|
|
913
|
+
|
|
914
|
+
def __init__(self):
|
|
915
|
+
self.legal_time = ""
|
|
916
|
+
self.policy = None # DeadlinePolicy §86 дела либо None
|
|
917
|
+
self.snapshot = None # запись снимка плана (`calendars`) либо None
|
|
918
|
+
self.cal = None # загруженный календарь
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
_CONTEXT = _cv.ContextVar("law_codegen_context")
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def begin_context() -> None:
|
|
925
|
+
"""Новый пустой контекст вычисления в текущем `contextvars.Context`."""
|
|
926
|
+
_CONTEXT.set(_Context())
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def _ctx() -> _Context:
|
|
930
|
+
try:
|
|
931
|
+
return _CONTEXT.get()
|
|
932
|
+
except LookupError:
|
|
933
|
+
c = _Context()
|
|
934
|
+
_CONTEXT.set(c)
|
|
935
|
+
return c
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def set_legal_time(t: str) -> None:
|
|
939
|
+
_ctx().legal_time = t
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def legal_time() -> str:
|
|
943
|
+
t = _ctx().legal_time
|
|
944
|
+
if not t:
|
|
945
|
+
raise Refused("legalTime не подан делом")
|
|
946
|
+
return t
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
# --- календарь §84–§86 (DECISION-0245) ---------------------------------------
|
|
950
|
+
# Порт `lawref.values` и `lawref.calendar` дословно по порядку проверок: код
|
|
951
|
+
# ошибки — часть ответа (issue модели), а не текст.
|
|
952
|
+
|
|
953
|
+
import datetime as _dt
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
class TermError(Exception):
|
|
957
|
+
"""Ошибка вычисления терма §58 с кодом: модель превращает её в issue
|
|
958
|
+
правила (`support.term_error` оракула)."""
|
|
959
|
+
|
|
960
|
+
def __init__(self, code: str):
|
|
961
|
+
super().__init__(code)
|
|
962
|
+
self.code = code
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def set_calendar(policy, snapshot) -> None:
|
|
966
|
+
c = _ctx()
|
|
967
|
+
c.policy, c.snapshot, c.cal = policy, snapshot, None
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
_WEEKDAYS = ("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def _calendar():
|
|
974
|
+
"""`TermEnv.calendar`: ленивая загрузка; ошибка загрузки — код плана."""
|
|
975
|
+
c = _ctx()
|
|
976
|
+
if c.cal is None:
|
|
977
|
+
snap = c.snapshot
|
|
978
|
+
if snap is None or snap.get("error") == "MISSING_CALENDAR":
|
|
979
|
+
raise TermError("MISSING_CALENDAR")
|
|
980
|
+
if "error" in snap:
|
|
981
|
+
raise TermError(snap["error"])
|
|
982
|
+
c.cal = (_dt.date.fromisoformat(snap["start"]), _dt.date.fromisoformat(snap["endExclusive"]),
|
|
983
|
+
snap["weekly"], {_dt.date.fromisoformat(k): v for k, v in snap["overrides"].items()})
|
|
984
|
+
return c.cal
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _coverage(cal, day) -> None:
|
|
988
|
+
if not (cal[0] <= day < cal[1]):
|
|
989
|
+
raise TermError("CALENDAR_OUT_OF_RANGE")
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _working(cal, day) -> bool:
|
|
993
|
+
_coverage(cal, day)
|
|
994
|
+
if day in cal[3]:
|
|
995
|
+
return cal[3][day]
|
|
996
|
+
return cal[2][day.weekday()]
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def _previous_working(cal, day):
|
|
1000
|
+
_coverage(cal, day)
|
|
1001
|
+
current = day - _dt.timedelta(days=1)
|
|
1002
|
+
while True:
|
|
1003
|
+
_coverage(cal, current)
|
|
1004
|
+
if _working(cal, current):
|
|
1005
|
+
return current
|
|
1006
|
+
current -= _dt.timedelta(days=1)
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _next_working(cal, day):
|
|
1010
|
+
while not _working(cal, day):
|
|
1011
|
+
day += _dt.timedelta(days=1)
|
|
1012
|
+
_coverage(cal, day)
|
|
1013
|
+
return day
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def _last_day(year: int, month: int) -> int:
|
|
1017
|
+
if month == 12:
|
|
1018
|
+
return 31
|
|
1019
|
+
return (_dt.date(year, month + 1, 1) - _dt.timedelta(days=1)).day
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def _add_months(day, months: int, month_end: str):
|
|
1023
|
+
total = day.month - 1 + months
|
|
1024
|
+
year, month = day.year + total // 12, total % 12 + 1
|
|
1025
|
+
last = _last_day(year, month)
|
|
1026
|
+
if day.day <= last:
|
|
1027
|
+
return _dt.date(year, month, day.day)
|
|
1028
|
+
if month_end == "last_day_of_month":
|
|
1029
|
+
return _dt.date(year, month, last)
|
|
1030
|
+
raise TermError("DEADLINE_POLICY_INVALID")
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def _needs_calendar(unit: str, policy) -> bool:
|
|
1034
|
+
if unit not in ("calendar_month", "calendar_year", "calendar_week"):
|
|
1035
|
+
return True
|
|
1036
|
+
policy = policy or {}
|
|
1037
|
+
return policy.get("roll") != "no_roll" or policy.get("include_end") is not True
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def _deadline(cal, after, count: int, policy, after_time, unit: str, calendar_day_step: bool = False):
|
|
1041
|
+
"""Порт `calendar.compute_deadline` без приостановлений (терм их не несёт)."""
|
|
1042
|
+
if policy is None:
|
|
1043
|
+
raise TermError("MISSING_POLICY")
|
|
1044
|
+
if set(policy) - {"start_count", "include_end", "roll", "cutoff", "month_end"}:
|
|
1045
|
+
raise TermError("DEADLINE_POLICY_INVALID")
|
|
1046
|
+
start_count, roll = policy.get("start_count"), policy.get("roll")
|
|
1047
|
+
include_end, cutoff = policy.get("include_end"), policy.get("cutoff")
|
|
1048
|
+
if start_count not in ("same_day", "next_day") \
|
|
1049
|
+
or roll not in ("no_roll", "next_working_day", "previous_working_day") \
|
|
1050
|
+
or not isinstance(include_end, bool) or not (cutoff is None or isinstance(cutoff, str)):
|
|
1051
|
+
raise TermError("DEADLINE_POLICY_INVALID")
|
|
1052
|
+
if count < 0:
|
|
1053
|
+
raise TermError("DEADLINE_POLICY_INVALID")
|
|
1054
|
+
month_end = policy.get("month_end")
|
|
1055
|
+
if unit in ("calendar_month", "calendar_year"):
|
|
1056
|
+
if month_end not in ("last_day_of_month", "reject_nonexistent"):
|
|
1057
|
+
raise TermError("DEADLINE_POLICY_INVALID")
|
|
1058
|
+
elif month_end is not None and month_end not in ("last_day_of_month", "reject_nonexistent"):
|
|
1059
|
+
raise TermError("DEADLINE_POLICY_INVALID")
|
|
1060
|
+
start = after
|
|
1061
|
+
wants = False if calendar_day_step else _needs_calendar(unit, policy)
|
|
1062
|
+
if wants:
|
|
1063
|
+
_coverage(cal, start)
|
|
1064
|
+
if cutoff is not None and after_time is not None and after_time > cutoff:
|
|
1065
|
+
start += _dt.timedelta(days=1)
|
|
1066
|
+
if wants:
|
|
1067
|
+
_coverage(cal, start)
|
|
1068
|
+
if start_count == "next_day":
|
|
1069
|
+
start += _dt.timedelta(days=1)
|
|
1070
|
+
if wants:
|
|
1071
|
+
_coverage(cal, start)
|
|
1072
|
+
if unit in ("calendar_month", "calendar_year"):
|
|
1073
|
+
months = count * (12 if unit == "calendar_year" else 1)
|
|
1074
|
+
due = _add_months(start, months, month_end) if count > 0 else start
|
|
1075
|
+
if count > 0 and not include_end:
|
|
1076
|
+
_coverage(cal, due)
|
|
1077
|
+
due = _previous_working(cal, due)
|
|
1078
|
+
elif calendar_day_step or unit == "calendar_week":
|
|
1079
|
+
multiplier = 1 if calendar_day_step else 7
|
|
1080
|
+
due = start + _dt.timedelta(days=multiplier * count) if count > 0 else start
|
|
1081
|
+
if count > 0 and not include_end:
|
|
1082
|
+
_coverage(cal, due)
|
|
1083
|
+
due = _previous_working(cal, due)
|
|
1084
|
+
elif count > 0:
|
|
1085
|
+
due, counted = start, 0
|
|
1086
|
+
while True:
|
|
1087
|
+
_coverage(cal, due)
|
|
1088
|
+
if (_working(cal, due) if unit == "business_day" else True):
|
|
1089
|
+
counted += 1
|
|
1090
|
+
if counted == count:
|
|
1091
|
+
break
|
|
1092
|
+
due += _dt.timedelta(days=1)
|
|
1093
|
+
if not include_end:
|
|
1094
|
+
due = _previous_working(cal, due)
|
|
1095
|
+
else:
|
|
1096
|
+
due = start
|
|
1097
|
+
skip_roll = (calendar_day_step or unit in ("calendar_month", "calendar_year", "calendar_week")) \
|
|
1098
|
+
and roll == "no_roll"
|
|
1099
|
+
if not skip_roll and not _working(cal, due) and roll != "no_roll":
|
|
1100
|
+
due = _next_working(cal, due) if roll == "next_working_day" else _previous_working(cal, due)
|
|
1101
|
+
return due
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def _month_parts(d) -> tuple:
|
|
1105
|
+
"""§84.2 (E-0084, DECISION-0267): год, месяц и последнее число месяца
|
|
1106
|
+
даты; не Date и не ISO-дата — TYPE_ERROR (`values.month_bound_value`)."""
|
|
1107
|
+
if not isinstance(d, Date):
|
|
1108
|
+
raise TermError("TYPE_ERROR")
|
|
1109
|
+
try:
|
|
1110
|
+
day = _dt.date.fromisoformat(d.v)
|
|
1111
|
+
except ValueError:
|
|
1112
|
+
raise TermError("TYPE_ERROR") from None
|
|
1113
|
+
if len(d.v) != 10:
|
|
1114
|
+
raise TermError("TYPE_ERROR")
|
|
1115
|
+
return day.year, day.month, _last_day(day.year, day.month)
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def month_start(d) -> Date:
|
|
1119
|
+
"""§84.2: первое число месяца; политика §86 и снимок §85 не читаются."""
|
|
1120
|
+
y, m, _ = _month_parts(d)
|
|
1121
|
+
return Date(f"{y:04d}-{m:02d}-01")
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def month_end(d) -> Date:
|
|
1125
|
+
"""§84.2: последнее число месяца."""
|
|
1126
|
+
y, m, last = _month_parts(d)
|
|
1127
|
+
return Date(f"{y:04d}-{m:02d}-{last:02d}")
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def days_between(a: Date, b: Date) -> Dec:
|
|
1131
|
+
"""§84.4 (E-0015): календарные дни от `a` до `b`, со знаком."""
|
|
1132
|
+
return Dec((_dt.date.fromisoformat(b.v) - _dt.date.fromisoformat(a.v)).days, 0)
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def _duration_between(a, b, unit: int, where: str) -> Dec:
|
|
1136
|
+
"""§84.1 (E-0028/E-0058): полных единиц между мгновениями, знак сохранён, дробь к нулю."""
|
|
1137
|
+
if not (isinstance(a, Instant) and isinstance(b, Instant)):
|
|
1138
|
+
raise TermError("TYPE_ERROR")
|
|
1139
|
+
x, y = _moment(a), _moment(b)
|
|
1140
|
+
seconds = y[0] - x[0]
|
|
1141
|
+
if seconds > 0 and y[1] < x[1]:
|
|
1142
|
+
seconds -= 1
|
|
1143
|
+
elif seconds < 0 and y[1] > x[1]:
|
|
1144
|
+
seconds += 1
|
|
1145
|
+
return Dec(seconds // unit if seconds >= 0 else -((-seconds) // unit), 0)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def hours_between(a, b) -> Dec:
|
|
1149
|
+
return _duration_between(a, b, 3600, "hours_between")
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def minutes_between(a, b) -> Dec:
|
|
1153
|
+
return _duration_between(a, b, 60, "minutes_between")
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
_DURATION_SECONDS = {"second": 1, "minute": 60, "hour": 3600, "day": 86400}
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def add_duration(after, count: int, unit: str) -> Instant:
|
|
1160
|
+
"""§84.1 (E-0185): мгновение через целую Quantity секунд/минут/часов/суток; канон UTC без дробных секунд."""
|
|
1161
|
+
if not isinstance(after, Instant) or unit not in _DURATION_SECONDS:
|
|
1162
|
+
raise TermError("TYPE_ERROR")
|
|
1163
|
+
seconds, _ = _moment(after)
|
|
1164
|
+
try:
|
|
1165
|
+
result = _dt.datetime(1970, 1, 1) + _dt.timedelta(seconds=seconds + int(count) * _DURATION_SECONDS[unit])
|
|
1166
|
+
except (OverflowError, ValueError) as exc:
|
|
1167
|
+
raise TermError("RESOURCE_LIMIT") from exc
|
|
1168
|
+
return Instant(f"{result.year:04d}-{result.month:02d}-{result.day:02d}T{result.hour:02d}:{result.minute:02d}:{result.second:02d}Z")
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def add_calendar_period(after: Date, count: int, unit: str) -> Date:
|
|
1172
|
+
"""§84.2 (E-0029): `TermEnv.calendar_period` — снимок не читается."""
|
|
1173
|
+
policy = _ctx().policy or {}
|
|
1174
|
+
if policy.get("roll") not in (None, "no_roll") or policy.get("cutoff") is not None \
|
|
1175
|
+
or policy.get("include_end") is not True:
|
|
1176
|
+
raise TermError("MISSING_POLICY")
|
|
1177
|
+
due = _deadline(None, _dt.date.fromisoformat(after.v), count, policy, None, unit,
|
|
1178
|
+
calendar_day_step=(unit == "calendar_day"))
|
|
1179
|
+
return Date(due.isoformat())
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
_COUNTER_UNITS = {
|
|
1183
|
+
"add_business_days": ("business_day", "calendar_day"),
|
|
1184
|
+
"add_calendar_period": ("calendar_day", "calendar_month", "calendar_year", "calendar_week"),
|
|
1185
|
+
"add_legal_term": ("business_day", "calendar_day", "calendar_month", "calendar_year", "calendar_week"),
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def counter(v, fn: str) -> tuple:
|
|
1190
|
+
"""§86.1 (DECISION-0256 п. 6): счётчик-связыватель — Quantity с единицей,
|
|
1191
|
+
допустимой функции, целый; иначе TYPE_ERROR (`values.add_*_value`). Знак не
|
|
1192
|
+
проверяется: отрицательный счётчик отвергает сама операция политикой §86."""
|
|
1193
|
+
if not isinstance(v, Qty) or v.unit not in _COUNTER_UNITS[fn] or v.u % (10 ** v.s) != 0:
|
|
1194
|
+
raise TermError("TYPE_ERROR")
|
|
1195
|
+
return v.u // (10 ** v.s), v.unit
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
def add_legal_term(after: Date, count: int, unit: str) -> Date:
|
|
1199
|
+
"""§86.1: ветвь по единице на деле — рабочий счёт либо календарный шаг."""
|
|
1200
|
+
if unit in ("business_day", "calendar_day"):
|
|
1201
|
+
return add_business_days(after, count, unit)
|
|
1202
|
+
return add_calendar_period(after, count, unit)
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def add_business_days(after: Date, count: int, unit: str, after_time=None) -> Date:
|
|
1206
|
+
"""§86: `TermEnv.deadline` — политика, затем снимок, если срок его спрашивает."""
|
|
1207
|
+
policy = _ctx().policy
|
|
1208
|
+
if policy is not None and policy.get("cutoff") is not None and after_time is None:
|
|
1209
|
+
raise TermError("MISSING_POLICY")
|
|
1210
|
+
cal = _calendar() if _needs_calendar(unit, policy) else None
|
|
1211
|
+
due = _deadline(cal, _dt.date.fromisoformat(after.v), count, policy, after_time, unit)
|
|
1212
|
+
return Date(due.isoformat())
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
# ---------------------------------------------------------------------------
|
|
1216
|
+
# §47.3 Канал суждения (DECISION-0274): порт `whynot.judgment_blockers`,
|
|
1217
|
+
# `defeater_judgment_blockers`, `power_judgment_blockers` и
|
|
1218
|
+
# `position_judgment_blockers` над ГОТОВЫМ состоянием модели. Один код для
|
|
1219
|
+
# раннера и API: модель и переводы термов приходят параметрами (`hooks`:
|
|
1220
|
+
# `val` — литерал-терм §53 → значение, `cell` — ячейка таблицы → литерал-терм).
|
|
1221
|
+
# Запрос называет судью только там, где его ответ может изменить исход:
|
|
1222
|
+
# живость правила по конъюнктам (E-0101), по поражению вывода (E-0103), по
|
|
1223
|
+
# способности дефитера снять вывод (E-0100), по иммунитету (E-0120); порядок
|
|
1224
|
+
# запросов — канонические байты объекта `request` (E-0111).
|
|
1225
|
+
JUDGMENT_ENUM_CAP = 8
|
|
1226
|
+
_J_TESTABLE = ("established", "supported", "monotone", "refuted")
|
|
1227
|
+
_J_MIRROR = {"TRUE_ONLY": "FALSE_ONLY", "FALSE_ONLY": "TRUE_ONLY", "BOTH": "BOTH", "NEITHER": "NEITHER"}
|
|
1228
|
+
_J_CONJUNCT = {"TRUE_ONLY": "SATISFIED", "FALSE_ONLY": "NOT_SATISFIED", "BOTH": "CONFLICTED", "NEITHER": "UNDETERMINED"}
|
|
1229
|
+
_J_COMPUTED = ("binary", "call", "if", "let", "match", "aggregate")
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
def jcanon(v) -> str:
|
|
1233
|
+
"""Канонические байты §208 объекта запроса: ключи по порядку, целое из JSON — цифрами."""
|
|
1234
|
+
import json as _json
|
|
1235
|
+
if isinstance(v, dict) and set(v) == {"int"}:
|
|
1236
|
+
return str(v["int"])
|
|
1237
|
+
if isinstance(v, dict):
|
|
1238
|
+
return "{" + ",".join(_json.dumps(k, ensure_ascii=False) + ":" + jcanon(v[k]) for k in sorted(v)) + "}"
|
|
1239
|
+
if isinstance(v, list):
|
|
1240
|
+
return "[" + ",".join(jcanon(x) for x in v) + "]"
|
|
1241
|
+
return _json.dumps(v, ensure_ascii=False)
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def request_key(lit: dict) -> str:
|
|
1245
|
+
return jcanon({"relation": lit["predicate"], "polarity": lit.get("polarity", "positive"), "args": lit.get("args") or []})
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def order_requests(lits: list) -> list:
|
|
1249
|
+
found: dict = {}
|
|
1250
|
+
for lit in lits:
|
|
1251
|
+
found.setdefault(request_key(lit), lit)
|
|
1252
|
+
return [found[k] for k in sorted(found)]
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def _jtruth(M, hooks, lit: dict) -> str:
|
|
1256
|
+
st = M.status(lit["predicate"], [hooks["val"](a) for a in lit.get("args") or []])
|
|
1257
|
+
return _J_MIRROR[st] if lit.get("polarity", "positive") == "negative" else st
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def _jvars(t, out: set) -> None:
|
|
1261
|
+
if isinstance(t, list):
|
|
1262
|
+
for x in t:
|
|
1263
|
+
_jvars(x, out)
|
|
1264
|
+
elif isinstance(t, dict):
|
|
1265
|
+
if t.get("kind") == "var":
|
|
1266
|
+
out.add(str(t.get("var")))
|
|
1267
|
+
else:
|
|
1268
|
+
for v in t.values():
|
|
1269
|
+
_jvars(v, out)
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
def _jkind(name: str) -> str:
|
|
1273
|
+
return {"urn:law:std#Integer": "i", "urn:law:std#Natural": "i", "urn:law:std#Decimal": "d", "urn:law:std#Money": "m",
|
|
1274
|
+
"urn:law:std#Rational": "r", "urn:law:std#Quantity": "q"}.get(name, "x")
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def _jterm(M, hooks, t: dict, subst: dict):
|
|
1278
|
+
"""Гард §57–§58 под подстановкой: переменные, литералы, константы §54 и
|
|
1279
|
+
арифметика §58; иное — исключение, у оракула это UNDETERMINED (E-0101)."""
|
|
1280
|
+
k = t.get("kind")
|
|
1281
|
+
if k == "var":
|
|
1282
|
+
if t["var"] not in subst:
|
|
1283
|
+
raise Refused("несвязанная переменная гарда")
|
|
1284
|
+
return _jterm(M, hooks, subst[t["var"]], subst)
|
|
1285
|
+
consts = getattr(M, "BACKCHAIN_CONSTS", None) or {}
|
|
1286
|
+
if k == "const_ref" and t.get("id") in consts:
|
|
1287
|
+
return _jterm(M, hooks, consts[t["id"]], subst)
|
|
1288
|
+
if k == "value":
|
|
1289
|
+
return hooks["val"](t), _jkind(str((t.get("type") or {}).get("name")))
|
|
1290
|
+
if k in ("entity_ref", "const_ref"):
|
|
1291
|
+
return hooks["val"](t), "x"
|
|
1292
|
+
if k == "binary" and t.get("op") in ("add", "sub", "mul", "div", "mod"):
|
|
1293
|
+
lv, lk = _jterm(M, hooks, t["left"], subst)
|
|
1294
|
+
rv, rk = _jterm(M, hooks, t["right"], subst)
|
|
1295
|
+
if lk == "x" or rk == "x":
|
|
1296
|
+
raise Refused("арифметика над нечисловым операндом")
|
|
1297
|
+
v = arith(t["op"], lv, rv, lk, rk)
|
|
1298
|
+
kind = "r" if isinstance(v, Rat) else "m" if isinstance(v, Money) else "q" if isinstance(v, Qty) \
|
|
1299
|
+
else "i" if lk == "i" and rk == "i" else "d"
|
|
1300
|
+
return v, kind
|
|
1301
|
+
raise Refused(f"терм гарда вне обхода суждения: {k}")
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
def _jconjunct(M, hooks, status: str, lit: dict, subst: dict):
|
|
1305
|
+
"""Порт `whynot._conjunct_report`: None — конъюнкт с несвязанной переменной;
|
|
1306
|
+
иначе (trigger, ground) — ground None у гарда, квантора и вычисляемого аргумента."""
|
|
1307
|
+
if status == "guard":
|
|
1308
|
+
names: set = set()
|
|
1309
|
+
_jvars(lit, names)
|
|
1310
|
+
if any(n not in subst for n in names):
|
|
1311
|
+
return None
|
|
1312
|
+
try:
|
|
1313
|
+
lv, _ = _jterm(M, hooks, lit["left"], subst)
|
|
1314
|
+
rv, _ = _jterm(M, hooks, lit["right"], subst)
|
|
1315
|
+
ok = cmp(lv, lit["op"], rv)
|
|
1316
|
+
except Exception: # noqa: BLE001 — терм-ошибка §58 не убивает правило
|
|
1317
|
+
return ("UNDETERMINED", None)
|
|
1318
|
+
return ("SATISFIED" if ok else "NOT_SATISFIED", None)
|
|
1319
|
+
if (lit or {}).get("kind") != "literal":
|
|
1320
|
+
return ("UNDETERMINED", None)
|
|
1321
|
+
args = lit.get("args") or []
|
|
1322
|
+
names = set()
|
|
1323
|
+
_jvars(args, names)
|
|
1324
|
+
if any(n not in subst for n in names):
|
|
1325
|
+
return None
|
|
1326
|
+
if any((a or {}).get("kind") in _J_COMPUTED for a in args):
|
|
1327
|
+
return ("UNDETERMINED", None)
|
|
1328
|
+
shown = dict(lit, args=[subst[a["var"]] if a.get("kind") == "var" else a for a in args])
|
|
1329
|
+
if any((a or {}).get("kind") == "wildcard" for a in args):
|
|
1330
|
+
# §98 (E-0165): свидетель существования, значение не связывается.
|
|
1331
|
+
if lit.get("polarity", "positive") != "positive" or status not in ("established", "supported", "monotone"):
|
|
1332
|
+
return ("UNDETERMINED", shown)
|
|
1333
|
+
want = [None if a.get("kind") == "wildcard" else hooks["val"](a) for a in shown["args"]]
|
|
1334
|
+
rows = M.TABLES.get(lit["predicate"], [])
|
|
1335
|
+
witness = any(len(r) == len(want) and all(w is None or key([v]) == key([w]) for v, w in zip(r, want))
|
|
1336
|
+
and (M.status(lit["predicate"], r) == "TRUE_ONLY" if status == "established"
|
|
1337
|
+
else M.status(lit["predicate"], r) in ("TRUE_ONLY", "BOTH"))
|
|
1338
|
+
for r in rows)
|
|
1339
|
+
return ("SATISFIED" if witness else "UNDETERMINED", shown)
|
|
1340
|
+
pair = _jtruth(M, hooks, shown)
|
|
1341
|
+
if status == "not_known":
|
|
1342
|
+
return ("SATISFIED" if pair == "NEITHER" else "NOT_SATISFIED", shown)
|
|
1343
|
+
if status in ("supported", "monotone"):
|
|
1344
|
+
return ("SATISFIED" if pair in ("TRUE_ONLY", "BOTH") else "UNDETERMINED", shown)
|
|
1345
|
+
if status in ("refuted", "unknown"):
|
|
1346
|
+
want_pair = "FALSE_ONLY" if status == "refuted" else "NEITHER"
|
|
1347
|
+
return ("SATISFIED" if pair == want_pair else "NOT_SATISFIED", shown)
|
|
1348
|
+
return (_J_CONJUNCT.get(pair, "UNDETERMINED"), shown)
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def _junify(head: dict, target: dict):
|
|
1352
|
+
"""Порт `unify_head`: подстановка головы под ground-цель; переменная дважды — одинаково."""
|
|
1353
|
+
if (head or {}).get("kind") != "literal" or (target or {}).get("kind") != "literal":
|
|
1354
|
+
return None
|
|
1355
|
+
if head.get("predicate") != target.get("predicate"):
|
|
1356
|
+
return None
|
|
1357
|
+
if head.get("polarity", "positive") != target.get("polarity", "positive"):
|
|
1358
|
+
return None
|
|
1359
|
+
h, g = head.get("args") or [], target.get("args") or []
|
|
1360
|
+
if len(h) != len(g):
|
|
1361
|
+
return None
|
|
1362
|
+
subst: dict = {}
|
|
1363
|
+
for x, y in zip(h, g):
|
|
1364
|
+
if x.get("kind") == "var":
|
|
1365
|
+
if x["var"] in subst and jcanon(subst[x["var"]]) != jcanon(y):
|
|
1366
|
+
return None
|
|
1367
|
+
subst[x["var"]] = y
|
|
1368
|
+
elif jcanon(x) != jcanon(y):
|
|
1369
|
+
return None
|
|
1370
|
+
return subst
|
|
1371
|
+
|
|
1372
|
+
|
|
1373
|
+
def _jenumerate(M, hooks, lit: dict, subst: dict, unbound: list) -> list:
|
|
1374
|
+
"""DECISION-0036 (б): перечисление судейского конъюнкта по активному домену —
|
|
1375
|
+
аргументы всех атомов таблиц, порядок §208, кап ДО фильтра по NEITHER."""
|
|
1376
|
+
args = lit.get("args") or []
|
|
1377
|
+
if any((a or {}).get("kind") == "wildcard" for a in args):
|
|
1378
|
+
return []
|
|
1379
|
+
domain: dict = {}
|
|
1380
|
+
for pred, rows in M.TABLES.items():
|
|
1381
|
+
for row in rows:
|
|
1382
|
+
for i, v in enumerate(row):
|
|
1383
|
+
t = hooks["cell"](pred, i, v)
|
|
1384
|
+
domain[jcanon(t)] = t
|
|
1385
|
+
terms = [domain[k] for k in sorted(domain)]
|
|
1386
|
+
if not terms:
|
|
1387
|
+
return []
|
|
1388
|
+
names = sorted(unbound)
|
|
1389
|
+
out: list = []
|
|
1390
|
+
total = len(terms) ** len(names)
|
|
1391
|
+
for idx in range(total if total < JUDGMENT_ENUM_CAP else JUDGMENT_ENUM_CAP): # `min` здесь — функция §58 рантайма
|
|
1392
|
+
# itertools.product: первая переменная меняется медленнее всех.
|
|
1393
|
+
full = dict(subst)
|
|
1394
|
+
rest = idx
|
|
1395
|
+
for name in reversed(names):
|
|
1396
|
+
full[name] = terms[rest % len(terms)]
|
|
1397
|
+
rest //= len(terms)
|
|
1398
|
+
ground = dict(lit, args=[full[a["var"]] if a.get("kind") == "var" else a for a in args])
|
|
1399
|
+
if _jtruth(M, hooks, ground) == "NEITHER":
|
|
1400
|
+
out.append(ground)
|
|
1401
|
+
return out
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
def _jprio(M) -> set:
|
|
1405
|
+
return getattr(M, "PRIO_ACTIVE", None) if getattr(M, "PRIO_ACTIVE", None) is not None else getattr(M, "PRIO", set())
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
def _jrow_key(hooks, lit: dict) -> str:
|
|
1409
|
+
return key([hooks["val"](a) for a in lit.get("args") or []])
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def _jconclusion_defeated(M, hooks, rule: dict, target: dict) -> bool:
|
|
1413
|
+
"""E-0103: поражён ли был бы вывод поражаемого правила при любом ответе судьи."""
|
|
1414
|
+
if rule.get("strength") != "defeasible":
|
|
1415
|
+
return False
|
|
1416
|
+
prio, tkey = _jprio(M), _jrow_key(hooks, target)
|
|
1417
|
+
pos = target.get("polarity", "positive") != "negative"
|
|
1418
|
+
for a in getattr(M, "APPLIED", []):
|
|
1419
|
+
if not a["defeater"] or a["urn"] != target["predicate"] or a["key"] != tkey or a["positive"] != pos:
|
|
1420
|
+
continue
|
|
1421
|
+
if (rule["name"], a["rule"]) not in prio:
|
|
1422
|
+
return True
|
|
1423
|
+
return False
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
def _jdefeater_can_remove(M, hooks, rule: dict, target: dict) -> bool:
|
|
1427
|
+
"""E-0100: способен ли живой дефитер снять СТОЯЩИЙ вывод — строгая опора не
|
|
1428
|
+
поражается; каждое уцелевшее поражаемое применение обязано уступать ему."""
|
|
1429
|
+
row = [hooks["val"](a) for a in target.get("args") or []]
|
|
1430
|
+
bits = M.support_bits(target["predicate"], row) if hasattr(M, "support_bits") else 0
|
|
1431
|
+
pos = target.get("polarity", "positive") != "negative"
|
|
1432
|
+
if bits & (1 if pos else 2):
|
|
1433
|
+
return False
|
|
1434
|
+
tkey, prio = key(row), _jprio(M)
|
|
1435
|
+
apps = [a for a in getattr(M, "APPLIED", [])
|
|
1436
|
+
if not a["defeater"] and not a["defeated"] and a["urn"] == target["predicate"] and a["key"] == tkey and a["positive"] == pos]
|
|
1437
|
+
if not apps:
|
|
1438
|
+
return False
|
|
1439
|
+
return all((a["rule"], rule["name"]) not in prio for a in apps)
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
def collect_judgments(M, hooks, goal: dict, root_defeaters: bool) -> list:
|
|
1443
|
+
"""Порт `whynot._collect_judgments`: общий обход §185 от цели без опоры
|
|
1444
|
+
(root_defeaters=False) и от стоящей цели через дефитеры её головы (True)."""
|
|
1445
|
+
J = getattr(M, "JUDGMENTS", {}) or {}
|
|
1446
|
+
index: dict = {}
|
|
1447
|
+
for r in getattr(M, "BACKCHAIN", []) or []:
|
|
1448
|
+
index.setdefault(r["head"]["predicate"], []).append(r)
|
|
1449
|
+
found: dict = {}
|
|
1450
|
+
visited: set = set()
|
|
1451
|
+
now = legal_time()
|
|
1452
|
+
|
|
1453
|
+
def dead(reports) -> bool:
|
|
1454
|
+
for st, _lit, rep in reports:
|
|
1455
|
+
if rep is None or rep[0] not in ("NOT_SATISFIED", "CONFLICTED"):
|
|
1456
|
+
continue
|
|
1457
|
+
# DECISION-0036 (а): refuted-тест над самим отношением суждения при паре NEITHER решает судья.
|
|
1458
|
+
if st == "refuted" and rep[1] is not None and rep[1]["predicate"] in J and _jtruth(M, hooks, rep[1]) == "NEITHER":
|
|
1459
|
+
continue
|
|
1460
|
+
return True
|
|
1461
|
+
return False
|
|
1462
|
+
|
|
1463
|
+
def walk(target: dict, defeaters_only: bool) -> None:
|
|
1464
|
+
vkey = jcanon({"p": target["predicate"], "a": target.get("args") or [], "n": target.get("polarity", "positive")})
|
|
1465
|
+
if vkey in visited:
|
|
1466
|
+
return
|
|
1467
|
+
visited.add(vkey)
|
|
1468
|
+
for r in index.get(target["predicate"], ()):
|
|
1469
|
+
if defeaters_only and r.get("strength") != "defeater":
|
|
1470
|
+
continue
|
|
1471
|
+
subst = _junify(r["head"], target)
|
|
1472
|
+
if subst is None:
|
|
1473
|
+
continue
|
|
1474
|
+
w = r.get("window")
|
|
1475
|
+
if w and ((w.get("start") and now < w["start"]) or (w.get("end") and now >= w["end"])):
|
|
1476
|
+
continue
|
|
1477
|
+
reports = [(st, lit, _jconjunct(M, hooks, st, lit, subst)) for st, lit in r["body"]]
|
|
1478
|
+
if dead(reports):
|
|
1479
|
+
continue
|
|
1480
|
+
if _jconclusion_defeated(M, hooks, r, target):
|
|
1481
|
+
continue
|
|
1482
|
+
if defeaters_only and not _jdefeater_can_remove(M, hooks, r, target):
|
|
1483
|
+
continue
|
|
1484
|
+
for st, lit, rep in reports:
|
|
1485
|
+
if rep is None:
|
|
1486
|
+
if st in _J_TESTABLE and (lit or {}).get("kind") == "literal" and lit.get("predicate") in J:
|
|
1487
|
+
names: set = set()
|
|
1488
|
+
_jvars(lit.get("args") or [], names)
|
|
1489
|
+
for g in _jenumerate(M, hooks, lit, subst, [n for n in names if n not in subst]):
|
|
1490
|
+
found.setdefault(request_key(g), g)
|
|
1491
|
+
continue
|
|
1492
|
+
trigger, ground = rep
|
|
1493
|
+
if ground is None or ground.get("kind") != "literal" \
|
|
1494
|
+
or any((a or {}).get("kind") == "wildcard" for a in ground.get("args") or []):
|
|
1495
|
+
continue
|
|
1496
|
+
if trigger == "UNDETERMINED":
|
|
1497
|
+
if ground["predicate"] in J:
|
|
1498
|
+
found.setdefault(request_key(ground), ground)
|
|
1499
|
+
else:
|
|
1500
|
+
walk(ground, False)
|
|
1501
|
+
elif st == "refuted" and trigger == "NOT_SATISFIED" and ground["predicate"] in J \
|
|
1502
|
+
and _jtruth(M, hooks, ground) == "NEITHER":
|
|
1503
|
+
found.setdefault(request_key(ground), ground)
|
|
1504
|
+
|
|
1505
|
+
walk(goal, root_defeaters)
|
|
1506
|
+
return list(found.values())
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
def _nlit_literal(hooks, n) -> dict:
|
|
1510
|
+
pred, positive, row = n[0], n[1], n[2]
|
|
1511
|
+
return {"kind": "literal", "polarity": "positive" if positive else "negative", "predicate": pred,
|
|
1512
|
+
"args": [hooks["cell"](pred, i, v) for i, v in enumerate(row)]}
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
def _nlit_status(M, n) -> str:
|
|
1516
|
+
st = M.status(n[0], n[2])
|
|
1517
|
+
return st if n[1] else _J_MIRROR[st]
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
def power_judgments(M, hooks, goal: dict) -> list:
|
|
1521
|
+
"""E-0100, носитель `valid_when` §127: судьи действительности осуществлённого
|
|
1522
|
+
полномочия, чей эффект есть цель; E-0120 — не при эффекте, запертом иммунитетом."""
|
|
1523
|
+
J = getattr(M, "JUDGMENTS", {}) or {}
|
|
1524
|
+
if goal.get("polarity", "positive") != "positive" or not hasattr(M, "POSITIONS"):
|
|
1525
|
+
return []
|
|
1526
|
+
gkey = (goal["predicate"], _jrow_key(hooks, goal))
|
|
1527
|
+
if gkey in (getattr(M, "BLOCKED_IMMUNITY", None) or set()):
|
|
1528
|
+
return []
|
|
1529
|
+
out: list = []
|
|
1530
|
+
for p in M.POSITIONS:
|
|
1531
|
+
if p["modality"] != "power" or p["status"] != "ACTIVE":
|
|
1532
|
+
continue
|
|
1533
|
+
eff = p["spec"]["effect"]
|
|
1534
|
+
if eff is None or not eff[1] or eff[0] != goal["predicate"] or key(eff[2]) != gkey[1]:
|
|
1535
|
+
continue
|
|
1536
|
+
ex = p["spec"]["exercise"]
|
|
1537
|
+
if ex is None or _nlit_status(M, ex) not in ("TRUE_ONLY", "BOTH"):
|
|
1538
|
+
continue
|
|
1539
|
+
vw = p["spec"]["validWhen"]
|
|
1540
|
+
if vw is None or vw[0] not in J:
|
|
1541
|
+
continue
|
|
1542
|
+
if _nlit_status(M, vw) == "NEITHER":
|
|
1543
|
+
out.append(_nlit_literal(hooks, vw))
|
|
1544
|
+
return order_requests(out)
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
def position_judgments(M, hooks) -> list:
|
|
1548
|
+
"""E-0100, носитель `condition` цели §123/§124: судьи статуса позиции обязанности."""
|
|
1549
|
+
J = getattr(M, "JUDGMENTS", {}) or {}
|
|
1550
|
+
if not hasattr(M, "POSITIONS"):
|
|
1551
|
+
return []
|
|
1552
|
+
out: list = []
|
|
1553
|
+
for p in M.POSITIONS:
|
|
1554
|
+
if p["status"] not in ("ACTIVE", "UNDETERMINED"):
|
|
1555
|
+
continue
|
|
1556
|
+
cond = p["spec"]["cond"]
|
|
1557
|
+
if cond is None or cond[0] not in J:
|
|
1558
|
+
continue
|
|
1559
|
+
if _nlit_status(M, cond) == "NEITHER":
|
|
1560
|
+
out.append(_nlit_literal(hooks, cond))
|
|
1561
|
+
return order_requests(out)
|
|
1562
|
+
|
|
1563
|
+
|
|
1564
|
+
def build_requests(M, blockers: list, tail: str) -> list:
|
|
1565
|
+
"""JudgmentRequest схемы evaluation по упорядоченным блокерам (без proof-узла)."""
|
|
1566
|
+
out: list = []
|
|
1567
|
+
for i, b in enumerate(blockers):
|
|
1568
|
+
d = (getattr(M, "JUDGMENTS", {}) or {}).get(b["predicate"], {})
|
|
1569
|
+
r = {"id": f"urn:judgment:{tail}:{i}", "predicate": b["predicate"], "authority": d.get("authority", b["predicate"]),
|
|
1570
|
+
"request": {"relation": b["predicate"], "polarity": b.get("polarity", "positive"), "args": b.get("args") or []}}
|
|
1571
|
+
if d.get("requestSchema") is not None:
|
|
1572
|
+
r["requestSchema"] = d["requestSchema"]
|
|
1573
|
+
out.append(r)
|
|
1574
|
+
return out
|
|
1575
|
+
|
|
1576
|
+
|
|
1577
|
+
def judgment_blockers(M, hooks, goal: dict, status: str, conflict: bool) -> list:
|
|
1578
|
+
"""Порт ветвей `queries.query_truth`: цель — отношение суждения без опоры;
|
|
1579
|
+
цель без опоры — тела правил и `valid_when`; стоящая цель — дефитеры головы."""
|
|
1580
|
+
J = getattr(M, "JUDGMENTS", {}) or {}
|
|
1581
|
+
if not J:
|
|
1582
|
+
return []
|
|
1583
|
+
unresolved = status == "NEITHER" and not conflict
|
|
1584
|
+
if goal["predicate"] in J and unresolved:
|
|
1585
|
+
return [goal]
|
|
1586
|
+
if unresolved:
|
|
1587
|
+
return order_requests(collect_judgments(M, hooks, goal, False) + power_judgments(M, hooks, goal))
|
|
1588
|
+
if status == "TRUE_ONLY" and not conflict:
|
|
1589
|
+
return order_requests(collect_judgments(M, hooks, goal, True))
|
|
1590
|
+
return []
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
_J_KINDS = {"urn:law:std#Boolean": "boolean", "urn:law:std#Decimal": "decimal", "urn:law:std#Text": "text",
|
|
1594
|
+
"urn:law:std#Date": "date", "urn:law:std#Instant": "instant", "urn:law:std#Integer": "integer",
|
|
1595
|
+
"urn:law:std#Natural": "integer", "urn:law:std#Money": "money", "urn:law:std#Quantity": "quantity",
|
|
1596
|
+
"urn:law:std#Rational": "rational"}
|
|
1597
|
+
|
|
1598
|
+
|
|
1599
|
+
def judgment_answer(M, assertion: dict):
|
|
1600
|
+
"""§47.3.1 (E-0089): ответ судьи с объектом `judgment` — орган и форма payload.
|
|
1601
|
+
Возвращает код issue либо None (ответ принят)."""
|
|
1602
|
+
judgment = assertion.get("judgment")
|
|
1603
|
+
if not isinstance(judgment, dict):
|
|
1604
|
+
return None
|
|
1605
|
+
pred = (assertion.get("literal") or {}).get("predicate")
|
|
1606
|
+
d = (getattr(M, "JUDGMENTS", {}) or {}).get(pred, {})
|
|
1607
|
+
if judgment.get("authority") != d.get("authority", pred):
|
|
1608
|
+
return "JUDGMENT_AUTHORITY_MISMATCH"
|
|
1609
|
+
payload = judgment.get("payload")
|
|
1610
|
+
if payload is not None:
|
|
1611
|
+
schema = d.get("requestSchema")
|
|
1612
|
+
if schema is None:
|
|
1613
|
+
return "JUDGMENT_PAYLOAD_MISMATCH"
|
|
1614
|
+
want = _J_KINDS.get(str(schema.get("name")))
|
|
1615
|
+
if want is not None:
|
|
1616
|
+
got = _J_KINDS.get(str((payload.get("type") or {}).get("name"))) if payload.get("kind") == "value" \
|
|
1617
|
+
else "entity_ref" if payload.get("kind") == "entity_ref" else "enum"
|
|
1618
|
+
if got != want:
|
|
1619
|
+
return "JUDGMENT_PAYLOAD_MISMATCH"
|
|
1620
|
+
return None
|
|
1621
|
+
|
|
1622
|
+
|
|
1623
|
+
def admit_numeric_inputs(case, query, judgments=False):
|
|
1624
|
+
# Typed request nodes only; assertion payload/metadata are not term AST.
|
|
1625
|
+
stack = [query, *((a.get("literal") or {}) for a in case.get("assertions") or [])]
|
|
1626
|
+
while stack:
|
|
1627
|
+
node = stack.pop()
|
|
1628
|
+
if isinstance(node, list): stack.extend(node)
|
|
1629
|
+
elif isinstance(node, dict):
|
|
1630
|
+
if node.get("kind") == "value" and (node.get("type") or {}).get("name") == "urn:law:std#Number":
|
|
1631
|
+
raise Refused("NUMBER_UNAVAILABLE: exact Number and slot narrowing (E-0316/E-0319/E-0334) are outside this slice")
|
|
1632
|
+
if judgments and node.get("kind") in ("aggregate", "comprehension"):
|
|
1633
|
+
raise Refused("AGGREGATE_JUDGMENT_UNAVAILABLE: open aggregate members require E-0357")
|
|
1634
|
+
stack.extend(v for v in node.values() if isinstance(v, (dict, list)))
|