Skip to content

feat(Query): query complexity framework with sorting examples#401

Open
kim-em wants to merge 59 commits into
leanprover:mainfrom
kim-em:combined-query-complexity
Open

feat(Query): query complexity framework with sorting examples#401
kim-em wants to merge 59 commits into
leanprover:mainfrom
kim-em:combined-query-complexity

Conversation

@kim-em
Copy link
Copy Markdown
Collaborator

@kim-em kim-em commented Mar 5, 2026

This PR implements Sebastian Graf's unified approach to query complexity (discussed in the CSLib Algorithm frameworks thread), combining the strengths of #372 (explicit Prog/FreeM query types) and #376 (monad-parametric approach).

Programs are Prog Q α (free monad over query type Q), and the oracle is supplied after the program produces its query plan — giving anti-cheating guarantees for both upper and lower bounds. No WP/Hoare triple machinery is needed: correctness is just equations about Prog.eval oracle, and cost is just equations about Prog.queriesOn oracle.

This provides an alternative to the TimeM-based cost analysis already in the repo: here query counting is structural (derived from the Prog tree) rather than annotation-based.

New files

File Contents
Query/Prog.lean Core Prog type, eval, queriesOn, simp lemmas
Query/Bounds.lean UpperBound and LowerBound definitions
Query/QueryTree.lean Decision trees with fixed response type, for lower bound proofs
Query/Sort/LEQuery.lean Comparison query type for sorting
Query/Sort/IsSort.lean IsSort correctness specification
Query/Sort/Insertion/{Defs,Lemmas}.lean Insertion sort: correctness + O(n²) upper bound
Query/Sort/Merge/{Defs,Lemmas}.lean Merge sort: correctness + n·⌈log₂ n⌉ upper bound
Query/Sort/QueryTree.lean Prog-to-QueryTree bridge + pigeonhole depth lemma
Query/Sort/LowerBound.lean Any correct comparison sort needs ≥ ⌈log₂(n!)⌉ queries

Results

  • Insertion sort: correctness (permutation + sortedness), n² upper bound, IsSort instance
  • Merge sort: correctness, n·⌈log₂ n⌉ upper bound, IsSort instance
  • Lower bound: any IsSort on an infinite type makes ≥ ⌈log₂(n!)⌉ queries. The proof constructs n! distinct total orders via permutations of embedded elements, shows they force distinct sorted outputs, then applies an adversarial pigeonhole argument on QueryTree depth.

🤖 Prepared with Claude Code

@kim-em kim-em requested a review from chenson2018 as a code owner April 22, 2026 03:40
@kim-em kim-em force-pushed the combined-query-complexity branch from 12a4d9f to b331f1b Compare April 22, 2026 03:40
@Shreyas4991
Copy link
Copy Markdown
Contributor

Shreyas4991 commented Apr 22, 2026

  1. I wish to point out that the authorship and copyright headers must also be changed. This is my code. The FRO can't claim copyright.
  2. Same for author comments. The order of authorship matters.
  3. This PR still doesn't address the technical deficiencies w.r.t. mine, because it entirely overwrites my content.
  4. Query Trees are still redundant. They are trivially derivable from Progs.
  5. The Arith query should use my query and build on top of it.
  6. Per Fabrizio's message to me there was no mention of "superseding" any of my work. He said the pr would build on top of mine.
  7. The overall technical merits of adding extra monadic polymorphism becomes even less apparent. Technically it doesn't even make sense to add another monad parameter since FreeM is already parametrising a functor (more general than a monad). That is it is already monad parametric.
  8. All the features claimed to be derived from model hiding can already be achieved in the model of 372. In fact this is as simple as quantifying over arbitrary models in theorem statements. The extra monad parameter is redundant. It can actually be substituted by any type constructor (which is essentially what the F in FreeM F a really is).
  9. The examples I added should be preserved.

@kim-em kim-em force-pushed the combined-query-complexity branch from b331f1b to 0f75466 Compare April 22, 2026 03:49
kim-em and others added 3 commits April 22, 2026 03:55
Add a framework for proving upper and lower bounds on query complexity
of comparison-based algorithms, using `Prog` (free monad over query
types) with oracle-parametric evaluation and structural query counting.

Results:
- Insertion sort: correctness + O(n²) upper bound
- Merge sort: correctness + n·⌈log₂ n⌉ upper bound
- Lower bound: any correct comparison sort on an infinite type
  needs ≥ ⌈log₂(n!)⌉ queries (via adversarial pigeonhole on
  QueryTree depth)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Shrys <shreyasss94@gmail.com>
