17.3. Constraint Propagation
Constraint propagation works on the True
and False
buckets of the whiteboard.
Whenever a term is added to one of those buckets, grind
fires dozens of small forward rules that derive further information from its logical consequences:
- Boolean connectives
- Inductive Types
If terms formed by applications of two different constructors of the same inductive type (e.g.
none
andsome
) are placed in the same equivalence class, a contradiction is derived. If two terms formed by applications of the same constructor are placed in the same equivalence class, then their arguments are also made equal.- Projections
From
h : (x, y) = (x', y')
we derivex = x'
andy = y'
.- Casts
Any term
cast h a : β
is equated witha : α
immediately (using heterogeneous equality).- Definitional equality
Definitional reduction is propagated, so
(a, b).1
is equated witha
. The η-equality rule for structures is not automatically used, so ifp
is an instance of a structureS
with two fields, thenp
is not equated with⟨p.1, p.2⟩
. However, taggingS
with@[grind ext]
causes the E-matching engine to prove these goals.
Below is a representative slice of the propagators that demonstrates their overall style. Each follows the same skeleton.
-
It inspect the truth value of sub‑expressions.
-
If further facts can be derived, it either equates terms (connecting them on the metaphorical whiteboard) using (
pushEq
), or it indicates truth values using (pushEqTrue
/pushEqFalse
). These steps produce proof terms using internal helper lemmas such asGrind.and_eq_of_eq_true_left
. -
If a contradiction arises, the goal is closed using (
closeGoal
).
Upward propagation derives facts about a term from facts about sub-terms, while downward propagation derives facts about sub-terms from facts about a term.
/-- 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
-- b = True ⇒ (a ∧ b) = a
pushEq e a <|
mkApp3 (mkConst ``Grind.and_eq_of_eq_true_right)
a b (← mkEqTrueProof b)
else if (← isEqFalse a) then
-- a = False ⇒ (a ∧ b) = False
pushEqFalse e <|
mkApp3 (mkConst ``Grind.and_eq_of_eq_false_left)
a b (← mkEqFalseProof a)
else if (← isEqFalse b) then
-- b = False ⇒ (a ∧ b) = False
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
-- (a ∧ b) = True ⇒ a = True
pushEqTrue a <| mkApp3
(mkConst ``Grind.eq_true_of_and_eq_true_left) a b h
-- (a ∧ b) = True ⇒ B = True
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 |
---|---|---|
|
| Uses the truth table for disjunction to derive further truth values |
|
|
Ensures that |
|
| Bridges Booleans, detects constructor clash |
|
| Equates the term with the chosen branch once the condition's truth value is known |
|
Values of structures tagged |
Generates η‑expansion |
Many specialized variants for Bool
mirror these rules exactly (e.g. propagateBoolAndUp
).
17.3.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 internalized.
Setting the option trace.grind.eqc
to true
causes grind
to print a line every time two equivalence classes merge, which is handy for seeing propagation in action.
The set of propagation rules is expanded and refined over time, so the InfoView will show increasingly rich True
and False
buckets.
The full equivalence classes are displayed automatically only when grind
fails, and only for the first subgoal that it could not close—use this output to inspect missing facts and understand why the subgoal remains open.
Identifying Missing Facts
In this example, grind
fails:
example :
x = y ∧ y = z →
w = x ∨ w = v →
w = z := α✝:Sort u_1x:α✝y:α✝z:α✝w:α✝v:α✝⊢ x = y ∧ y = z → w = x ∨ w = v → w = z
α✝:Sort u_1x:α✝y:α✝z:α✝w:α✝v:α✝⊢ x = y ∧ y = z → w = x ∨ w = v → w = z
The resulting error message includes the identified equivalence classes along with the true and false propositions:
Both x = y
and y = z
were discovered by constraint propagation from the x = y ∧ y = z
premise.
In this proof, grind
performed a case split on w = x ∨ w = v
.
In the second branch, it could not place w
and z
in the same equivalence class.
