Internal hygiene + future-proofing. No serving path, no new capability — the 15-case reader is structurally cleaner and the projection no longer recovers meaning by re-parsing a string. - New generate/quantitative_expr.py: a typed Expr IR (Literal/Symbol/Add/Sub/SumOf) with to_canonical_string (BYTE-IDENTICAL to the legacy "ref + delta" / "ref - delta" / "a + b" format), dependencies, operation_kind, and to_relation (the structured projection). - The reader builds the Expr per equation as the SOURCE OF MEANING; rhs_canonical, dependencies, and operation_kind are all DERIVED from it (BoundEquation stays a string — the binding-graph's deliberate decoupling layer is untouched). QuantComprehension carries the IR as equation_exprs. - to_relational_metric reads the IR via to_relation — the rhs_canonical string-reparse is GONE. An unhandled equation shape refuses (None). - The dead _rhs string builder is removed. Gates held: relational_metric answer lane 15/15 wrong=0; setup-oracle 15/15 setup_wrong=0; malformed-target refusals intact; realize-binding-graph + architectural invariants green (95). rhs_canonical is byte-identical, so the binding-graph + downstream hashes are unchanged. No model change. This is the foundation PR-5's R1 frames build on — structured equations, not string parsing — and only after independent R1 gold is hand-authored.
128 lines
3.9 KiB
Python
128 lines
3.9 KiB
Python
"""Typed expression IR for the arithmetic reader (PR-4).
|
|
|
|
The READER's source of meaning for an equation's right-hand side. The binding-graph
|
|
deliberately keeps ``BoundEquation.rhs_canonical`` a *string* (a decoupling layer that
|
|
does not import the symbolic substrate); this IR lives ABOVE that boundary in the reader,
|
|
serializes DOWN to the canonical string (``to_canonical_string``), and is read directly by
|
|
the projection (``to_relation``) so meaning is never recovered by re-parsing the string.
|
|
|
|
``to_canonical_string`` is byte-identical to the strings the reader emitted before PR-4
|
|
("ref + delta", "ref - delta", "a + b") — so the binding-graph and every downstream hash
|
|
are unchanged. Deterministic; no clock, no randomness.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any, Union
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Literal:
|
|
"""A grounded integer operand (a value sourced from the text)."""
|
|
|
|
value: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Symbol:
|
|
"""A reference to another bound symbol."""
|
|
|
|
symbol_id: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Add:
|
|
left: "Expr"
|
|
right: "Expr"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Sub:
|
|
left: "Expr"
|
|
right: "Expr"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SumOf:
|
|
"""An aggregate over ≥2 symbols (the part-whole total)."""
|
|
|
|
parts: tuple[Symbol, ...]
|
|
|
|
|
|
Expr = Union[Literal, Symbol, Add, Sub, SumOf]
|
|
|
|
|
|
def to_canonical_string(expr: Expr) -> str:
|
|
"""Serialize to the canonical rhs string — byte-identical to the pre-IR format."""
|
|
match expr:
|
|
case Literal(value):
|
|
return str(value)
|
|
case Symbol(symbol_id):
|
|
return symbol_id
|
|
case Add(left, right):
|
|
return f"{to_canonical_string(left)} + {to_canonical_string(right)}"
|
|
case Sub(left, right):
|
|
return f"{to_canonical_string(left)} - {to_canonical_string(right)}"
|
|
case SumOf(parts):
|
|
return " + ".join(to_canonical_string(p) for p in parts)
|
|
raise TypeError(f"not an Expr: {expr!r}") # pragma: no cover - exhaustive above
|
|
|
|
|
|
def dependencies(expr: Expr) -> frozenset[str]:
|
|
"""The symbols the expression reads (the equation's dependency set)."""
|
|
match expr:
|
|
case Literal(_):
|
|
return frozenset()
|
|
case Symbol(symbol_id):
|
|
return frozenset({symbol_id})
|
|
case Add(left, right) | Sub(left, right):
|
|
return dependencies(left) | dependencies(right)
|
|
case SumOf(parts):
|
|
out: frozenset[str] = frozenset()
|
|
for p in parts:
|
|
out |= dependencies(p)
|
|
return out
|
|
raise TypeError(f"not an Expr: {expr!r}") # pragma: no cover
|
|
|
|
|
|
def operation_kind(expr: Expr) -> str:
|
|
"""The binding-graph ``operation_kind`` an expression lowers to."""
|
|
match expr:
|
|
case Add(_, _) | SumOf(_):
|
|
return "add"
|
|
case Sub(_, _):
|
|
return "subtract"
|
|
case _:
|
|
raise TypeError(f"expression has no operation_kind: {expr!r}")
|
|
|
|
|
|
def to_relation(lhs: str, expr: Expr) -> dict[str, Any] | None:
|
|
"""Project to a relational_metric relation, read from STRUCTURE (no string parse).
|
|
|
|
``None`` for a shape the projection does not handle — the caller refuses rather than
|
|
emit a guessed relation (wrong=0 boundary).
|
|
"""
|
|
match expr:
|
|
case Add(Symbol(ref), Literal(delta)):
|
|
return {"kind": "more_than", "entity": lhs, "ref": ref, "delta": delta}
|
|
case Sub(Symbol(ref), Literal(delta)):
|
|
return {"kind": "fewer_than", "entity": lhs, "ref": ref, "delta": delta}
|
|
case SumOf(parts):
|
|
return {"kind": "sum_of", "entity": lhs, "parts": [p.symbol_id for p in parts]}
|
|
case _:
|
|
return None
|
|
|
|
|
|
__all__ = [
|
|
"Add",
|
|
"Expr",
|
|
"Literal",
|
|
"Sub",
|
|
"SumOf",
|
|
"Symbol",
|
|
"dependencies",
|
|
"operation_kind",
|
|
"to_canonical_string",
|
|
"to_relation",
|
|
]
|