Add `Prog.cost`, a weighted generalization of `Prog.queriesOn` where
each query type can have a different cost. Demonstrate this with complex
multiplication: naive (4 muls + 2 adds) vs Gauss's trick (3 muls + 5 adds),
proving correctness, exact parametric costs, and the crossover condition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Shrys <shreyasss94@gmail.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Shrys <shreyasss94@gmail.com>
@kim-em kim-em force-pushed the combined-query-complexity branch from 0f75466 to 7327006 Compare April 22, 2026 03:55
Copy link
Copy Markdown

@sgraf812 sgraf812 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think 3 of my 4 items in #401 (review) haven't been addressed yet. I think the doX stuff has been addressed.

Comment thread Cslib/Algorithms/Lean/Query/Prog.lean Outdated
/-- Evaluate a program by answering each query using `oracle`. -/
@[expose] def eval (oracle : {ι : Type} → Q ι → ι) : Prog Q α → α
| .pure a => a
| .liftBind op cont => eval oracle (cont (oracle op))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still relevant


This is the free monad specialized to a single fixed-type operation, used to reify
algorithms as explicit trees for query complexity lower bounds. -/
inductive QueryTree (Q : Type) (R : Type) (α : Type) where
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this file. Use ProgM throughout.

@Shreyas4991
Copy link
Copy Markdown
Contributor

Shreyas4991 commented Apr 27, 2026

Programs are Prog Q α (free monad over query type Q), and the oracle is supplied after the program produces its query plan — giving anti-cheating guarantees for both upper and lower bounds. No WP/Hoare triple machinery is needed: correctness is just equations about Prog.eval oracle, and cost is just equations about Prog.queriesOn oracle.

One small nitpick, in the counting of time complexity, the complexity of an oracle call can depend on size/parameters of inputs supplied to it. For example the cost of calling a vertex connectivity query to a subgraph depends on the edge and vertex size of a subgraph. The current definition simply assumes each oracle call costs 1.

kim-em and others added 4 commits April 28, 2026 02:46
Prog Q α was already a definitional `abbrev` for FreeM Q α; this commit
deletes the Prog namespace and moves the eval/queriesOn/cost interpreters
to a new Cslib/Algorithms/Lean/Query/FreeM.lean. All call sites in the
query subtree (sorting, arith examples, bounds) now refer to FreeM
directly. One-step query constructors (LEQuery.ask, ArithQuery.doAdd/
doSub/doMul) now use FreeM.lift rather than raw .liftBind … .pure.

QueryTree and the Prog→QueryTree bridge (now FreeM.toQueryTree) remain in
place; deleting QueryTree requires generalising the lower-bound lemma
and lands separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The QueryTree decision-tree datatype was a single-response-type
specialisation of FreeM, kept around because the existing
combinatorial lower-bound lemma was easier to state with a fixed
response type. This commit ports the lemma directly to FreeM:

  FreeM.exists_queriesOn_ge_clog : if every response type has
    cardinality ≤ r, n distinct injective oracles force some oracle
    to make ≥ ⌈log_r n⌉ queries.

The proof mostly mirrors the QueryTree version, using @liftBind to
bind the existential response type, and one extra ceiling-division
step (Nat.div_le_div_left) to relate the per-node branching factor
to the global bound r.

Sort/LowerBound.lean now applies the FreeM lemma directly, with
LEQuery.fintypeResponse / cardResponse_le_two witnessing that
LEQuery responses are always Bool. The Prog→QueryTree bridge
(toQTOracle / fromQTOracle / toQueryTree / *_eval / *_queriesOn) is
gone; only LEQuery.oracleOf survives, renamed and moved into
Sort/LEQuery.lean.

Both QueryTree.lean and Sort/QueryTree.lean are deleted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ment

The threshold theorem `gauss_le_naive` uses `3 * c_add ≤ c_mul` (inclusive),
so the section header should say "at least 3×", not "more than 3×".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…liftM

Addresses Eric Wieser's review (Mar 5 2026, on the original Prog.lean):
"pattern matching on the free monad is exploiting an implementation detail,
and that everything should really go through the universal property,
FreeM.liftM."

All three interpreters are now defined as `liftM` into a target monad:

  eval      : liftM (m := Id) oracle
  cost      : liftM (m := Tally) (fun op => ⟨weight op, oracle op⟩) |>.cost
  queriesOn : cost oracle (fun _ => 1)

