The first capability slice on the R1 arc, gated by the setup-oracle: turn the
"twice / N times as many" reading from REFUSED into a correct setup, without a single
misread. Builds on the typed IR (PR-4) and the R1 gold (PR-5b).
- IR: a Mul(symbol, literal-factor) node — to_canonical_string "ref * factor",
operation_kind "multiply", dependencies {ref}, to_relation -> times_as_many. The
product keeps the symbol's unit (count * scalar = count), admitted by the REAL
check_admissibility multiply path (the literal factor is dimensionless, not a dep).
- Reader: a multiplicative template "Y has <factor> as many <unit> as X" (factor word:
twice/double/triple/quadruple) and "Y has <N> times as many <unit> as X", checked
BEFORE the digit gate (the factor may be a word). 'half' (a /2) is deliberately
deferred — divide-by-literal is a separate admissibility path.
- setup-oracle: relation_signature now canonicalizes times_as_many.
Setup-oracle R1 result: 2 setup_correct (r1-01 twice; r1-05 the multi-step chain
ivy/jon=3*ivy/kim=jon+2), 0 setup_WRONG, 8 setup_refused. Every hard negative stays a
safe refusal: missing-base (Rosa ungrounded), ambiguous referent, distractor, inverse,
partition, 'altogether'/'in total' phrasings, and 'half' (divide). wrong=0 held through
the first capability addition.
Gates green: setup-oracle R1 setup_wrong=0; 15-case setup gate 15/15 setup_wrong=0;
relational_metric answer lane 15/15 wrong=0; binding-graph admissibility + realize +
architectural invariants + chat-runtime + pipeline (122+). No serving path touched (this
reader feeds the relational_metric / setup-oracle lanes, not the candidate-graph serving).
145 lines
4.6 KiB
Python
145 lines
4.6 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 Mul:
|
||
"""A scalar multiple of a symbol — the multiplicative comparative ("twice/N times
|
||
as many"). ``left`` is the referenced symbol, ``right`` a dimensionless literal
|
||
factor; the product keeps the symbol's unit (``count × scalar = count``)."""
|
||
|
||
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, Mul, 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 Mul(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) | Mul(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 Mul(_, _):
|
||
return "multiply"
|
||
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 Mul(Symbol(ref), Literal(factor)):
|
||
return {"kind": "times_as_many", "entity": lhs, "ref": ref, "factor": factor}
|
||
case SumOf(parts):
|
||
return {"kind": "sum_of", "entity": lhs, "parts": [p.symbol_id for p in parts]}
|
||
case _:
|
||
return None
|
||
|
||
|
||
__all__ = [
|
||
"Add",
|
||
"Expr",
|
||
"Literal",
|
||
"Mul",
|
||
"Sub",
|
||
"SumOf",
|
||
"Symbol",
|
||
"dependencies",
|
||
"operation_kind",
|
||
"to_canonical_string",
|
||
"to_relation",
|
||
]
|