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).
96 lines
3.5 KiB
Python
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))
|