The proof_chain keystone: a hand-rolled ROBDD canonicalizer mirroring math_symbolic_equivalence one domain over (normalize -> canonical key -> byte-equality -> three-valued verdict; REFUSED preserves wrong=0). - generate/logic_canonical.py: formula -> ROBDD identity under sorted-atom ordering; LogicError/LogicBudgetError refusals; inspectable canonical key. - generate/logic_equivalence.py: EQUIVALENT/NOT_EQUIVALENT/REFUSED wrapper. - tests/test_logic_canonical.py: 33 standalone tests (canonicity laws, discrimination, terminals, determinism, refusals); mutation-verified non-vacuous. - ADR-0201: canonicalizer decision (ROBDD not CNF/DNF; hand-rolled; propositional-only honesty boundary). - ADR-0202: proposition representation contract — single source the canonicalizer and the proof corpus conform to (formula grammar + atom layer binding to ADR-0144 EpistemicNode + honesty boundary). Additive: no existing file touched, zero consumers. Standalone keystone only; binding-graph wiring, acyclicity refusal, and inference rules deferred. smoke: 67 passed.
88 lines
2.6 KiB
Python
88 lines
2.6 KiB
Python
"""ADR-0201 — Propositional equivalence check.
|
|
|
|
Boolean-logic twin of :mod:`generate.math_symbolic_equivalence`. Given two
|
|
propositional formulas A and B, produces an :class:`EquivalenceVerdict` of
|
|
EQUIVALENT, NOT_EQUIVALENT, or REFUSED, by canonicalizing each to its ROBDD
|
|
identity (:mod:`generate.logic_canonical`) and comparing the canonical keys by
|
|
byte-equality.
|
|
|
|
REFUSED preserves ``wrong == 0``: out-of-grammar input or a diagram that exceeds
|
|
the node budget refuses rather than emitting a verdict — the same posture as the
|
|
algebra sibling refusing on out-of-scope expressions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from typing import Final
|
|
|
|
from generate.logic_canonical import (
|
|
DEFAULT_MAX_NODES,
|
|
LogicBudgetError,
|
|
LogicError,
|
|
canonicalize,
|
|
)
|
|
|
|
|
|
class Verdict(str, Enum):
|
|
EQUIVALENT = "equivalent"
|
|
NOT_EQUIVALENT = "not_equivalent"
|
|
REFUSED = "refused"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class EquivalenceVerdict:
|
|
verdict: Verdict
|
|
canonical_a: str | None
|
|
canonical_b: str | None
|
|
reason: str
|
|
|
|
|
|
REFUSED_VERDICTS: Final[frozenset[Verdict]] = frozenset({Verdict.REFUSED})
|
|
"""Helper set for callers that need to gate on refusal vs decision."""
|
|
|
|
|
|
def check_equivalence(
|
|
formula_a: str,
|
|
formula_b: str,
|
|
*,
|
|
max_nodes: int = DEFAULT_MAX_NODES,
|
|
) -> EquivalenceVerdict:
|
|
"""Return whether two propositional formulas are logically equivalent.
|
|
|
|
Equivalence is decided by ROBDD canonical-key byte-equality, which is exact
|
|
for propositional logic. Refuses (rather than guesses) on malformed input or
|
|
on diagram blowup beyond ``max_nodes``.
|
|
"""
|
|
try:
|
|
canon_a = canonicalize(formula_a, max_nodes=max_nodes).canonical_key
|
|
canon_b = canonicalize(formula_b, max_nodes=max_nodes).canonical_key
|
|
except LogicBudgetError as exc:
|
|
return EquivalenceVerdict(
|
|
verdict=Verdict.REFUSED,
|
|
canonical_a=None,
|
|
canonical_b=None,
|
|
reason=f"canonicalization_budget_exceeded: {exc}",
|
|
)
|
|
except LogicError as exc:
|
|
return EquivalenceVerdict(
|
|
verdict=Verdict.REFUSED,
|
|
canonical_a=None,
|
|
canonical_b=None,
|
|
reason=f"canonicalize refused: {exc}",
|
|
)
|
|
|
|
if canon_a == canon_b:
|
|
return EquivalenceVerdict(
|
|
verdict=Verdict.EQUIVALENT,
|
|
canonical_a=canon_a,
|
|
canonical_b=canon_b,
|
|
reason="",
|
|
)
|
|
return EquivalenceVerdict(
|
|
verdict=Verdict.NOT_EQUIVALENT,
|
|
canonical_a=canon_a,
|
|
canonical_b=canon_b,
|
|
reason="",
|
|
)
|