core/generate/quantitative_expr.py
Shay 06450928c9 refactor(comprehension): typed expression IR as the source of meaning (PR-4)
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.
2026-06-06 16:57:53 -07:00

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",
]