where `Tally` is a tiny accumulator monad (a value paired with a `Nat`-valued
running cost) introduced in this file with `Monad` and `LawfulMonad` instances.

The right primitive turned out to be `def`, not `abbrev`. With `def`, the
constructor-form simp lemmas (eval_pure, eval_liftBind, cost_pure,
cost_liftBind, queriesOn_pure, queriesOn_liftBind) all reduce by rfl, so
downstream proof ergonomics are unchanged from the original pattern-match
definitions. simp normal form is determined by the explicit @[simp] theorems
rather than opportunistic abbrev unfolding (which would otherwise mix
`queriesOn` and `cost _ (fun _ => 1)` forms in goals and confuse omega).

Net effect: the universal property is the actual definition, not a post-hoc
characterisation. queriesOn_eq_cost_one is rfl. No downstream proof needed
updating.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kim-em
Copy link
Copy Markdown
Collaborator Author

kim-em commented Apr 28, 2026

  • Define queriesOn as an abbreviation of cost, kill lemmas for the former

@sgraf812 could you take a look again and see if you'd still like me to remove things here?

Comment thread Cslib/Algorithms/Lean/Query/FreeM.lean Outdated
Comment thread Cslib/Algorithms/Lean/Query/FreeM.lean Outdated
/-- Evaluate a program by answering each query using `oracle`.
Defined as `liftM` to `Id`, the canonical interpreter into pure values. -/
@[expose] def eval (oracle : {ι : Type} → F ι → ι) (p : FreeM F α) : α :=
p.liftM (m := Id) oracle
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing pure and Id.run

…linter

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
of its input, and produces a sorted list when the oracle implements a total order. -/
structure IsSort (sort : List α → FreeM (LEQuery α) (List α)) : Prop where
/-- The sort produces a permutation of its input, for any oracle. -/
perm : ∀ (xs : List α) (oracle : {ι : Type} → LEQuery α ι → ι),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requiring this for even bad oracles is an interesting but reasonable choice.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although is it really reasonable? I think it could unnecessarily rule out some high performance implementations. I don't have a concrete example, though...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name isSort is misleading because you also include perm inside. More descriptive/explicit name is better e.g., isSortPerm.

| le (a b : α) : LEQuery α Bool

