behavior-contracts 0.1.2__tar.gz
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.
- behavior_contracts-0.1.2/PKG-INFO +89 -0
- behavior_contracts-0.1.2/README.md +77 -0
- behavior_contracts-0.1.2/pyproject.toml +26 -0
- behavior_contracts-0.1.2/setup.cfg +4 -0
- behavior_contracts-0.1.2/src/behavior_contracts/__init__.py +108 -0
- behavior_contracts-0.1.2/src/behavior_contracts/canonical.py +222 -0
- behavior_contracts-0.1.2/src/behavior_contracts/codec.py +88 -0
- behavior_contracts-0.1.2/src/behavior_contracts/conformance.py +284 -0
- behavior_contracts-0.1.2/src/behavior_contracts/envelope.py +89 -0
- behavior_contracts-0.1.2/src/behavior_contracts/expr_eval.py +418 -0
- behavior_contracts-0.1.2/src/behavior_contracts/guard.py +54 -0
- behavior_contracts-0.1.2/src/behavior_contracts/plan.py +191 -0
- behavior_contracts-0.1.2/src/behavior_contracts/template.py +109 -0
- behavior_contracts-0.1.2/src/behavior_contracts.egg-info/PKG-INFO +89 -0
- behavior_contracts-0.1.2/src/behavior_contracts.egg-info/SOURCES.txt +19 -0
- behavior_contracts-0.1.2/src/behavior_contracts.egg-info/dependency_links.txt +1 -0
- behavior_contracts-0.1.2/src/behavior_contracts.egg-info/entry_points.txt +2 -0
- behavior_contracts-0.1.2/src/behavior_contracts.egg-info/requires.txt +3 -0
- behavior_contracts-0.1.2/src/behavior_contracts.egg-info/top_level.txt +1 -0
- behavior_contracts-0.1.2/tests/test_conformance.py +10 -0
- behavior_contracts-0.1.2/tests/test_public_api.py +500 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: behavior-contracts
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Multi-language IR-Runtime common core (Python port). The COMMON 'thin core' primitives from dsl-contracts runtime-boundary.md — validate_envelope / evaluate_expression / render_template / run_plan / canonical_value / canonical_json / py_float_repr / assert_portable — with zero backend (DynamoDB/graphddb) dependencies.
|
|
5
|
+
Author: foo-log
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: dsl,ir,runtime,conformance,canonical
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
12
|
+
|
|
13
|
+
# behavior-contracts (Python)
|
|
14
|
+
|
|
15
|
+
The **COMMON "thin core"** runtime primitives from
|
|
16
|
+
[`runtime-boundary.md`](../runtime-boundary.md), ported to Python from the
|
|
17
|
+
TypeScript reference implementation (`ts/src/*`) with **identical semantics**.
|
|
18
|
+
|
|
19
|
+
This package contains **only** the DSL-agnostic primitives that WS1 classified as
|
|
20
|
+
COMMON. It has **zero** backend (DynamoDB / graphddb) dependencies — no boto3, no
|
|
21
|
+
key marshalling, no retry tuning, no hydrate, no bundle format. Those stay in the
|
|
22
|
+
consumer (graphddb).
|
|
23
|
+
|
|
24
|
+
## Public API
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from behavior_contracts import (
|
|
28
|
+
validate_envelope, # spec-version fail-closed check
|
|
29
|
+
evaluate_expression, # expression-ir.md evaluator (i64-checked, truncated %, code-point cmp)
|
|
30
|
+
render_template, # {param} rendering (strict, Python str() parity)
|
|
31
|
+
run_plan, final_tree, # execution-plan skeleton (stage / Skip propagation / Policy Kind)
|
|
32
|
+
canonical_value, # key identity (top-level key sort)
|
|
33
|
+
canonical_json, # fingerprint (recursive key sort)
|
|
34
|
+
py_float_repr, # CPython repr(float) canonical decimal
|
|
35
|
+
assert_portable, # Portability Guard
|
|
36
|
+
decode_value, deep_equals, # conformance runner adapter (int/float type distinction)
|
|
37
|
+
encode_value, resolve_partial, cmp_code_points,
|
|
38
|
+
SPEC_VERSIONS, ENVELOPE_SPEC_VERSION,
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Each COMMON primitive raises a typed `*Failure` exception carrying a `.code`
|
|
43
|
+
string matching the conformance protocol's failure-code set.
|
|
44
|
+
|
|
45
|
+
## §8 cross-language traps this port handles
|
|
46
|
+
|
|
47
|
+
- **`%` sign**: normative mod is *truncated* (dividend sign). Python's native `%`
|
|
48
|
+
returns the divisor sign, so `evaluate_expression({"mod":[-7,2]}) == -1`
|
|
49
|
+
(native `-7 % 2 == 1`). Implemented via truncated-quotient correction; float
|
|
50
|
+
mod via `math.fmod`.
|
|
51
|
+
- **int is checked i64**: Python `int` is arbitrary-precision, so i64 bounds are
|
|
52
|
+
enforced manually; overflow → `INT_OVERFLOW`.
|
|
53
|
+
- **string comparison**: code-point order. Python `str` compares by code point
|
|
54
|
+
already (no UTF-16 surrogate hazard) — verified in tests.
|
|
55
|
+
- **NaN/Inf → Failure**; `div` always float with `|int| > 2^53` widening →
|
|
56
|
+
`PRECISION_LOSS`; `py_float_repr` matches CPython `repr(float)` byte-for-byte
|
|
57
|
+
(parametrized parity tests).
|
|
58
|
+
- **bool is not int**: `isinstance(x, bool)` is checked before `int` everywhere
|
|
59
|
+
(Python's `bool <: int`).
|
|
60
|
+
|
|
61
|
+
## Conformance
|
|
62
|
+
|
|
63
|
+
Runs the 4 shared suites (93 vectors) from `../conformance/vectors/*.json`
|
|
64
|
+
through this package per [`../conformance/PROTOCOL.md`](../conformance/PROTOCOL.md),
|
|
65
|
+
including the pre-flight version fail-closed sweep.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python -m behavior_contracts.conformance # → "93 passed, 0 failed / 93 vectors across 4 suites"
|
|
69
|
+
# or the console script:
|
|
70
|
+
behavior-contracts-conformance
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Override the vectors location with `DSL_CONTRACTS_VECTORS=/path/to/vectors`.
|
|
74
|
+
|
|
75
|
+
## Tests
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install -e ".[test]"
|
|
79
|
+
python -m pytest # unit tests over the public API + the conformance runner as a test
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Docker integration: N/A
|
|
83
|
+
|
|
84
|
+
This package has **no external service dependency** (no database, no network) —
|
|
85
|
+
it is pure in-process logic over JSON-shaped values. There is therefore nothing
|
|
86
|
+
for a docker-compose integration harness to stand up; the conformance runner and
|
|
87
|
+
pytest exercise the full surface in-process. (The graphddb *adapter PoC* in WS4
|
|
88
|
+
Part B is where docker-backed integration applies, because graphddb talks to
|
|
89
|
+
DynamoDB Local.)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# behavior-contracts (Python)
|
|
2
|
+
|
|
3
|
+
The **COMMON "thin core"** runtime primitives from
|
|
4
|
+
[`runtime-boundary.md`](../runtime-boundary.md), ported to Python from the
|
|
5
|
+
TypeScript reference implementation (`ts/src/*`) with **identical semantics**.
|
|
6
|
+
|
|
7
|
+
This package contains **only** the DSL-agnostic primitives that WS1 classified as
|
|
8
|
+
COMMON. It has **zero** backend (DynamoDB / graphddb) dependencies — no boto3, no
|
|
9
|
+
key marshalling, no retry tuning, no hydrate, no bundle format. Those stay in the
|
|
10
|
+
consumer (graphddb).
|
|
11
|
+
|
|
12
|
+
## Public API
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from behavior_contracts import (
|
|
16
|
+
validate_envelope, # spec-version fail-closed check
|
|
17
|
+
evaluate_expression, # expression-ir.md evaluator (i64-checked, truncated %, code-point cmp)
|
|
18
|
+
render_template, # {param} rendering (strict, Python str() parity)
|
|
19
|
+
run_plan, final_tree, # execution-plan skeleton (stage / Skip propagation / Policy Kind)
|
|
20
|
+
canonical_value, # key identity (top-level key sort)
|
|
21
|
+
canonical_json, # fingerprint (recursive key sort)
|
|
22
|
+
py_float_repr, # CPython repr(float) canonical decimal
|
|
23
|
+
assert_portable, # Portability Guard
|
|
24
|
+
decode_value, deep_equals, # conformance runner adapter (int/float type distinction)
|
|
25
|
+
encode_value, resolve_partial, cmp_code_points,
|
|
26
|
+
SPEC_VERSIONS, ENVELOPE_SPEC_VERSION,
|
|
27
|
+
)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Each COMMON primitive raises a typed `*Failure` exception carrying a `.code`
|
|
31
|
+
string matching the conformance protocol's failure-code set.
|
|
32
|
+
|
|
33
|
+
## §8 cross-language traps this port handles
|
|
34
|
+
|
|
35
|
+
- **`%` sign**: normative mod is *truncated* (dividend sign). Python's native `%`
|
|
36
|
+
returns the divisor sign, so `evaluate_expression({"mod":[-7,2]}) == -1`
|
|
37
|
+
(native `-7 % 2 == 1`). Implemented via truncated-quotient correction; float
|
|
38
|
+
mod via `math.fmod`.
|
|
39
|
+
- **int is checked i64**: Python `int` is arbitrary-precision, so i64 bounds are
|
|
40
|
+
enforced manually; overflow → `INT_OVERFLOW`.
|
|
41
|
+
- **string comparison**: code-point order. Python `str` compares by code point
|
|
42
|
+
already (no UTF-16 surrogate hazard) — verified in tests.
|
|
43
|
+
- **NaN/Inf → Failure**; `div` always float with `|int| > 2^53` widening →
|
|
44
|
+
`PRECISION_LOSS`; `py_float_repr` matches CPython `repr(float)` byte-for-byte
|
|
45
|
+
(parametrized parity tests).
|
|
46
|
+
- **bool is not int**: `isinstance(x, bool)` is checked before `int` everywhere
|
|
47
|
+
(Python's `bool <: int`).
|
|
48
|
+
|
|
49
|
+
## Conformance
|
|
50
|
+
|
|
51
|
+
Runs the 4 shared suites (93 vectors) from `../conformance/vectors/*.json`
|
|
52
|
+
through this package per [`../conformance/PROTOCOL.md`](../conformance/PROTOCOL.md),
|
|
53
|
+
including the pre-flight version fail-closed sweep.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python -m behavior_contracts.conformance # → "93 passed, 0 failed / 93 vectors across 4 suites"
|
|
57
|
+
# or the console script:
|
|
58
|
+
behavior-contracts-conformance
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Override the vectors location with `DSL_CONTRACTS_VECTORS=/path/to/vectors`.
|
|
62
|
+
|
|
63
|
+
## Tests
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install -e ".[test]"
|
|
67
|
+
python -m pytest # unit tests over the public API + the conformance runner as a test
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Docker integration: N/A
|
|
71
|
+
|
|
72
|
+
This package has **no external service dependency** (no database, no network) —
|
|
73
|
+
it is pure in-process logic over JSON-shaped values. There is therefore nothing
|
|
74
|
+
for a docker-compose integration harness to stand up; the conformance runner and
|
|
75
|
+
pytest exercise the full surface in-process. (The graphddb *adapter PoC* in WS4
|
|
76
|
+
Part B is where docker-backed integration applies, because graphddb talks to
|
|
77
|
+
DynamoDB Local.)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "behavior-contracts"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Multi-language IR-Runtime common core (Python port). The COMMON 'thin core' primitives from dsl-contracts runtime-boundary.md — validate_envelope / evaluate_expression / render_template / run_plan / canonical_value / canonical_json / py_float_repr / assert_portable — with zero backend (DynamoDB/graphddb) dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "foo-log" }]
|
|
13
|
+
keywords = ["dsl", "ir", "runtime", "conformance", "canonical"]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
test = ["pytest>=7.0"]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
behavior-contracts-conformance = "behavior_contracts.conformance:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools]
|
|
22
|
+
package-dir = { "" = "src" }
|
|
23
|
+
packages = ["behavior_contracts"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""behavior-contracts — 多言語 IR-Runtime 共通層(Python 実装)。
|
|
2
|
+
|
|
3
|
+
runtime-boundary.md で COMMON と判定された「薄い核」プリミティブのみを公開する。
|
|
4
|
+
DynamoDB / graphddb 固有の概念(backend 実行・key resolution・retry Tuning・hydrate・
|
|
5
|
+
bundle 形式)は一切含まない。TS 参照実装 ``ts/src/*`` の意味論を完全一致で移植したもの。
|
|
6
|
+
|
|
7
|
+
公開 COMMON プリミティブ(runtime-boundary.md §2.1):
|
|
8
|
+
- validate_envelope — spec-version fail-closed 検査
|
|
9
|
+
- evaluate_expression — expression-ir.md の規範評価
|
|
10
|
+
- render_template — ``{param}`` 束縛描画(strict)
|
|
11
|
+
- run_plan — 実行計画の骨格実行(stage / Skip 伝播 / Policy Kind)
|
|
12
|
+
- canonical_value / canonical_json / py_float_repr — 決定的正準直列化
|
|
13
|
+
- assert_portable — Portability Guard(IR 可搬性不変条件)
|
|
14
|
+
- decode_value / deep_equals — conformance runner adapter 共通部
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
# ── 対応する仕様バージョン(IR/vector の spec version。ライブラリ semver とは別管理)──
|
|
20
|
+
SPEC_VERSIONS = {
|
|
21
|
+
"expression": 1,
|
|
22
|
+
"template": 1,
|
|
23
|
+
"plan": 1,
|
|
24
|
+
"canonical": 1,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# validate_envelope に渡す graphddb 形式の既定 supported 版(``"<major>.<minor>"``)。
|
|
28
|
+
ENVELOPE_SPEC_VERSION = "1.1"
|
|
29
|
+
|
|
30
|
+
# ── expression(evaluate_expression) ──────────────────────────────────────────
|
|
31
|
+
from .expr_eval import ( # noqa: E402
|
|
32
|
+
ExprFailure,
|
|
33
|
+
cmp_code_points,
|
|
34
|
+
encode_value,
|
|
35
|
+
evaluate as evaluate_expression,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# ── template(render_template) ────────────────────────────────────────────────
|
|
39
|
+
from .template import ( # noqa: E402
|
|
40
|
+
TemplateFailure,
|
|
41
|
+
render_template,
|
|
42
|
+
resolve_partial,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# ── plan(run_plan) ───────────────────────────────────────────────────────────
|
|
46
|
+
from .plan import ( # noqa: E402
|
|
47
|
+
PlanFailure,
|
|
48
|
+
final_tree,
|
|
49
|
+
run_plan,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# ── canonical(canonical_value / canonical_json / py_float_repr) ─────────────
|
|
53
|
+
from .canonical import ( # noqa: E402
|
|
54
|
+
CanonicalFailure,
|
|
55
|
+
canonical_json,
|
|
56
|
+
canonical_value,
|
|
57
|
+
py_float_repr,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# ── envelope(validate_envelope) ──────────────────────────────────────────────
|
|
61
|
+
from .envelope import ( # noqa: E402
|
|
62
|
+
EnvelopeFailure,
|
|
63
|
+
validate_envelope,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# ── guard(Portability Guard) ────────────────────────────────────────────────
|
|
67
|
+
from .guard import ( # noqa: E402
|
|
68
|
+
PortabilityError,
|
|
69
|
+
assert_portable,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# ── conformance runner adapter 共通部 ─────────────────────────────────────────
|
|
73
|
+
from .codec import ( # noqa: E402
|
|
74
|
+
decode_value,
|
|
75
|
+
deep_equals,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
"SPEC_VERSIONS",
|
|
80
|
+
"ENVELOPE_SPEC_VERSION",
|
|
81
|
+
# expression
|
|
82
|
+
"evaluate_expression",
|
|
83
|
+
"encode_value",
|
|
84
|
+
"cmp_code_points",
|
|
85
|
+
"ExprFailure",
|
|
86
|
+
# template
|
|
87
|
+
"render_template",
|
|
88
|
+
"resolve_partial",
|
|
89
|
+
"TemplateFailure",
|
|
90
|
+
# plan
|
|
91
|
+
"run_plan",
|
|
92
|
+
"final_tree",
|
|
93
|
+
"PlanFailure",
|
|
94
|
+
# canonical
|
|
95
|
+
"canonical_value",
|
|
96
|
+
"canonical_json",
|
|
97
|
+
"py_float_repr",
|
|
98
|
+
"CanonicalFailure",
|
|
99
|
+
# envelope
|
|
100
|
+
"validate_envelope",
|
|
101
|
+
"EnvelopeFailure",
|
|
102
|
+
# guard
|
|
103
|
+
"assert_portable",
|
|
104
|
+
"PortabilityError",
|
|
105
|
+
# codec
|
|
106
|
+
"decode_value",
|
|
107
|
+
"deep_equals",
|
|
108
|
+
]
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""canonical.py — canonical-serialization.md の参照実装(Python port)。
|
|
2
|
+
|
|
3
|
+
TS 参照実装 ``ts/src/canonical.ts`` の意味論を完全一致で移植。3 プリミティブ:
|
|
4
|
+
- py_float_repr(n) : CPython repr(float) 相当の正準10進(parity SSoT = Python)
|
|
5
|
+
- canonical_value(v) : key identity(トップレベルのみキーソート) — §2 serializeContractKey
|
|
6
|
+
- canonical_json(v) : fingerprint(全階層キーソート) — §3 canonicalJson
|
|
7
|
+
|
|
8
|
+
値モデルは expr_eval と共有(int / float / str / bool / None / list / dict)。
|
|
9
|
+
runtime 値の直列化:
|
|
10
|
+
- int → 素の 10 進整数(範囲外も 10 進で素通し。DynamoDB N 値層)
|
|
11
|
+
- float → py_float_repr(§4.1)。NaN/±Inf は Failure(§4.1-6)
|
|
12
|
+
|
|
13
|
+
**Python parity SSoT の要点**: py_float_repr は CPython の ``repr(float)`` を SSoT とする。
|
|
14
|
+
Python では ``repr(float)`` そのものが CPython の最短往復10進 + scientific 閾値
|
|
15
|
+
(decpt<=-4 || decpt>16) + 指数 2 桁 pad と一致する。したがって Python port は
|
|
16
|
+
``repr()`` 準拠で実装し、TS 参照アルゴリズムと byte 一致することを検証する。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import math
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
Value = Any
|
|
26
|
+
|
|
27
|
+
_CANONICAL_FAILURE_CODES = frozenset({"NAN_OR_INF", "INVALID_VALUE"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CanonicalFailure(Exception):
|
|
31
|
+
"""canonical 直列化の Failure(NAN_OR_INF / INVALID_VALUE)。"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, code: str, message: str) -> None:
|
|
34
|
+
super().__init__(message)
|
|
35
|
+
self.code = code
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _fail(code: str, message: str) -> "None":
|
|
39
|
+
raise CanonicalFailure(code, message)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _is_int(v: Value) -> bool:
|
|
43
|
+
return isinstance(v, int) and not isinstance(v, bool)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _is_plain_object(v: Value) -> bool:
|
|
47
|
+
return isinstance(v, dict)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── §4.1 float の正準10進(CPython repr(float))────────────────────────────────
|
|
51
|
+
def py_float_repr(n: float) -> str:
|
|
52
|
+
"""CPython の ``repr(float)`` / ``json.dumps(float)`` と byte 一致する 10 進表現。
|
|
53
|
+
|
|
54
|
+
規則(canonical-serialization.md §4.1 / CPython format_float_short('r')):
|
|
55
|
+
1. 最短往復 digits。
|
|
56
|
+
2. scientific 判定: decpt <= -4 || decpt > 16 で指数表記。
|
|
57
|
+
3. 指数は符号つき・最低 2 桁 zero-pad(e+05 / e-07)。
|
|
58
|
+
4. 整数値 float は ``.0`` を保つ(123 → "123.0")。
|
|
59
|
+
5. -0.0 は "-0.0"、+0.0 は "0.0"。
|
|
60
|
+
6. NaN / ±Inf は Failure。
|
|
61
|
+
|
|
62
|
+
実装: CPython の ``repr(float)`` は既にこの正準表現を生成するため、そこから
|
|
63
|
+
digits/decpt を導出し TS 参照実装 (formatFixed/formatScientific) と同一の分岐で
|
|
64
|
+
組み立てる。これにより仕様の閾値ロジックを Python 側でも明示的に踏む。
|
|
65
|
+
"""
|
|
66
|
+
if not isinstance(n, float):
|
|
67
|
+
# int が渡された場合など。仕様上 floatRepr は float のみ。
|
|
68
|
+
_fail("INVALID_VALUE", f"py_float_repr expects a float, got {type(n).__name__}")
|
|
69
|
+
if math.isnan(n) or math.isinf(n):
|
|
70
|
+
_fail("NAN_OR_INF", f"non-finite float cannot be serialized: {n}")
|
|
71
|
+
|
|
72
|
+
# §4.1-5: -0.0 を signbit で捕捉。
|
|
73
|
+
if n == 0.0:
|
|
74
|
+
return "-0.0" if math.copysign(1.0, n) < 0 else "0.0"
|
|
75
|
+
|
|
76
|
+
neg = n < 0
|
|
77
|
+
abs_n = abs(n)
|
|
78
|
+
|
|
79
|
+
digits, decpt = _shortest_digits(abs_n)
|
|
80
|
+
|
|
81
|
+
if decpt <= -4 or decpt > 16:
|
|
82
|
+
body = _format_scientific(digits, decpt)
|
|
83
|
+
else:
|
|
84
|
+
body = _format_fixed(digits, decpt)
|
|
85
|
+
|
|
86
|
+
return "-" + body if neg else body
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _shortest_digits(abs_n: float):
|
|
90
|
+
"""正の有限 float → shortest round-trip digits と decpt(値 = 0.<digits> × 10^decpt)。
|
|
91
|
+
|
|
92
|
+
CPython の ``repr(float)`` が shortest round-trip を出すので、それを指数正規化して
|
|
93
|
+
digits と 10 進指数を取り出す(TS 実装が ``Number#toString`` から取り出すのと同じ手順)。
|
|
94
|
+
"""
|
|
95
|
+
# repr は "123.0" / "1.5" / "1e-07" / "1.25e+30" 等(Python の正準表現)。
|
|
96
|
+
s = repr(abs_n)
|
|
97
|
+
e_idx = s.find("e")
|
|
98
|
+
if e_idx >= 0:
|
|
99
|
+
mantissa = s[:e_idx]
|
|
100
|
+
exp = int(s[e_idx + 1 :])
|
|
101
|
+
else:
|
|
102
|
+
mantissa = s
|
|
103
|
+
exp = 0
|
|
104
|
+
|
|
105
|
+
dot = mantissa.find(".")
|
|
106
|
+
if dot >= 0:
|
|
107
|
+
int_part = mantissa[:dot]
|
|
108
|
+
frac_part = mantissa[dot + 1 :]
|
|
109
|
+
else:
|
|
110
|
+
int_part = mantissa
|
|
111
|
+
frac_part = ""
|
|
112
|
+
|
|
113
|
+
all_digits = int_part + frac_part
|
|
114
|
+
# decpt(暫定) = 整数部桁数 + exp
|
|
115
|
+
decpt = len(int_part) + exp
|
|
116
|
+
|
|
117
|
+
# 先頭ゼロ除去("0025" → "25" で decpt を減らす)。
|
|
118
|
+
lead = 0
|
|
119
|
+
while lead < len(all_digits) - 1 and all_digits[lead] == "0":
|
|
120
|
+
lead += 1
|
|
121
|
+
if lead > 0:
|
|
122
|
+
all_digits = all_digits[lead:]
|
|
123
|
+
decpt -= lead
|
|
124
|
+
|
|
125
|
+
# 末尾ゼロ除去(有効数字のみ)。
|
|
126
|
+
all_digits = all_digits.rstrip("0")
|
|
127
|
+
if all_digits == "":
|
|
128
|
+
return "0", 1
|
|
129
|
+
return all_digits, decpt
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _format_fixed(digits: str, decpt: int) -> str:
|
|
133
|
+
"""固定小数点表記。整数値 float は ``.0`` を付ける(§4.1-4)。"""
|
|
134
|
+
if decpt <= 0:
|
|
135
|
+
return "0." + "0" * (-decpt) + digits
|
|
136
|
+
if decpt >= len(digits):
|
|
137
|
+
return digits + "0" * (decpt - len(digits)) + ".0"
|
|
138
|
+
return digits[:decpt] + "." + digits[decpt:]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _format_scientific(digits: str, decpt: int) -> str:
|
|
142
|
+
"""指数表記。d.ddde±XX(指数は最低 2 桁 zero-pad)(§4.1-2/3)。"""
|
|
143
|
+
first = digits[0]
|
|
144
|
+
rest = digits[1:]
|
|
145
|
+
mant = f"{first}.{rest}" if rest else first
|
|
146
|
+
e = decpt - 1
|
|
147
|
+
sign = "-" if e < 0 else "+"
|
|
148
|
+
mag = str(abs(e)).rjust(2, "0")
|
|
149
|
+
return f"{mant}e{sign}{mag}"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ── §2 canonical_value(serialize_contract_key)— トップレベルのみキーソート ──────
|
|
153
|
+
def canonical_value(v: Value) -> str:
|
|
154
|
+
"""key identity 用の正準直列化。
|
|
155
|
+
|
|
156
|
+
トップレベル object のキーのみ昇順ソートし、compact JSON を返す。ネスト値は挿入順のまま
|
|
157
|
+
(canonical-serialization.md §2)。
|
|
158
|
+
"""
|
|
159
|
+
if _is_plain_object(v):
|
|
160
|
+
keys = sorted(v.keys(), key=_code_point_sort_key)
|
|
161
|
+
parts = [f"{_json_string(k)}:{_encode_nested(v[k])}" for k in keys]
|
|
162
|
+
return "{" + ",".join(parts) + "}"
|
|
163
|
+
return _encode_nested(v)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ── §3 canonical_json — 全階層キーソート ────────────────────────────────────────
|
|
167
|
+
def canonical_json(v: Value) -> str:
|
|
168
|
+
"""fingerprint 用。全 object 階層のキーを昇順ソート(配列は順序保持)。"""
|
|
169
|
+
if _is_plain_object(v):
|
|
170
|
+
keys = sorted(v.keys(), key=_code_point_sort_key)
|
|
171
|
+
parts = [f"{_json_string(k)}:{canonical_json(v[k])}" for k in keys]
|
|
172
|
+
return "{" + ",".join(parts) + "}"
|
|
173
|
+
if isinstance(v, list):
|
|
174
|
+
return "[" + ",".join(canonical_json(e) for e in v) + "]"
|
|
175
|
+
return _scalar_json(v)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ── 内部: ネスト値の直列化(canonical_value 用。ネスト object は挿入順)──────────────
|
|
179
|
+
def _encode_nested(v: Value) -> str:
|
|
180
|
+
if _is_plain_object(v):
|
|
181
|
+
parts = [f"{_json_string(k)}:{_encode_nested(v[k])}" for k in v.keys()]
|
|
182
|
+
return "{" + ",".join(parts) + "}"
|
|
183
|
+
if isinstance(v, list):
|
|
184
|
+
return "[" + ",".join(_encode_nested(e) for e in v) + "]"
|
|
185
|
+
return _scalar_json(v)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _scalar_json(v: Value) -> str:
|
|
189
|
+
"""スカラ(int/float/str/bool/None)の JSON 直列化。float は py_float_repr。"""
|
|
190
|
+
if v is None:
|
|
191
|
+
return "null"
|
|
192
|
+
if isinstance(v, bool):
|
|
193
|
+
return "true" if v else "false"
|
|
194
|
+
if isinstance(v, str):
|
|
195
|
+
return _json_string(v)
|
|
196
|
+
if _is_int(v):
|
|
197
|
+
return str(v) # int: 素の 10 進整数(値層 JSON number)
|
|
198
|
+
if isinstance(v, float):
|
|
199
|
+
return py_float_repr(v) # float: CPython repr(§4.1)
|
|
200
|
+
_fail("INVALID_VALUE", "cannot serialize value of unknown type")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _json_string(s: str) -> str:
|
|
204
|
+
"""文字列を JSON 直列化(ensure_ascii=False = 非 ASCII 素通し)。
|
|
205
|
+
|
|
206
|
+
TS の ``JSON.stringify`` と一致させるため、Python の ``json.dumps`` を
|
|
207
|
+
``ensure_ascii=False`` で使う(compact separators は文字列単体では無関係)。
|
|
208
|
+
"""
|
|
209
|
+
return json.dumps(s, ensure_ascii=False)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _code_point_sort_key(s: str):
|
|
213
|
+
"""Unicode code-point 順のソートキー(正準キーソートの規範。全言語共通)。
|
|
214
|
+
|
|
215
|
+
canonical-serialization.md §2/§6: オブジェクトキーのソートは **code-point 順** が規範
|
|
216
|
+
(expression-ir.md §6 の文字列比較 = code-point 順と整合。UTF-16 code unit 順ではない —
|
|
217
|
+
astral-plane(U+10000+)キーで両者は食い違う)。Python の ``str`` はタプル化した
|
|
218
|
+
code point 列で比較されるので、ここでは code point のタプルを返す(``sorted`` の既定
|
|
219
|
+
``str`` 比較と同一だが、規範が code-point であることを明示するための明示キー)。
|
|
220
|
+
BMP 内では code-point == UTF-16 なので既存 vector は不変。
|
|
221
|
+
"""
|
|
222
|
+
return [ord(ch) for ch in s]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""codec.py — conformance runner adapter の共通部(Python port)。
|
|
2
|
+
|
|
3
|
+
TS 参照実装 ``ts/src/codec.ts`` の意味論を移植。golden vector の型付き値エンコード
|
|
4
|
+
(``{int:"..."}`` / ``{float:n}`` / ``{nan}`` / ``{inf}`` / 素の JSON number 分類)と
|
|
5
|
+
runtime 値の相互変換、および int/float を型で区別する深い等価判定。全言語 runner が
|
|
6
|
+
同じ判定規則で同じ pass/fail を出すための共有ロジック(PROTOCOL.md §4)。
|
|
7
|
+
|
|
8
|
+
**Python の int/float 分類の要点**: Python の ``json.load`` は整数値の JSON number を
|
|
9
|
+
``int``、小数部ありを ``float`` として復元する(TS のように精度損失しない)。したがって
|
|
10
|
+
``decode_value`` は「Python の型をそのまま尊重しつつ ``{int}``/``{float}``/``{nan}``/``{inf}``
|
|
11
|
+
ラッパを解く」だけでよい。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
Value = Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_int(v: Value) -> bool:
|
|
23
|
+
return isinstance(v, int) and not isinstance(v, bool)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def decode_value(x: Any) -> Value:
|
|
27
|
+
"""golden vector の JSON 値表現を runtime 値へ復元する。
|
|
28
|
+
|
|
29
|
+
- 素の JSON number: 整数値 → int、小数部あり → float(Python の json が既に区別)。
|
|
30
|
+
- ``{int:"..."}`` → int(安全整数域外も正確)。
|
|
31
|
+
- ``{float:n}`` → float(整数値 float を明示)。
|
|
32
|
+
- ``{nan}`` / ``{inf: ±1}`` → NaN / ±Inf。
|
|
33
|
+
"""
|
|
34
|
+
if x is None or isinstance(x, bool) or isinstance(x, str):
|
|
35
|
+
return x
|
|
36
|
+
if _is_int(x):
|
|
37
|
+
return int(x)
|
|
38
|
+
if isinstance(x, float):
|
|
39
|
+
return x
|
|
40
|
+
if isinstance(x, list):
|
|
41
|
+
return [decode_value(e) for e in x]
|
|
42
|
+
if isinstance(x, dict):
|
|
43
|
+
keys = list(x.keys())
|
|
44
|
+
if len(keys) == 1:
|
|
45
|
+
k = keys[0]
|
|
46
|
+
if k == "int" and isinstance(x["int"], str):
|
|
47
|
+
return int(x["int"])
|
|
48
|
+
if k == "float" and isinstance(x["float"], (int, float)) and not isinstance(
|
|
49
|
+
x["float"], bool
|
|
50
|
+
):
|
|
51
|
+
return float(x["float"])
|
|
52
|
+
if k == "nan":
|
|
53
|
+
return float("nan")
|
|
54
|
+
if k == "inf":
|
|
55
|
+
return float("-inf") if x["inf"] < 0 else float("inf")
|
|
56
|
+
return {k: decode_value(v) for k, v in x.items()}
|
|
57
|
+
raise ValueError(f"cannot decode value: {x!s}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def deep_equals(a: Value, b: Value) -> bool:
|
|
61
|
+
"""int/float を型で区別する深い等価(PROTOCOL.md §4)。
|
|
62
|
+
|
|
63
|
+
int と float は等しくない。NaN は NaN と等しいとみなす(Failure 判定用)。
|
|
64
|
+
"""
|
|
65
|
+
if a is None or b is None:
|
|
66
|
+
return a is b
|
|
67
|
+
# bool は int の前に判定(bool ≠ int)。
|
|
68
|
+
if isinstance(a, bool) or isinstance(b, bool):
|
|
69
|
+
return isinstance(a, bool) and isinstance(b, bool) and a == b
|
|
70
|
+
if _is_int(a) or _is_int(b):
|
|
71
|
+
return _is_int(a) and _is_int(b) and a == b
|
|
72
|
+
if isinstance(a, float) or isinstance(b, float):
|
|
73
|
+
if not (isinstance(a, float) and isinstance(b, float)):
|
|
74
|
+
return False
|
|
75
|
+
return a == b or (math.isnan(a) and math.isnan(b))
|
|
76
|
+
if isinstance(a, str) or isinstance(b, str):
|
|
77
|
+
return isinstance(a, str) and isinstance(b, str) and a == b
|
|
78
|
+
if isinstance(a, list):
|
|
79
|
+
if not isinstance(b, list) or len(a) != len(b):
|
|
80
|
+
return False
|
|
81
|
+
return all(deep_equals(x, y) for x, y in zip(a, b))
|
|
82
|
+
if isinstance(b, list):
|
|
83
|
+
return False
|
|
84
|
+
if isinstance(a, dict) and isinstance(b, dict):
|
|
85
|
+
if set(a.keys()) != set(b.keys()):
|
|
86
|
+
return False
|
|
87
|
+
return all(deep_equals(a[k], b[k]) for k in a)
|
|
88
|
+
return False
|