Skip to content

Commit 59391b0

Browse files
volodeykaclaude
andcommitted
feat: vcgen frames cost monads over a stateful base
Extend `@[frameproc]` frame inference to a *nested-base* assertion lattice (e.g. `ComplexiT (StateM σ)`, lattice `Tick → σ → Prop`). There the frame operator is applied to more coordinates than the registered direct split lemma handles (`pre ⊑ cCostConj c R n s`, not `… n`), so `splitLatticeOp?` returns `none` and the raw goal — with an un-specced inner `wp` — is dumped on `finish`. Add two fallback steps in `solve`, run after the direct split and the wp phase (so flat lattices are untouched): for a registered frame `conj` or its residual wand, `mkReduceRule` builds a backward rule from a registered reduce-equation (`FrameProc.conjReduce` / `impReduce`) whose conclusion matches the goal head, and `applyChecked`s it. The single premise is the reduced subgoal — the built-in connective for the conj (the meet split then peels it over all coordinates), or the shifted body for the wand (exposing the inner `wp` for the spec step). `mkReduceRule` reuses `mkBackwardRuleForLattice`'s composition tail (`liftEqByArgs` + `Eq.mp` + `mkBackwardRuleFromExpr`), minus the `relLemma`. `tests/bench/vcgen/mwe_nested_frame.lean` documents the 2-level setup. Flat `TickM` is unchanged (the fallbacks never fire). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a391359 commit 59391b0

6 files changed

Lines changed: 250 additions & 2 deletions

File tree

src/Lean/Elab/Tactic/Do/Internal/VCGen/Context.lean

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@ public structure VCGen.Context where
208208
keyed by the `conj` head constant. Consulted by `splitLatticeOp?` (dispatching on the inner
209209
operator) so a custom frame's magic wand decomposes instead of surfacing in a VC. -/
210210
customImpSplits : Std.HashMap Name VCGen.LatticeSplit := {}
211+
/-- Conj-reduction equations (`FrameProc.conjReduce`) for custom frame operators, keyed by the `conj`
212+
head constant. Consulted by `reduceFrameConj?` to reduce `conj F rest` to its built-in connective over
213+
a nested-base lattice. -/
214+
customConjReduces : Std.HashMap Name Name := {}
215+
/-- Wand-reduction equations (`FrameProc.impReduce`) for custom frame operators, keyed by the `conj`
216+
head constant. Consulted by `reduceFrameImp?` to peel a residual
217+
`PreservesSup.upperAdjoint conj F rest` over a nested-base lattice. -/
218+
customImpReduces : Std.HashMap Name Name := {}
211219
/-- User-customizable simp methods used to pre-simplify hypotheses. -/
212220
hypSimpMethods : Option Sym.Simp.Methods := none
213221
/-- The `trivial` config option: when `true` (default), `Driver.emitVC` runs