/-- Lift `LEQuery.le a b` into a `FreeM` that returns the comparison result. -/
@[expose] def LEQuery.ask (a b : α) : FreeM (LEQuery α) Bool :=
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest making all these wrappers for FreeM.lift abbrevs

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(as I do in #525)

Copy link
Copy Markdown

@sgraf812 sgraf812 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better 👍

of its input, and produces a sorted list when the oracle implements a total order. -/
structure IsSort (sort : List α → FreeM (LEQuery α) (List α)) : Prop where
/-- The sort produces a permutation of its input, for any oracle. -/
perm : ∀ (xs : List α) (oracle : {ι : Type} → LEQuery α ι → ι),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although is it really reasonable? I think it could unnecessarily rule out some high performance implementations. I don't have a concrete example, though...

(p.liftM (m := Tally) (fun op => ⟨weight op, oracle op⟩)).cost

/-- Count the number of queries along the path determined by `oracle`. -/
@[expose] def queriesOn (oracle : {ι : Type} → F ι → ι) (p : FreeM F α) : Nat :=
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: I tend to read queriesOn and think "this gives back a trace of the queries done by p" even if it doesn't make sense typically. Maybe cost1? Or countQueries (which nicely matches existing use of count in Std)?

(orderedInsert x (y :: ys)).eval oracle =
if oracle (.le x y) then x :: y :: ys
else y :: (orderedInsert x ys).eval oracle := by
simp [orderedInsert, LEQuery.ask]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LEQuery.ask here hints at a missing simp lemma? Shouldn't eval_ask have fired? Maybe it is fixed by making LEQuery.ask an abbrev, as Eric suggests...

if oracle (.le x y)
then x :: (merge xs' (y :: ys')).eval oracle
else y :: (merge (x :: xs') ys').eval oracle := by
simp [merge, LEQuery.ask]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly here. Is there a reusable simp lemma missing?

1 + if oracle (.le x y)
then (merge xs' (y :: ys')).queriesOn oracle
else (merge (x :: xs') ys').queriesOn oracle := by
simp [merge, LEQuery.ask]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here


/-- Count the number of queries along the path determined by `oracle`. -/
@[expose] def queriesOn (oracle : {ι : Type} → F ι → ι) (p : FreeM F α) : Nat :=
cost oracle (fun _ => 1) p
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Queries can have more generic, input-size dependent costs.

Comment on lines +69 to +73
/-- Weighted query cost: each query has a cost given by `weight`, accumulated along the
oracle-determined path. Defined as `liftM` into the accumulator monad `TimeM`. -/
@[expose] def cost (oracle : {ι : Type} → F ι → ι)
(weight : {ι : Type} → F ι → Nat) (p : FreeM F α) : Nat :=
TimeM.time <| p.liftM fun op => ⟨oracle op, weight op⟩
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's a good reason to restrict to Nat here.

Comment on lines +31 to +32
@[simp] theorem eval_orderedInsert_nil (oracle : {ι : Type} → LEQuery α ι → ι) (x : α) :
(orderedInsert x ([] : List α)).eval oracle = [x] := by
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just prove that (orderedInsert x l).eval oracle = l.orderedInsert x (fun x y => oracle (.le x y)), and then we can delete most of this file.

{ix : Type} (p : FreeM F α) (S : Finset ix) (hS : S.Nonempty)
(oracles : ix → ({ρ : Type} → F ρ → ρ))
(h_inj : Set.InjOn (fun i => p.eval (oracles i)) ↑S) :
∃ i ∈ S, p.queriesOn (oracles i) ≥ Nat.clog r S.card := by
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautiful.


Because the oracle is supplied *after* the program produces its query plan (the `FreeM` tree),
a sound implementation has no way to "guess" what the oracle would respond. This is the
foundation of the anti-cheating guarantee for both upper and lower bounds.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FreeM is more robust than TimeM, but I understand that FreeM is not immune to cost cheating, right? If so, we should explicitly write a warning/caveat to the users so they aware some weaknesses. In particular, we should write how much trust do we require in this model so that the complexity is counted correctly.

Copy link
Copy Markdown
Contributor

@Shreyas4991 Shreyas4991 May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fairly trivial. The scope for cheating is essentially "sneak in operations via pure". This is different from TimeM, where additionally the location and individual cost annotations can vary from place to place which can also cause problems. One must also ideally avoiding adding extraneous typeclass instances and choose Boolean propositions.

This is however one form of "cheating" that I am not too worried about anymore.

  1. On the one hand, we do want pure operations when we don't care too much about their implementation details. This came up in my MPI talk.

  2. I have two ideas on how to eliminate pure operations based issues with model-level lower bound proofs entirely, in addition to model/input hiding. Can elaborate on Zulip if asked.

/-! # Merge Sort as a Query Program

Merge sort implemented as a `FreeM (LEQuery α)`, making all comparison queries explicit.
Uses an alternating split (odds/evens) to avoid needing `List.length` in the termination
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on why using List.length is bad in the termination argument?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The odd even split uses structural recursion instead of well founded recursion.

/-- Merge two sorted lists using comparison queries. -/
@[expose] def merge (xs ys : List α) : FreeM (LEQuery α) (List α) :=
match xs, ys with
| [], ys => pure ys
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| [], ys => pure ys
| [], ys => pure ys

return ys has better stylistic reading. This applies to other similar lines.

Comment on lines +22 to +30
@[expose] def orderedInsert (x : α) : List α → FreeM (LEQuery α) (List α)
| [] => pure [x]
| y :: ys => do
let le ← LEQuery.ask x y
if le then
pure (x :: y :: ys)
else do
let rest ← orderedInsert x ys
pure (y :: rest)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@[expose] def orderedInsert (x : α) : List α → FreeM (LEQuery α) (List α)
| [] => pure [x]
| y :: ys => do
let le ← LEQuery.ask x y
if le then
pure (x :: y :: ys)
else do
let rest ← orderedInsert x ys
pure (y :: rest)
@[expose] def orderedInsert (x : α) : List α → FreeM (LEQuery α) (List α)
| [] => pure [x]
| y :: ys => do
let le ← LEQuery.ask x y
if le then
pure (x :: y :: ys)
else do
let rest ← orderedInsert x ys
pure (y :: rest)

Stylistic change suggestion:

@[expose] def orderedInsert (x : α) : List α → FreeM (LEQuery α) (List α)
| [] => return [x]
| y :: ys => do
let le ← LEQuery.ask x y
if le then
return (x :: y :: ys)
else do
let rest ← orderedInsert x ys
return (y :: rest)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants