core/tests/test_constraint_comprehension_model.py
Shay e71531c0c9 feat(constraint): R2 linear-constraint IR — typed problem model (R2 C1)
generate/constraint_comprehension/{expr,model}.py: frozen, slots'd dataclasses, no behavior. expr = LinearExpr (sum(coeff*symbol)+constant) + LinearConstraint (lhs eq rhs, optional source_span). model = Unknown (symbol/entity/unit/finite-integer domain), AttributeFact (per-category coefficient provenance), ConstraintQuery (symbol+unit), ConstraintProblem (unknowns/facts/constraints/query).

Terms pinned as (symbol, coefficient) to match the gold serialization. Query is a minimal dedicated type, not R1's BoundUnknown (no degenerate fit). Off-serving package; no generate.derivation / reliability_gate import. 9 IR tests (shape + frozen + defaults).
2026-06-07 07:10:20 -07:00

96 lines
3.5 KiB
Python

"""Unit tests for the R2 constraint IR (C1) — pure dataclasses, no behavior.
Pins the typed shape the gold/oracle (C2) and solver (C3) build on: unknowns with a
finite-integer domain, attribute coefficients, a canonical linear system, and a minimal
query. Frozen-ness is load-bearing (the immutability doctrine — the IR is value data); the
bus + chickens systems are pinned as IR so later slices can assert the reader reconstructs
exactly these.
"""
from __future__ import annotations
import dataclasses
from typing import Any
import pytest
from generate.constraint_comprehension import (
AttributeFact,
ConstraintProblem,
ConstraintQuery,
LinearConstraint,
LinearExpr,
Unknown,
)
def _bus_problem() -> ConstraintProblem:
# 6 buses total; large holds 50, small holds 30; 260 students; ask large.
return ConstraintProblem(
unknowns=(
Unknown("large_bus", "large bus", "bus", "nonnegative_integer"),
Unknown("small_bus", "small bus", "bus", "nonnegative_integer"),
),
facts=(
AttributeFact("large_bus", "student", 50),
AttributeFact("small_bus", "student", 30),
),
constraints=(
LinearConstraint(LinearExpr((("large_bus", 1), ("small_bus", 1))), "eq", 6),
LinearConstraint(LinearExpr((("large_bus", 50), ("small_bus", 30))), "eq", 260),
),
query=ConstraintQuery("large_bus", "bus"),
)
def test_bus_problem_ir_shape() -> None:
p = _bus_problem()
assert tuple(u.symbol for u in p.unknowns) == ("large_bus", "small_bus")
assert all(u.domain == "nonnegative_integer" for u in p.unknowns)
assert p.constraints[0].relation == "eq" and p.constraints[0].rhs == 6
assert p.constraints[1].lhs.terms == (("large_bus", 50), ("small_bus", 30))
assert p.query == ConstraintQuery("large_bus", "bus")
def test_chickens_problem_ir_shape() -> None:
# 18 animals; chickens 2 legs, cows 4 legs; 50 legs; ask chickens.
p = ConstraintProblem(
unknowns=(
Unknown("chicken", "chicken", "animal", "nonnegative_integer"),
Unknown("cow", "cow", "animal", "nonnegative_integer"),
),
facts=(AttributeFact("chicken", "leg", 2), AttributeFact("cow", "leg", 4)),
constraints=(
LinearConstraint(LinearExpr((("chicken", 1), ("cow", 1))), "eq", 18),
LinearConstraint(LinearExpr((("chicken", 2), ("cow", 4))), "eq", 50),
),
query=ConstraintQuery("chicken", "animal"),
)
assert {f.measured_unit for f in p.facts} == {"leg"}
assert p.constraints[1].lhs.terms == (("chicken", 2), ("cow", 4))
def test_linear_expr_constant_defaults_zero() -> None:
assert LinearExpr((("x", 1),)).constant == 0
def test_constraint_source_span_defaults_none() -> None:
# Gold-authored constraints carry no input span; the reader (C5+) populates it.
assert LinearConstraint(LinearExpr((("x", 1),)), "eq", 3).source_span is None
@pytest.mark.parametrize(
"obj",
[
Unknown("x", "x", "item", "integer"),
AttributeFact("x", "leg", 2),
ConstraintQuery("x", "item"),
LinearExpr((("x", 1),)),
LinearConstraint(LinearExpr((("x", 1),)), "eq", 1),
],
)
def test_ir_dataclasses_are_frozen(obj: Any) -> None:
# Immutability doctrine: the IR is value data — mutation must raise, never silently alias.
field = dataclasses.fields(obj)[0].name
with pytest.raises(dataclasses.FrozenInstanceError):
setattr(obj, field, getattr(obj, field))