feat(Query): add query complexity framework#376
feat(Query): add query complexity framework#376kim-em wants to merge 2 commits intoleanprover:mainfrom
Conversation
a889930 to
5299afa
Compare
|
I have intentionally kept this at the level of "single tick" counting of costs. It is a fairly straightforward change to make this more flexible, which I have not done here to ease reviewing of the essential idea of using monad parametricity. |
| namespace Cslib.Query | ||
|
|
||
| structure TickT.State where | ||
| count : Nat |
There was a problem hiding this comment.
I would like to make this private, but this breaks a proof below in a way I haven't yet understood.
There was a problem hiding this comment.
I think you reported this issue to me in Dec (I hope it's the same one). I handed over the following reproducer to @Kha:
module
public structure State where
private count : Nat
private example {P : State → Prop} (h : P ⟨State.count s⟩) :
P ⟨State.count s⟩ := by
set_option trace.Meta.Tactic.simp true in
set_option trace.Debug.Meta.Tactic.simp true in
set_option trace.Debug.Meta.Tactic.simp.congr true in
set_option trace.Meta.realizeConst true in
simp [*]
private example {P : State → Prop} (h : P ⟨State.count s⟩) :
P ⟨State.count s⟩ := by
omega
private example {P : State → Prop} (h : P ⟨State.count s⟩) :
P ⟨State.count s⟩ := by
grindwe traced the crash down to mkCongrSimp?, IIRC it was due to getFunInfo. But I think we never fixed it; should have kept better track of this.
There was a problem hiding this comment.
Actually no, it appears this issue has been fixed! At least the reproducer works, and the following works as well (but proofs are broken on latest Mathlib due to DefEq changes...)
module
public import Std.Do.Triple.Basic
import Std.Tactic.Do
open Std.Do
public section
structure TickM.State where
private count : Nat
@[expose] def TickM (α : Type) := StateM TickM.State α
namespace TickM
instance : Monad TickM := inferInstanceAs (Monad (StateM TickM.State))
instance : LawfulMonad TickM := inferInstanceAs (LawfulMonad (StateM TickM.State))
instance : Std.Do.WP TickM (.arg TickM.State .pure) := inferInstanceAs (Std.Do.WP (StateM TickM.State) _)
def tick : TickM Unit := fun s => ⟨(), ⟨s.count + 1⟩⟩
@[spec]
private theorem tick_spec {Q : PostCond Unit (.arg TickM.State .pure)} :
Triple tick (fun s => Q.1 () ⟨s.count+1⟩) Q := by
simp [tick, Triple, wp, Id.run]So maybe try again to make the field private?
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
61d9a60 to
6b6a308
Compare
| @[expose] def TickT (m : Type → Type) (α : Type) := StateT TickT.State m α | ||
|
|
||
| /-- The tick-counting monad, specializing `TickT` to `Id`. -/ | ||
| @[expose] def TickM (α : Type) := TickT Id α |
There was a problem hiding this comment.
This is effectively identical to TimeM Nat
Add a general correctness theorem for mapSum parameterized by an abstract predicate family `pre : Int → Assertion ps`, which captures "the Int state is c" for any postcondition shape. Both mapSum_spec and mapSum_spec_tick are now derived as corollaries, removing the TODO comment about the desired generalization. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sgraf812
left a comment
There was a problem hiding this comment.
Nice! It would be great if we could prove specs about Costs using mvcgen, though. Can take a look once I'm back from PTO
| namespace Cslib.Query | ||
|
|
||
| structure TickT.State where | ||
| count : Nat |
There was a problem hiding this comment.
I think you reported this issue to me in Dec (I hope it's the same one). I handed over the following reproducer to @Kha:
module
public structure State where
private count : Nat
private example {P : State → Prop} (h : P ⟨State.count s⟩) :
P ⟨State.count s⟩ := by
set_option trace.Meta.Tactic.simp true in
set_option trace.Debug.Meta.Tactic.simp true in
set_option trace.Debug.Meta.Tactic.simp.congr true in
set_option trace.Meta.realizeConst true in
simp [*]
private example {P : State → Prop} (h : P ⟨State.count s⟩) :
P ⟨State.count s⟩ := by
omega
private example {P : State → Prop} (h : P ⟨State.count s⟩) :
P ⟨State.count s⟩ := by
grindwe traced the crash down to mkCongrSimp?, IIRC it was due to getFunInfo. But I think we never fixed it; should have kept better track of this.
| count : Nat | ||
|
|
||
| /-- A monad transformer that adds tick-counting to any monad `m`. -/ | ||
| @[expose] def TickT (m : Type → Type) (α : Type) := StateT TickT.State m α |
There was a problem hiding this comment.
From my past (limited) experience with defeq abuse, it may make sense to make TickT @[irreducible] and add an explicit injection/projection pair.
| /-- Run a `TickT` computation, starting with tick count 0, | ||
| returning the result and the final tick count. -/ | ||
| def run [Monad m] (x : TickT m α) : m (α × Nat) := do | ||
| let (a, s) ← StateT.run x ⟨0⟩ |
There was a problem hiding this comment.
(defeq abuse, but probably fine if you formulate your lemmas about .run.)
|
|
||
| /-- Instrument a pure function as a tick-counted query. | ||
| `counted f a` increments the tick counter by 1 and returns `f a`. -/ | ||
| @[expose] def counted [Monad m] (f : α → β) (a : α) : TickT m β := do tick; pure (f a) |
There was a problem hiding this comment.
This function suggests the existence of counted2 [Monad m] (f : α → β → γ) (a : α) (b : β) : TickT m γ etc., similar to liftA<n> in Haskell. It's probably convenient to have counted, but I think it would be convenient to have counted2 and counted3 as well.
Although it would be reasonable for callers to just uncurry their functions before using counted. I think it doesn't scale if we also provide variants of RunsInT below.
| simp only [Triple.iff] | ||
| unfold tick | ||
| show _ ⊢ₛ (PredTrans.pushArg fun s => wp (pure ((), { count := s.count + 1 }) : n _)).apply Q | ||
| simp only [PredTrans.apply_pushArg, WP.pure]; exact .rfl |
There was a problem hiding this comment.
This works, but I think it would be simpler and yield a more reusable proof setup if
- You defined injection (
TickT.mk) and projection (TickT.runn : TickT m α -> StateT TickT.State m α, becauserunis taken) functions withsimplemma(TickT.mk x).runn = x - You defined
wp[x : TickT m α] = wp[x.runn : StateT TickT.State m α] - You proved a
simplemmawp[TickT.mk x : TickT m α] Q = wp[x : StateT TickT.State m α] Q - You proved a
simplemma fortick.runnbeing equal tomodify (\. + 1), presumably by defining it asTickT.mk (modify (\. + 1)) - Then you do
ext Q : 1; simp?in this proof to end up withwp[modify (\. + 1)] Qwhich is easily discharged by a singlemvcgen, I think
| private theorem ExceptConds.false_and_self (ps : PostShape) : | ||
| (ExceptConds.false (ps := ps) ∧ₑ ExceptConds.false).entails ExceptConds.false := by |
There was a problem hiding this comment.
Ah yes, the generalization to any e : ExceptConds ps would be good to upstream.
| /-- `RunsInT n f bound` asserts that when the monad-generic function `f` | ||
| is specialized to `TickT n`, with any query that calls `tick` at most once per invocation, | ||
| the total number of ticks is bounded by `bound x`. | ||
| The function `f` is generic over monads that extend `n` via `MonadLift`, | ||
| ensuring it cannot observe the tick instrumentation. -/ | ||
| @[expose] def RunsInT {n : Type → Type} {ps : PostShape} [Monad n] [WP n ps] | ||
| (f : ∀ {m : Type → Type} [Monad m] [MonadLiftT n m], (α → m β) → γ → m δ) | ||
| (bound : γ → Nat) : Prop := | ||
| ∀ (query : α → TickT n β), (∀ a, TickT.Costs (query a) 1) → | ||
| ∀ x, TickT.Costs (f query x) (bound x) |
There was a problem hiding this comment.
Neat!!
- I think you need to add somewhere (maybe to the top-level comment) that relying on parametricity is only credible when everything is computable. Otherwise you can have
if h : α = Nat then ... else ...byClassical.choice. - I wonder if callers can provide an
fthat uses higher-order constructs such asforloops indonotation. Can't quite play it out in my head right now, but I think it's one of the reasons that theIteratorLoopclass is so complicated.
| induction xs with | ||
| | nil => exact TickT.Costs.pure () | ||
| | cons x xs ih => | ||
| simp only [List.length]; rw [Nat.add_comm] | ||
| have ih : TickT.Costs (mapSum query xs) xs.length := ih | ||
| exact TickT.Costs.bind (hquery x) (fun y => by | ||
| have := TickT.Costs.bind | ||
| (TickT.Costs.monadLift (modify (· + y) : StateM Int Unit) (fun P => by mvcgen)) | ||
| (fun _ => ih) | ||
| rwa [Nat.zero_add] at this) |
There was a problem hiding this comment.
I guess it would be nice to use mvcgen for this kind of proof. Can take a look when I'm back from PTO! You shouldn't need to replicate your own reasoning framework with Costs.pure/Costs.bind etc.
| The predicate family `pre c` captures "the Int state is c" within the | ||
| abstract postcondition shape `ps`. The hypotheses `hf` and `h_modify` | ||
| assert that `f` preserves this predicate and the lifted `modify` transitions it. -/ |
There was a problem hiding this comment.
My gut says it yields simpler VCs if you write specs that are schematic in the post (like tick_spec) rather than the precondition
| (h_modify : ∀ v c, ⦃pre c⦄ | ||
| (MonadLiftT.monadLift (modify (· + v) : StateM Int Unit) : m Unit) | ||
| ⦃⇓ _ => pre (c + v)⦄) | ||
| (xs : List Int) : | ||
| ∀ c, ⦃pre c⦄ mapSum f xs ⦃⇓ _ => pre (c + (xs.map g).sum)⦄ := by |
There was a problem hiding this comment.
This sounds to me like it could be expressed as a loop invariant lemma, similar to Spec.forIn_list etc.
|
@kim-em : This is duplicating #372 (refinement of #275) is it not? Also there I have a specific meaning of queries which are pre-declared inductive types there. I think the reuse of the word @sgraf812 : Related question, could we set up mvcgen for the framework in #372 ? Essentially it runs code in Id monad and measures complexity in |
| List α → m (List α) | ||
| | [] => pure [x] | ||
| | y :: ys => do | ||
| let lt ← cmp (x, y) |
There was a problem hiding this comment.
You say
"Because insertionSort is parametric in m, it cannot observe the tick instrumentation.
It must call cmp the same number of times regardless of which monad it runs in.
Therefore any upper bound proved via TickM is a true bound on query count in all monads."
But you could just as easily use a pure function version of cmp. So you can still sneak in a 0-cost comparison in this model. Same in other examples. The basic limitation of using monadic DSLs applies here too. Anything can be snuck into pure in-principle.
There was a problem hiding this comment.
If your counter-argument here is the absence of BEq, we already use that in the linearSearch example in #372
There was a problem hiding this comment.
I think you are misunderstanding this PR. I'm offline for the weekend, but see if you can subvert it! I claim you can't write any monad polymorphic function from lists to lists that sorts, and has a "too small" RunsIn.
There was a problem hiding this comment.
I see that you simply take a comparison function as a parameter. In this case our models have the same security from misuse of return. However I have noted the strong limitations of this approach as opposed to mine below. In my case, I only interpret this function concretely in my model.
I strongly disagree. You don't state your queries up front in your model. So you can't state reductions, lower bounds etc. all this makes use of explicit queries and custom cost functions. Instead you use a lean function as a parameter. This is a huge limitation in algorithms theory and complexity. In the model of #372, one can formally state multiple RAM models and even DSLs for Turing machines and circuits from circuit complexity. Therefore we can already define uniform circuit classes in complexity which involves writing programs in two different models with two different cost models (TMs and circuits). The fact that I can write circuits tells me that I can also encode parallel algorithms and the equivalence between circuits and parallel algorithms (there are several standard equivalences between these models). I have kept my pr limited to simple examples to keep it focused (the maintainers explicitly asked this). In fact it can even talk about lower bounds. In summary : the other PR allows for a comprehensive top down treatment of the multitude of models in algorithms and the relationships between them(one of the standard models like RAM and TM could be sink nodes in this relationship network). What this PR is doing is equivalent to compressing the process of defining a query and an evaluation function (and a cost function) in my PR, by directly providing the interpreted version as a parameter (cmp). But it is losing a lot in the process. So, you get a slight generalisation from evaluating in
|
This PR adds infrastructure for proving upper bounds on the number of queries
(comparisons, oracle calls, etc.) an algorithm makes, using monad parametricity to ensure validity of the bounds.
TickT mmonad transformer with tick counting,Costspredicate, and combinators(
Costs.pure,Costs.bind,Costs.monadLift,Costs.ite, etc.)RunsIn/RunsInTpredicates packaging the parametricity argumentmapSumwith functional correctness under tick instrumentationQuoting from the module doc for
RunsIn:RunsIn f bound(and its generalizationRunsInT) assert that a monad-generic algorithmfmakes at most
bound xqueries on inputx.An algorithm like
is written generically over the monad
m. To measure its query complexity, we specializemtoTickM(orTickT nfor algorithms with additional effects) and provide acmpimplementation that callstickonce per invocation.Because
insertionSortis parametric inm, it cannot observe the tick instrumentation.It must call
cmpthe same number of times regardless of which monad it runs in.Therefore any upper bound proved via
TickMis a true bound on query count in all monads.🤖 Prepared with Claude Code