17.6. Constraint Propagation
Constraint propagation works on the True and False buckets of the white‑board. Whenever a literal is added to one of those buckets, grind
fires dozens of small forward rules to push its logical consequences:
-
Boolean connectives — e.g. if
A
is True, markA ∨ B
True; ifA ∧ B
is True, mark bothA
andB
True; ifA ∧ B
is False, at least one ofA
,B
becomes False. -
Inductive datatypes — two different constructors (
none
vssome _
) collapsing into the same class yield contradiction; equal tuples yield equal components. -
Projections and casts — from
h : (x, y) = (x', y')
we derivex = x'
andy = y'
; any termcast h a
is merged witha
immediately (using a heterogeneous equality) so both live in the same class. -
Structural eta and definitional equalities —
⟨a, b⟩.1
propagates toa
, etc.
Below is a representative slice of the propagators so you can see the style they follow. Each follows the same skeleton: inspect the truth‑value of sub‑expressions, push equalities (pushEq
) or truth‑values (pushEqTrue
/ pushEqFalse
), and optionally close the goal if a contradiction (closeGoal
) arises. A few high‑signal examples:
/-- Propagate equalities *upwards* for conjunctions. -/
builtin_grind_propagator propagateAndUp ↑And := fun e => do
let_expr And a b := e | return ()
if (← isEqTrue a) then
-- a = True ⇒ (a ∧ b) = b
pushEq e b <|
mkApp3 (mkConst ``Grind.and_eq_of_eq_true_left)
a b (← mkEqTrueProof a)
else if (← isEqTrue b) then
pushEq e a <|
mkApp3 (mkConst ``Grind.and_eq_of_eq_true_right)
a b (← mkEqTrueProof b)
else if (← isEqFalse a) then
pushEqFalse e <|
mkApp3 (mkConst ``Grind.and_eq_of_eq_false_left)
a b (← mkEqFalseProof a)
else if (← isEqFalse b) then
pushEqFalse e <|
mkApp3 (mkConst ``Grind.and_eq_of_eq_false_right)
a b (← mkEqFalseProof b)
/--
Truth flows *down* when the whole `And` is proven `True`.
-/
builtin_grind_propagator propagateAndDown ↓And :=
fun e => do
if (← isEqTrue e) then
let_expr And a b := e | return ()
let h ← mkEqTrueProof e
pushEqTrue a <| mkApp3
(mkConst ``Grind.eq_true_of_and_eq_true_left) a b h
pushEqTrue b <| mkApp3
(mkConst ``Grind.eq_true_of_and_eq_true_right) a b h
Other frequently‑triggered propagators follow the same pattern:
| Propagator | Handles | Notes |
| ------------------------------------------------- | ------------------------------- | ---------------------------------------------- |
| propagateOrUp
/ propagateOrDown
| a ∨ b
| True/False pushes for disjunctions |
| propagateNotUp
/ propagateNotDown
| ¬ a
| Links ¬ a
with the Boolean of a
|
| propagateEqUp
/ propagateEqDown
| a = b
| Bridges Booleans, detects constructor clash |
| propagateIte
/ propagateDIte
| ite
/ dite
| Replaces chosen branch once condition is fixed |
| propagateEtaStruct
| structures tagged [grind ext]
| Generates η‑expansion a = ⟨a.1, …⟩
|
Many specialised variants for Bool
mirror these rules exactly (e.g. propagateBoolAndUp
).
17.6.1. Propagation‑only examples
These goals are closed purely by constraint propagation—no case splits, no theory solvers:
example (a : Bool) : (a && !a) = false := a:Bool⊢ (a && !a) = false
All goals completed! 🐙
-- Conditional (ite):
-- once the condition is true, ite picks the 'then' branch.
example (c : Bool) (t e : Nat) (h : c = true) :
(if c then t else e) = t := c:Boolt:Nate:Nath:c = true⊢ (if c = true then t else e) = t
All goals completed! 🐙
-- Negation propagates truth downwards.
example (a : Bool) (h : (!a) = true) : a = false := a:Boolh:(!a) = true⊢ a = false
All goals completed! 🐙
These snippets run instantly because the relevant propagators (propagateBoolAndUp
, propagateIte
, propagateBoolNotDown
) fire as soon as the hypotheses are internalised.
Note If you toggle
set_option trace.grind.eqc true
,grind
will print a line every time two equivalence classes merge—handy for seeing propagation in action.
Implementation tip grind
is still under active development. Until the API has stabilised we recommend refraining from custom elaborators or satellite solvers. If you really need a project‑local propagator, use the user‑facing grind_propagator
command rather than builtin_grind_propagator
(the latter is reserved for Lean’s own code). When adding new propagators keep them small and orthogonal—they should fire in ≤1 µs and either push one fact or close the goal. This keeps the propagation phase predictable and easy to debug.
We continuously expand and refine the rule set—expect the Info View to show increasingly rich True
/False
buckets over time. The full equivalence classes are displayed automatically only when grind
fails, and only for the first subgoal it could not close—use this output to inspect missing facts and understand why the subgoal remains open.
