Skip to content

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

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

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

Conversation

@kim-em

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

Copy link
Copy Markdown
Collaborator

This PR adds a FreeM-based framework for proving upper and lower bounds on query complexity. It incorporates and preserves the history of the earlier work in #372, together with subsequent design and review contributions discussed in the CSLib Algorithm frameworks thread.

A program is represented directly as FreeM F α, where F : Type u → Type v maps each query to its response type. The program is constructed independently of an oracle; evaluation supplies oracle responses only through lifted queries, while allowing later queries to depend on earlier responses.

The framework provides three universe-polymorphic interpreters, defined through FreeM.liftM:

  • FreeM.eval evaluates a query program against an oracle.
  • FreeM.countQueries counts the queries made along the oracle-determined execution path.
  • FreeM.cost assigns query-dependent weights in an arbitrary additive monoid.

Pure computation is uncharged: costs attach only to lifted queries, so the interpreters measure query complexity. Counting is structural, derived from the program tree, rather than relying on trusted annotations as in a TimeM-style analysis. The FreeM module docstring includes a short recipe for setting up a new query type.

A caveat is that only operations represented by FreeM.lift are counted. If the intended oracle can be reconstructed from structure otherwise available to the program—for example, if its answers are determined by laws and computable using ordinary Lean definitions—an implementation may reimplement the oracle internally and avoid counted calls. The model is therefore strongest when oracle operations remain abstract. Parametric problems are less vulnerable: for example, an algorithm uniform over an arbitrary ring, with ring operations exposed only through ArithQuery, cannot generally reproduce those operations without issuing the corresponding queries. Parametricity alone is not sufficient if the same operations are also available directly through typeclass instances or other definitions.

Bounds and general lower-bound theorem

UpperBound and LowerBound express query bounds quantified over oracles. UpperBound.of_pointwise derives an upper bound from a per-input count bound and monotonicity of the bound function, and LowerBound.le_upperBound shows that a lower bound for a program never exceeds an upper bound for it.

The central combinatorial result is FreeM.exists_countQueries_ge_clog: if

  • every query response type is finite and has cardinality at most r, and
  • n oracles produce distinct results from a fixed program,

then some oracle forces the program to make at least ⌈log_r n⌉ queries.

The proof works directly on FreeM, without a separate fixed-response QueryTree datatype.

Sorting

The PR defines comparison queries LEQuery, a correctness specification IsSort, and query implementations of insertion sort and merge sort mirroring List.insertionSort and List.mergeSort exactly: eval_insertionSort and eval_mergeSort identify each program, evaluated against any oracle, with the corresponding standard-library function, so correctness (permutation, sortedness, and, for merge sort, stability) transfers from the existing API rather than being reproved. IsSort.eval_eq shows the specification pins down the behaviour: under any oracle implementing an antisymmetric total transitive relation, all correct comparison sorts produce the same output.

Proved bounds:

  • insertion sort makes at most n * (n - 1) / 2 queries, a bound attained by the all-false oracle, with as a corollary;
  • merge sort makes at most n * ⌈log₂ n⌉ queries;
  • every correct comparison sort on an infinite type has worst-case query complexity at least ⌈log₂(n!)⌉, by constructing n! hidden total orders with distinct sorted outputs and applying the general FreeM lower-bound theorem;
  • comparing merge sort's two bounds via LowerBound.le_upperBound yields the arithmetic fact ⌈log₂(n!)⌉ ≤ n * ⌈log₂ n⌉ with no further work.

Weighted-cost example

The arithmetic example demonstrates query-dependent costs using naive complex multiplication and Gauss's trick:

  • naive multiplication has exact cost 4 * c_mul + 2 * c_add;
  • Gauss's trick has exact cost 3 * c_mul + 5 * c_add;
  • Gauss's trick is no more expensive exactly when 3 * c_add ≤ c_mul.

Upstream mirrors

Three general-purpose results are proposed upstream and kept private here until they land: List.mergeSort_append (leanprover/lean4#14995), Function.Injective.extend_of_disjoint together with its extend_sum_inl_inr corollary (leanprover-community/mathlib4#43325), and the Std.Total (InvImage r f) instance (leanprover-community/mathlib4#43326).

Files

File Contents
Query/FreeM.lean Evaluation, query counting, weighted costs, the general lower-bound theorem, and the query-type recipe
Query/Bounds.lean UpperBound, LowerBound, of_pointwise, le_upperBound
Query/Arith/{Defs,Lemmas}.lean Parametric arithmetic costs and complex-multiplication example
Query/Sort/LEQuery.lean Boolean comparison queries and their response-cardinality facts
Query/Sort/IsSort.lean Correctness specification for comparison sorts and output uniqueness
Query/Sort/Insertion/{Defs,Lemmas}.lean Insertion sort, agreement with List.insertionSort, triangular bound
Query/Sort/Merge/{Defs,Lemmas}.lean Merge sort, agreement with List.mergeSort, n * ⌈log₂ n⌉ bound
Query/Sort/Merge/Bounds.lean Combined merge sort bounds and the ⌈log₂(n!)⌉ ≤ n * ⌈log₂ n⌉ corollary
Query/Sort/LowerBound.lean ⌈log₂(n!)⌉ comparison-sorting lower bound
CslibTests/Query.lean Executable checks and API examples

kim-em and others added 8 commits September 2, 2026 01:45
Generalize Bounds to query families Q : Type u → Type v, add a combinator
deriving UpperBound from a pointwise count bound and monotonicity, and a
sandwich lemma showing a LowerBound never exceeds an UpperBound for the
same program.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
The triangular bound is attained by the all-false oracle; the previous
n ^ 2 bound remains as a corollary and the UpperBound instance now goes
through UpperBound.of_pointwise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
State countQueries_mergeSort_cons_cons with List.mergeSort arguments (the
form eval_mergeSort rewrites to), isolate the List.mergeSort.eq_3 use in a
private helper linking leanprover/lean4#14995, and
derive mergeSort_upperBound through UpperBound.of_pointwise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
Under an oracle implementing an antisymmetric total transitive relation,
all correct comparison sorts produce the same output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
Function.Injective.extend_sum_inl_inr is proposed in
leanprover-community/mathlib4#43325 (with a golfed
LeftInverse proof, mirrored here) and the Std.Total (InvImage r f) instance
in leanprover-community/mathlib4#43326; keeping the
local copies private avoids conflicts when those land.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
Instantiate the comparison-sorting lower bound at mergeSort and compose it
with the upper bound, yielding clog 2 n! <= n * clog 2 n for free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
@eric-wieser
eric-wieser dismissed their stale review September 2, 2026 02:05

I'd like to review the updates

kim-em and others added 2 commits September 2, 2026 02:27
Replace the private cons-cons unfolding with a mirror of the
mergeSort_append lemma proposed in
leanprover/lean4#14995 (merging the sorted halves
of any balanced split gives mergeSort), deriving the split form from it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
@Shreyas4991

Shreyas4991 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

I'm reasonably happy with this now, though let's wait for discussion to stop in the CSLib reviewer channel before finally merging.

@eric-wieser I was told there would be a discussion comparing #685 and #401 involving me. Why is it that this PR is being merged directly. It is still suboptimal in design. I have waited two three months for said discussion.

This was discussed in the cslib meetings.
Cc: @arademaker and @fmontesi

For the record I maintain that #685 should be merged. This PR (401) has done a good job of performing what amounts to a shallow copy of my work. However #372 (and #685) makes better design choices and has been battle tested. It has a better downstream track record.

It is also extremely dishonest to claim that subsequent reviews have improved it over #372. At best it is an inadequate approximation, with some minor changes that can be PRed back to #685 (successor of #372 which contains this PR's history, and was made at the maintainers' request).

pull Bot pushed a commit to DaviRain-Su/lean4 that referenced this pull request Sep 2, 2026
…4995)

This PR adds two lemmas exposing the recursion of `List.mergeSort`
without reference to `MergeSort.Internal.splitInTwo`:

- `mergeSort_append`: merging the sorted halves of any balanced split
(`l₂.length ≤ l₁.length ≤ l₂.length + 1`) gives `(l₁ ++ l₂).mergeSort`.
This is the primary statement: it has no index arithmetic, holds
uniformly for every list length, and any specific unfolding (take/drop
at the midpoint, cons-cons forms) is a two-line corollary.
- `@[simp] mergeSort_pair`: `[a, b].mergeSort le = if le a b then [a, b]
else [b, a]`, completing the `mergeSort_nil`/`mergeSort_singleton`
progression. Unlike `mergeSort_append` it genuinely simplifies, so it is
marked `@[simp]`.

Downstream libraries currently have to use the auto-generated
`List.mergeSort.eq_3` (whose numbering is unstable, and which is not
accessible from files using the module system without `import all`) or
unfold `splitInTwo`'s subtype plumbing by hand; this came up in
leanprover/cslib#401, where a query-complexity model of merge sort is
proved to agree with `List.mergeSort`.

🤖 Prepared with Claude Code

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Replace the separate finiteness and Nat.card hypotheses of
FreeM.exists_countQueries_ge_clog with one Cardinal inequality (a natural
bound on a cardinal implies finiteness), per Eric's review suggestion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
@Shreyas4991

Shreyas4991 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Additionally I wish to note that in any sensible open source project that respects its contributors, a PR with this much overlap with a prior PR would be closed as a duplicate PR. The contributor would be asked to build on top of existing work. Senior members in particular wouldn’t scoop the work of junior members (with/without AI)

Even Google's AI knows this. I am sure a Claude user can figure this out :

https://share.google/aimode/h7o10KNuBhufnoDM0

/-- Sort a list using insertion sort with comparison queries. -/
@[expose] def insertionSort : List α → FreeM (LEQuery α) (List α)
/-- Sort a list using insertion sort with monadic comparisons. -/
@[expose] def insertionSortM : List α → m (List α)

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.

What monad are you planning to use other than free monads?

@Shreyas4991 Shreyas4991 Sep 3, 2026

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.

This change you are applying you could be used to rewrite all monadic functions in all of lean and mathlib, including tactic monads and tactics in a monad polymorphic way. That doesn't mean one should. This is the design used by so-called mtl style transformers. It doesn't add anything meaningful here.

@Shreyas4991 Shreyas4991 Sep 3, 2026

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.

Another point : This doesn't scale for large and composite query models. It results in redundancy in parameters Explained here on zulip

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.

What monad are you planning to use other than free monads?

Id and PFunctor.FreeM are two natural choices; but you could equally do something silly like IO for a game where you ask the human to do the comparison, or perhaps some kind of LogM monad that records the comparisons as they happen.

Of course you can get here by starting with something in FreeM and using liftM, but my guess is that Lean's compiler cannot optimize this to anywhere near the same extent.

The actual motivation for this is to:

  • present a pattern that allow algorithms to be written without CSLib, but then have their complexity proved downstream in CSLib
  • allow monad-generic implementations to be proved lawful, in the sense that they are preserved under IsMonadHom (feat: add a predicate for monad morphisms #856).

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 change you are applying you could be used to rewrite all monadic functions in all of lean and mathlib,

See List.find/List.findM, List.any/List.anyM, etc; there is lots of precedent for already doing this.

including tactic monads and tactics in a monad polymorphic way

To some extend the functions written with [MonadEnv m] instead of CoreM are also opting into this pattern.

@eric-wieser eric-wieser Sep 3, 2026

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.

Transitioning to PFunctor.FreeM is much simpler since PFunctor.FreeM generalizes FreeM.

I'd encourage you to start a Zulip thread comparing these. As I understand it, there are queries in FreeM that have no representation in PFunctor.FreeM and vice versa. I think this is not well-explained by the current docstring of PFunctor.FreeM, and it would be great to construct contrived or even plausible examples of each.

@Shreyas4991 Shreyas4991 Sep 3, 2026

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.

It's not true though.

  1. FreeM and this MTL style approach are actually complementary. They are both separately used to compose multiple effects to achieve effectful programming.
  2. Secondly to construct the interface, you would compose an existing queue type, an existing stack type, and an existing fibonacci heap type using direct sums, and use the composition lemmas for these. Writing a bespoke structure means we can't directly use those lemmas.

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.

Transitioning to PFunctor.FreeM is much simpler since PFunctor.FreeM generalizes FreeM.

I'd encourage you to start a Zulip thread comparing these. As I understand it, there are queries in FreeM that have no representation in PFunctor.FreeM and vice versa. I think this is not well-explained by the current docstring of PFunctor.FreeM, and it would be great to construct contrived or even plausible examples of each.

Michael Sammler already explained how PFunctor.FreeM can express everything FreeM can but not vice versa. Quang Dao corrected it:

https://leanprover.zulipchat.com/#narrow/channel/513188-CSLib/topic/Free.20monad.20over.20a.20polynomial.20functor/near/584202846

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@Shreyas4991 Shreyas4991 Sep 5, 2026

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.

I wish to note that Eric and I discussed on Zulip that this change is orthogonal to the query combinator model and has been refactored to #861. Further we discussed that this change is of no use to algorithmic theory.

Given the amount of misunderstanding expressed by several people about this framework, this content only serves to obfuscate the above points to maintainers.

kim-em and others added 3 commits September 5, 2026 01:38
The generic List.orderedInsertM/insertionSortM commute with any monad
morphism, stated with the IsMonadHom laws of
leanprover#856 inlined and needing no
lawfulness on either side. Since evaluation against an oracle is a monad
morphism to Id, the executable Id instantiation is List.insertionSort
with no separate proof about the generic definition, and the framework's
complexity bounds apply to the generic program definitionally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
Match the current form of the Mathlib PR: `Function.Injective.extend_of_disjoint`,
with `extend_sum_inl_inr` as a corollary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjjWKN4JxeBhmHnUVY3mC5
felipponn added a commit to felipponn/fad that referenced this pull request Sep 9, 2026
…red copy

Replace the self-contained Fad/QueryModel.lean with the real query-complexity
framework from CSLib PR leanprover/cslib#401 (kim-em:combined-query-complexity):
FreeM-based programs with eval / cost / queriesOn interpreters and the
UpperBound/LowerBound predicates.

- lakefile.toml: point cslib at the PR branch (pinned rev 391cab00)
- lean-toolchain: v4.33.0-rc1 -> v4.30.0-rc2 (required by that cslib/mathlib pin)
- lake-manifest.json: regenerated for the new pins
- Fad/QueryModel.lean: removed (no longer vendored)
- Fad/Chapter2-Query.lean: rewritten against the FreeM API (append, concat1,
  concat2); same programs measured under a fixed oracle with varying weights
- Fad/Chapter2-Amortized.lean: new — binary counter showing amortized O(1) via
  the potential method (Phi = number of 1-bits), a natural fit for queriesOn
- Fad/Chapter3.lean: List.toAssocList' (top-level in v4.30, was Lean.List.* in
  v4.33) — collateral fix for the toolchain downgrade

Full `lake build` green (8347 jobs); remaining warnings are pre-existing sorries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbkMyCwYRAiUy3NFwn9M4f
mathlib-bors Bot pushed a commit to leanprover-community/mathlib4 that referenced this pull request Sep 10, 2026
This PR adds `Function.Injective.extend_of_disjoint`: if `f : α → β`, `g : α → γ` and `j : β → γ` are injective and `g` and `j` have disjoint ranges, then `Function.extend f g j` is injective. It is used in leanprover/cslib#401 to build families of total orders for a comparison-sorting lower bound, via `Function.extend f Sum.inl Sum.inr : β → α ⊕ β`.

🤖 Prepared with Claude Code
@Shreyas4991

Shreyas4991 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

@eric-wieser I would be grateful for similar deduplication commits on #685 to ensure proper comparability.

Minor follow up work cannot be normally used to justify denying credit and stewardship of an approach in an open source project when a follow up PR such as this fundamentally copies the design of an earlier in-progress PR. At least as far as ethical standards are concerned.

But it might still be better to have parity of support from the official side.

EDIT : The approval was accidental.

Comment on lines +29 to +64
/-- Split a list into contiguous halves; if the length is odd, the first half is one element
longer. This agrees with `List.MergeSort.Internal.splitInTwo`, so that `mergeSort` agrees
with `List.mergeSort`. -/
@[expose] def split (xs : List α) : List α × List α :=
(xs.take ((xs.length + 1) / 2), xs.drop ((xs.length + 1) / 2))

@[simp] theorem split_fst_length_eq (xs : List α) :
(split xs).1.length = (xs.length + 1) / 2 := by
simp [split]
omega

@[simp] theorem split_snd_length_eq (xs : List α) :
(split xs).2.length = xs.length / 2 := by
simp [split]
omega

theorem split_fst_append_split_snd (xs : List α) : (split xs).1 ++ (split xs).2 = xs :=
List.take_append_drop _ xs

variable [Monad m] (cmp : α → α → m Bool)

-- TODO: this is a duplicate of `List.mergeSortM`
/-- Sort a list using merge sort with monadic comparisons. -/
@[expose] def mergeSortM' (xs : List α) : m (List α) :=
match xs with
| [] => return []
| [x] => return [x]
| x :: y :: zs => do
let halves := split (x :: y :: zs)
let sl ← mergeSortM' halves.1
let sr ← mergeSortM' halves.2
mergeM sl sr cmp
termination_by xs.length
decreasing_by
· simp only [split_fst_length_eq, List.length_cons]; omega
· simp only [split_snd_length_eq, List.length_cons]; omega

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.

Ideally we would delete these too. The main inconvenience in doing so is that we need to replace List.split with List.MergeSort.Internal.splitInTwo in the theorem statements, which leads to quite a long goal state.

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