src/Lean/Elab/Tactic/Do/Internal/VCGen/FrameProc.lean

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ structure FrameProc where
4040
/-- The lattice split decomposing the residual `PreservesSup.upperAdjoint conj F R` (the frame's magic wand)
4141
on the RHS of an entailment, so the wand never surfaces in a VC. -/
4242
impSplit : LatticeSplit
43+
/-- Optional equation `conj r b = <built-in connective>` (e.g. a `meet`). A fallback to *reduce*
44+
`conj r b` over a nested-base lattice, where the direct `split` cannot peel the extra state
45+
coordinate; reducing it to the built-in connective lets the meet/ofProp splits decompose it over all
46+
coordinates. `none` for frames whose flat `split` always suffices. -/
47+
conjReduce : Option Name := none
48+
/-- Optional equation `upperAdjoint (conj r) b = <closed form>` (e.g. a cost shift `fun m => b (m+r)`).
49+
A fallback to *reduce* the residual wand over a nested-base lattice, where the direct `impSplit`
50+
cannot peel the extra state coordinate; reducing it exposes the body `b` (with its inner `wp`) so the
51+
normal spec step runs. `none` for frames whose flat `impSplit` always suffices. -/
52+
impReduce : Option Name := none
4353

4454
unsafe def getFrameProcFromDeclImpl (declName : Name) : ImportM FrameProc := do
4555
let ctx ← read
@@ -62,13 +72,23 @@ structure FrameProcs where
6272
splits : Std.HashMap Name LatticeSplit := {}
6373
/-- Splits for the residual wands `PreservesSup.upperAdjoint conj F R`, keyed by `conj` head. -/
6474
impSplits : Std.HashMap Name LatticeSplit := {}
75+
/-- Optional conj-reduction equations (`FrameProc.conjReduce`), keyed by `conj` head. -/
76+
conjReduces : Std.HashMap Name Name := {}
77+
/-- Optional wand-reduction equations (`FrameProc.impReduce`), keyed by `conj` head. -/
78+
impReduces : Std.HashMap Name Name := {}
6579

6680
instance : Inhabited FrameProcs := ⟨{}⟩
6781

6882
def FrameProcs.insert (s : FrameProcs) (_declName : Name) (fp : FrameProc) : FrameProcs :=
6983
{ procs := s.procs.insert fp.prog fp
7084
splits := s.splits.insert fp.conj fp.split
71-
impSplits := s.impSplits.insert fp.conj fp.impSplit }
85+
impSplits := s.impSplits.insert fp.conj fp.impSplit
86+
conjReduces := match fp.conjReduce with
87+
| some eq => s.conjReduces.insert fp.conj eq
88+
| none => s.conjReduces
89+
impReduces := match fp.impReduce with
90+
| some eq => s.impReduces.insert fp.conj eq
91+
| none => s.impReduces }
7292

7393
abbrev FrameProcExtension := ScopedEnvExtension Name (Name × FrameProc) FrameProcs
7494

src/Lean/Elab/Tactic/Do/Internal/VCGen/Frontend.lean

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ public def mkContext (lemmas : Syntax) (goal : MVarId) (ignoreStarArg := false)
121121
let frameProcs ← VCGen.getFrameProcs
122122
let ctx : VCGen.Context :=
123123
{ backwardRules, frameInferenceProc := VCGen.matchFrame?.toRef,
124-
customLatticeSplits := frameProcs.splits, customImpSplits := frameProcs.impSplits }
124+
customLatticeSplits := frameProcs.splits, customImpSplits := frameProcs.impSplits,
125+
customConjReduces := frameProcs.conjReduces, customImpReduces := frameProcs.impReduces }
125126
return (ctx, { specs := allSpecThms })
126127

127128
end VCGen

src/Lean/Elab/Tactic/Do/Internal/VCGen/RuleConstruction.lean

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,44 @@ public def LatticeSplit.mkBackwardRuleForLattice
186186
let res ← abstractMVars prf
187187
mkBackwardRuleFromExpr res.expr res.paramNames.toList
188188

189+
/--
190+
Creates a backward rule that *reduces* an operator-applied RHS via a registered equation
191+
`eqName : LHS = RHS`, where `LHS` is the operator applied to its arguments (e.g. `cCostConj c R`, or
192+
`PreservesSup.upperAdjoint (cCostConj c) R`). Unlike `mkBackwardRuleForLattice` it chains no `relLemma`:
193+
the single premise `pre ⊑ RHS s₁…sₙ` *is* the reduced subgoal. Produces
194+
195+
```
196+
∀ … (pre), pre ⊑ RHS s₁…sₙ → pre ⊑ LHS s₁…sₙ
197+
```
198+
199+
with the operator's own arguments and the excess coordinates `s₁…sₙ` (read off from `goalRhs` past the
200+
`LHS` arity) as fresh metavariables — so `applyChecked` unifies the rule's conclusion (same head as the
201+
goal) directly, with no defeq/normalization. Returns `none` if `eqName` is not an equation or `goalRhs`
202+
has fewer arguments than `LHS`. -/
203+
public def mkReduceRule (eqName : Name) (goalRhs : Expr) : MetaM (Option BackwardRule) := do
204+
let eqConst ← mkConstWithFreshMVarLevels eqName
205+
let (argMVars, _, eqBody) ← forallMetaTelescope (← Meta.inferType eqConst)
206+
let some (_, lhs, _) := eqBody.eq? | return none
207+
let numLhsArgs := lhs.getAppNumArgs
208+
if goalRhs.getAppNumArgs < numLhsArgs then return none
209+
-- Pin the operator's own arguments (carrier `Pred`, instance, resource …) to the goal by unifying
210+
-- the equation's `LHS` with the goal RHS's operator prefix, so `LHS`'s carrier type is concrete and
211+
-- the coordinates below apply.
212+
unless ← isDefEq lhs (mkAppN goalRhs.getAppFn (goalRhs.getAppArgs.extract 0 numLhsArgs)) do
213+
return none
214+
-- The excess coordinates are the goal RHS arguments past the operator's own; freshen them so the
215+
-- rule is a pattern (like `mkBackwardRuleForLattice`'s `ss`).
216+
let ss ← (goalRhs.getAppArgs.extract numLhsArgs goalRhs.getAppNumArgs).mapM fun arg => do
217+
mkFreshExprMVar (← Meta.inferType arg)
218+
let eqDistributed ← liftEqByArgs (mkAppN eqConst argMVars) ss.toList -- LHS s₁…sₙ = RHS s₁…sₙ
219+
let some (_, goal, _) := (← Meta.inferType eqDistributed).eq? | return none
220+
let pre ← mkFreshExprMVar (userName := `Pre) (← Meta.inferType goal)
221+
let relEq ← mkCongrArg (← mkAppM ``PartialOrder.rel #[pre]) eqDistributed
222+
-- eqMp : (pre ⊑ RHS s₁…sₙ) → (pre ⊑ LHS s₁…sₙ)
223+
let eqMp ← mkAppM ``Eq.mp #[← mkEqSymm relEq]
224+
let res ← abstractMVars eqMp
225+
return some (← mkBackwardRuleFromExpr res.expr res.paramNames.toList)
226+
189227
/-! ## Spec rules -/
190228

191229
/-- Build the explicit pointwise implication premise used to weaken a concrete `post`.

src/Lean/Elab/Tactic/Do/Internal/VCGen/Solve.lean

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,58 @@ private def foldUpperAdjointMeet? (goal : MVarId) (target rhs : Expr) : VCGenM (
475475
let newTarget ← mkAppNS target.getAppFn (relArgs.set! (relArgs.size - 1) newRhs)
476476
return some (← goal.replaceTargetDefEq newTarget)
477477

478+
/-- `unfoldReducible`-normalize a goal's target, so a later `applyChecked` unifies past reducible state
479+
types (`StateM σ`, `Tick`, …) that sit behind reducible definitions — the normalization that
480+
`BackwardRule.applyChecked`'s `+debug` retry (`Util.lean`) diagnoses. When `betaRhs` is set, the RHS of
481+
the `pre ⊑ rhs` target is also beta-reduced first (the wand reduction leaves a `(fun m => …) s` redex
482+
whose inner `wp` head must be exposed). -/
483+
private def normalizeReducedGoal (goal : MVarId) (betaRhs : Bool := false) : VCGenM MVarId := do
484+
let ty ← goal.getType
485+
unless betaRhs do return ← goal.replaceTargetDefEq (← unfoldReducible ty)
486+
let relArgs := ty.getAppArgs
487+
let some rhs := relArgs[relArgs.size - 1]? | return goal
488+
let newTarget ← mkAppNS ty.getAppFn (relArgs.set! (relArgs.size - 1) (← unfoldReducible rhs.headBeta))
489+
goal.replaceTargetDefEq newTarget
490+
491+
/-- Fallback for a registered frame `conj` that the direct `splitLatticeOp?` could not peel over a
492+
*nested-base* assertion lattice: `vcgen` introduced an extra inner state coordinate, so the goal is
493+
`conj c R n s` — one application deeper than the registered direct split lemma expects — and
494+
`splitLatticeOp?` returns `none`.
495+
496+
Build a backward rule from the registered `conjReduce` equation (`conj c R = <built-in connective>`,
497+
e.g. a `meet`) via `mkReduceRule` and `applyChecked` it. The rule's conclusion has the same head as the
498+
goal, so it unifies directly (no defeq/normalization); its single premise `pre ⊑ <connective> n s` is the
499+
reduced subgoal. `unfoldReducible`-normalize it so the next iteration's `splitLatticeOp?` decomposes the
500+
exposed connective over *all* coordinates and the operand `wp` flows into the normal spec step.
501+
502+
Keyed on `customConjReduces`, so only registered frame `conj` heads are touched, and run *after* the
503+
direct split and the wp phase — a flat lattice keeps its direct split, and only the nested case that
504+
would otherwise stall on `.noProgress` reaches here. Terminates: each firing exposes a non-`conj`
505+
connective head, so it never re-fires on its own output. -/
506+
private def reduceFrameConj? (goal : MVarId) (rhs : Expr) : VCGenM (Option MVarId) := do
507+
let some headName := rhs.getAppFn.constName? | return none
508+
let some eqName := (← read).customConjReduces[headName]? | return none
509+
let some rule ← mkReduceRule eqName rhs | return none
510+
let .goals [g] ← rule.applyChecked goal | return none
511+
return some (← normalizeReducedGoal g)
512+
513+
/-- Wand companion to `reduceFrameConj?`: reduce a residual frame wand `PreservesSup.upperAdjoint
514+
(conj c) R` that the direct `impSplit` couldn't peel over a nested-base lattice (the wand is applied to
515+
an extra inner state coordinate). Build a backward rule from the registered `impReduce` equation
516+
(`upperAdjoint (conj c) R = <closed form>`, e.g. a cost shift `fun m => R (m + c)`) via `mkReduceRule`
517+
and `applyChecked` it; the premise `pre ⊑ <closed form> n s` is the reduced subgoal. Beta-reduce and
518+
`unfoldReducible`-normalize it so the body `R` — and its inner `wp` — is exposed to the normal spec step
519+
instead of stranding in a VC. Run as a fallback, so a flat lattice keeps its direct `impSplit`. -/
520+
private def reduceFrameImp? (goal : MVarId) (rhs : Expr) : VCGenM (Option MVarId) := do
521+
unless rhs.isAppOf ``Lean.Order.PreservesSup.upperAdjoint do return none
522+
-- `@PreservesSup.upperAdjoint α inst (conj c) R …`: the slice `conj c` is at index 2; reduce keyed on
523+
-- its head.
524+
let some sliceHead := rhs.getAppArgs[2]?.bind (·.getAppFn.constName?) | return none
525+
let some eqName := (← read).customImpReduces[sliceHead]? | return none
526+
let some rule ← mkReduceRule eqName rhs | return none
527+
let .goals [g] ← rule.applyChecked goal | return none
528+
return some (← normalizeReducedGoal g (betaRhs := true))
529+
478530
/--
479531
The main VC generation step. Operates on a plain `MVarId` with no knowledge of grind.
480532
Returns `.goals subgoals` when the goal was decomposed, or a classification result
@@ -565,6 +617,13 @@ public def solve (scope : VCGen.Scope) (goal : MVarId) : VCGenM SolveResult := g
565617
| .notFramed goal info => return ← applySpec scope goal info
566618
throwError "Failed to decompose weakest precondition for {info.prog}. This should not happen."
567619

620+
-- Phase 5 (fallback): a registered frame `conj`/wand that the direct split couldn't peel over a
621+
-- nested-base lattice. `reduceFrameConj?` exposes the `conj`'s `meet`-first connective; `reduceFrameImp?`
622+
-- reduces the residual wand via its `impReduce` equation. Either way the next iteration's
623+
-- `splitLatticeOp?`/spec step decomposes the result over all coordinates and specs the inner `wp`.
624+
if let some g ← reduceFrameConj? goal rhs then return .goals scope [g]
625+
if let some g ← reduceFrameImp? goal rhs then return .goals scope [g]
626+
568627
return .stop (.noProgress pre rhs)
569628

570629
end VCGen
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/-
2+
Copyright (c) 2026 Lean FRO LLC. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Vladimir Gladshtein, Sebastian Graf
5+
-/
6+
import Lean
7+
import Std.Internal
8+
import Std.Tactic.Do
9+
10+
set_option mvcgen.warning false
11+
set_option grind.warning false
12+
13+
/-!
14+
# MWE: `vcgen` cost-framing over a *2-level* (stateful-base) assertion lattice
15+
16+
`tests/elab/vcgenTickFrames.lean` shows `vcgen` cost-framing working for `TickM := StateM Nat`,
17+
whose assertion lattice is the **flat** `Nat → Prop` (the single cost coordinate). The *same*
18+
construction over a **stateful base monad** — e.g. `ComplexiT (StateM σ)` in the `loom-complexity`
19+
project, assertion lattice `Nat → (σ → Prop)` (a cost coordinate **and** a base-state coordinate) —
20+
*used to* fail (`vcgen [foo] with finish` reported `finish failed`). It is **now fixed** by the
21+
`reduceFrameConj?` / `reduceFrameImp?` fallbacks in `VCGen/Solve.lean` (see "The fix" below). This
22+
file kept the diagnosis that pinned the root cause down.
23+
24+
Concretely, with `foo : ComplexiT (StateM Unit) Unit := do withTick ()` and
25+
`fooSpec : ⦃fun acc _ => acc = 0⦄ foo ⦃fun _ acc _ => acc ≤ 1⦄`, after the registered frameproc
26+
fires the raw stuck goal is (`s✝¹` = cost, `s✝` = base `Unit` state):
27+
28+
(s✝¹ = 0) ⊑ costConj s✝¹ (wp (Gadget.skipFrame foo) (fun a => (costConj s✝¹) -⋆ Q) ⊥) s✝¹ s✝
29+
└──────────────────── costConj applied to TWO coords ───────────┘
30+
31+
The registered *direct* split `le_costConj_point_apply : pre ⊑ costConj c R n` matches `costConj`
32+
applied to **one** coordinate. Over the 2-level lattice `vcgen` has introduced the extra base-state
33+
arg, so the goal is `costConj c R n s` (one application deeper); the split can't unify, returns
34+
`none`, and the raw `costConj` goal — with the un-specced inner `wp` still inside it — is dumped on
35+
`finish`, which cannot evaluate it.
36+
37+
## What this file pins down (each `example` below verifies a claim)
38+
39+
* **§A — the built-in stateful `meet` split is NOT the culprit.** `vcgen` decomposes
40+
`pre ⊑ (guard ⊓ shift) coords` over a *2-level* lattice for every operand shape we tried (opaque,
41+
λ, cost-shifted, `⌜·⌝`-guarded — i.e. the exact `costConj` shape). In `loom-complexity` it was
42+
further verified to split even when an operand is `wp (Gadget.skipFrame prog) …` (reaching the
43+
inner `wp`, i.e. getting *past* the meet). So once the meet is exposed, the split fires fine,
44+
over both coordinates.
45+
46+
* **§B — `costConj` is NOT defeq to its `meet` form.** The pi-lattice meet is a genuinely different
47+
term (`meet_apply` is a *lemma*, not `rfl`). So a fix cannot reduce the *current* `costConj` with
48+
`Meta.unfoldDefinition?` + `MVarId.replaceTargetDefEq` (the `rfl` it relies on does not hold); the
49+
meet form must be exposed either by *defining* `costConj` meet-first, or by a propositional
50+
`replaceTargetEq`.
51+
52+
## The fix (implemented in `VCGen/Solve.lean`)
53+
54+
Two fallback steps in `solve`, run *after* `splitLatticeOp?` so flat lattices keep their direct split:
55+
56+
* `reduceFrameConj?` — for a registered frameproc `conj` the direct split couldn't peel, delta-unfold
57+
the `conj` (defined **meet-first**, §B) to expose its `meet`, then **`unfoldReducible`-normalize**.
58+
That last step was the real blocker: the meet split's `applyChecked` only unifies against the
59+
reducible-normal form (state types `StateM σ`, `Tick` sit behind reducible defs). `applyChecked`'s
60+
`+debug` retry diagnoses exactly this ("succeeded after `unfoldReducible`-normalization"). The next
61+
`solve` iteration's meet split then peels both coordinates and specs the inner `wp`. Fires every
62+
iteration, so it recurses at each framing level.
63+
64+
* `reduceFrameImp?` — for ≥2 cost calls, the residual wand `upperAdjoint (conj c) R` wraps the *next*
65+
call's un-specced `wp` and the direct `impSplit` can't peel the extra coord. Rewrite it via a
66+
registered `FrameProc.impReduce` equation (here `costConj_imp : upperAdjoint (costConj c) R =
67+
fun m => R (m+c)`), then beta + `unfoldReducible`, exposing the inner `wp` for the spec step.
68+
69+
The leftover VCs (budget guard `c ≤ n`, `WP.Frames`) close under `with finish`.
70+
-/
71+
72+
open Lean Order Std Internal.Do
73+
74+
/-- The 2-level cost-frame operator: a guard on the cost coordinate `n`, met — on the base-state
75+
lattice `σ → Prop` — with the body run on the remaining cost `n - r`. The analog of the flat
76+
`costConj` of `vcgenTickFrames.lean`, but over `Nat → (σ → Prop)`. -/
77+
def costConj {σ : Type} (r : Nat) (b : Nat → (σ → Prop)) : Nat → (σ → Prop) :=
78+
fun n => ⌜r ≤ n⌝ ⊓ b (n - r)
79+
80+
/-! ## §A — the built-in stateful `meet` split decomposes 2-level meets
81+
82+
Each goal is a bare lattice entailment whose RHS is a `meet` of two `Nat → (Unit → Prop)` functions;
83+
`vcgen` introduces the cost arg *and* the base-state arg and fires the meet split across **both**
84+
coordinates, leaving one obligation per operand, which `finish` discharges. -/
85+
86+
/-- Two `⊤` operands over the 2-level lattice `Nat → Unit → Prop`: the split introduces the cost arg
87+
`n` *and* the base-state arg `s`, then closes `pre ⊑ ⊤` on each side. -/
88+
example (pre : Nat → Unit → Prop) :
89+
pre ⊑ (fun _ => (⊤ : Unit → Prop)) ⊓ (fun _ => (⊤ : Unit → Prop)) := by
90+
vcgen with finish [top_apply]
91+
92+
-- The exact `costConj` shape — `⌜·⌝`-guard ⊓ cost-shift — over the 2-level lattice. The split peels
93+
-- **both** coordinates (`s✝¹` the cost, `s✝` the base state), leaving the guard obligation (`vc1`)
94+
-- and the shifted-body obligation (`vc2`), each trivially true. (In `vcgenTickFrames.lean` the
95+
-- analogous split has only the single cost coordinate; here it must — and does — recurse into base.)
96+
/--
97+
error: unsolved goals
98+
case vc1
99+
b : Nat → PUnit → Prop
100+
c s✝¹ : Nat
101+
s✝ : PUnit
102+
⊢ (c ≤ s✝¹ ∧ b (s✝¹ - c) s✝) ⊑ ⌜c ≤ s✝¹⌝ s✝
103+
104+
case vc2
105+
b : Nat → PUnit → Prop
106+
c s✝¹ : Nat
107+
s✝ : PUnit
108+
⊢ (c ≤ s✝¹ ∧ b (s✝¹ - c) s✝) ⊑ b (s✝¹ - c) s✝
109+
-/
110+
#guard_msgs in
111+
example (b : Nat → Unit → Prop) (c : Nat) :
112+
(fun n s => c ≤ n ∧ b (n - c) s) ⊑ (fun n => ⌜c ≤ n⌝) ⊓ (fun n => b (n - c)) := by
113+
vcgen
114+
115+
/-! ## §B — `costConj` is not definitionally its `meet` form -/
116+
117+
/-- The `rfl` that an `unfoldDefinition?` + `replaceTargetDefEq` reduction would rely on **fails**;
118+
the equation holds only *propositionally* (`meet_apply` is a lemma, not `rfl`). -/
119+
example {σ : Type} (c : Nat) (b : Nat → σ → Prop) :
120+
costConj c b = (fun n => ⌜c ≤ n⌝) ⊓ (fun n => b (n - c)) := by
121+
fail_if_success rfl
122+
funext n; simp only [costConj, meet_apply]

0 commit comments

Comments
 (0)