Posted by Cursor assistant (model: GPT-5.4) on behalf of the user (Quang Dao) with approval.
This design direction arose out of discussion with Julian Sutherland (@Julek), Katy Hristova (@katyhr), and Derek Sorensen (@dhsorens).
Summary
We should generalize CMvPolynomial from the current finite-arity surface
to a variable-indexed surface
for an arbitrary ordered index type σ, possibly infinite.
The key design constraint is that this remains a computable multivariate polynomial representation. In particular, we do not want to simply redefine the core in terms of Finsupp, because that defeats the purpose of this library’s computable sparse runtime representation.
The design direction described below is the one currently implemented in the in-progress branch:
- sparse monomials as canonical arrays of
(σ × Nat) entries
- sparse outer polynomial storage as
Std.ExtTreeMap (CMvMonomial σ) R
- comparator-style assumptions on variables (
Ord / TransOrd / LawfulEqOrd), not LinearOrder
Finsupp used only at the boundary for MvPolynomial interoperability
This issue documents:
- the design choice
- why we think it is the right one
- what alternatives we considered and rejected
- how this compares to other computer algebra systems
- the current blocker: normalization and sorting proofs, especially
qsort vs List.mergeSort
Goal
We want the computable multivariate polynomial layer to support arbitrary variable types σ, including possibly infinite ones, while preserving:
- sparse computable storage
- support-driven algorithms
- canonical monomial keys
- a public surface closer to
Mathlib’s MvPolynomial σ R
Today, much of the existing core is built around Fin n and dense exponent vectors. That is workable for fixed finite arity, but it bakes ambient-dimension assumptions into many operations and creates friction with the MvPolynomial surface.
The generalized core should instead support:
X : σ → CMvPolynomial σ R
- support-driven
degreeOf, vars, degrees, restrictDegree, eval, rename, bind₁
- variable types
σ that may be infinite
- a bridge to
MvPolynomial σ R at the boundary
without sacrificing computability of the core representation.
Chosen design
Monomials
A monomial is represented as a sparse finite Nat-valued map on variables σ, where:
- missing variable means exponent
0
- stored zero exponents are forbidden
- the representation is canonical
Concretely, the runtime representation is:
- a canonical sparse array of
(σ × Nat) entries
- sorted by variable
- with duplicate variables merged
- with zero exponents removed
- with cached
totalDegree
The current implementation branch models this through a raw/lawful split:
RawMonomial σ stores:
totalDegree : Nat
entries : Array (σ × Nat)
CMvMonomial σ is the canonical lawful wrapper
This preserves the semantic model of a finite Nat-valued map while keeping the concrete runtime representation sparse and efficient.
Polynomials
A polynomial remains a sparse outer map from monomials to coefficients:
Unlawful σ R := Std.ExtTreeMap (CMvMonomial σ) R
Lawful σ R := { p : Unlawful σ R // no zero coefficients }
CMvPolynomial σ R := Lawful σ R
So the overall design is sparse at both levels:
- sparse monomials
- sparse outer polynomial map
Typeclass assumptions on variables
We want to stay at comparator granularity rather than requiring LinearOrder.
The intended contract is:
- raw normalization code uses
[Ord σ]
- lawful monomials / polynomial APIs use
[TransOrd σ] [LawfulEqOrd σ]
This is the right fit for the stdlib ordered-map world we are already using.
Finsupp only at the boundary
Finsupp is still useful and appropriate for the Mathlib bridge:
CMvMonomial.toFinsupp
CMvMonomial.ofFinsupp
CMvPolynomial.support : Finset (σ →₀ ℕ)
- conversions to and from
MvPolynomial σ R
But Finsupp should not become the core runtime representation.
Why this design
1. It preserves computability
This is the core reason.
A direct redefinition in terms of Finsupp would make the representation mathematically convenient, but it would give up the explicit computable sparse runtime representation that CompPoly is trying to provide.
The chosen design keeps the runtime model explicit:
- sparse support
- canonical normalization
- ordered storage
- computable key comparison
2. It supports arbitrary and infinite variable types
Dense exponent vectors fundamentally assume a finite ambient dimension. That is exactly what we want to move away from.
With sparse support arrays:
- only variables that actually occur are stored
- only stored support is traversed
- infinite
σ becomes perfectly natural
This is a major improvement for:
degreeOf
totalDegree
vars
degrees
restrictDegree
evalMonomial
rename
3. It is symmetric in variables
A recursive sparse representation would privilege one “main variable” at each layer. That is attractive for some algorithms, but it is the wrong default for the surface we want.
The array-of-support design keeps variables symmetric and fits better with:
- arbitrary index types
rename : (σ → τ) → ...
bind₁
- a surface aligned with
MvPolynomial σ R
4. It is a good performance compromise
We considered tree-backed sparse monomials as well, but monomials are also keys of the outer ExtTreeMap. That makes the concrete monomial representation very important.
A sparse monomial stored as a canonical sorted array is preferable to a tree-backed monomial because:
- key comparison is zero-allocation lexicographic array comparison
- memory layout is tighter
- monomial add/divide/divides become linear merges over sorted supports
- we avoid the “tree as key” problem where extensional comparison wants flattening/caching anyway
So the current choice is:
- outer tree map for polynomials
- inner sorted sparse arrays for monomials
That looks like the right split.
Alternative designs we considered
A. Keep dense exponent tuples (Vector ℕ n-style monomials)
This is closest to the old design.
Benefits
- simpler proofs
- simpler
MvPolynomial bridge
- very good constants for tiny dense
Fin n workloads
Drawbacks
- fundamentally tied to finite ambient arity
- ambient-dimension scans everywhere
- poor fit for arbitrary or infinite variable types
- worse for sparse monomials in large ambient spaces
This does not meet the goal of a genuinely generic CMvPolynomial σ R.
B. Represent monomials as ExtTreeMap σ Nat
This is the most obvious “sparse map” design if one reasons only semantically.
Benefits
- sparse lookup by variable
- conceptually close to “finite map from variables to exponents”
Drawbacks
- monomials are also keys of the outer polynomial map
- extensional comparison of tree-backed monomials is awkward
- comparing monomials by flattening/sorting their entries would allocate and be expensive
- memory layout is much worse than a compact sorted array
We concluded that a tree-backed monomial is the wrong concrete runtime representation, even though the semantics are indeed “finite Nat-valued map plus invariant”.
C. Make the whole polynomial recursively sparse
This is closer to FriCAS / Maxima style.
Benefits
- better fit for algorithms that privilege a main variable
- more natural for elimination-style division and GCD-style recursion
finSucc / split-variable equivalences become more structural
Drawbacks
- loses symmetry between variables
- less natural surface for
MvPolynomial σ R
- renaming and arbitrary-index substitution become more awkward
- poorer fit for the “generic arbitrary index type” goal
We do not think recursive sparse should be the core representation for this migration.
D. Use an outer sorted term array rather than ExtTreeMap
This is closer to a Singular-style sorted term list.
Benefits
- better iteration locality
- attractive for term-heavy kernels
- potentially a better fit for future Gröbner-oriented algorithms
Drawbacks
- coefficient lookup is worse
- maintaining canonicality after insertion is more work
- incremental arithmetic is more awkward than with a tree map
This remains a plausible future alternative for performance experiments, but it is not the right move for the current cutover.
E. Rebuild the core around Finsupp
This would maximize parity with MvPolynomial.
Benefits
- simplest bridge to Mathlib
- easiest statement of theorems
- closest surface alignment
Drawbacks
- gives up the explicit computable sparse runtime representation
- defeats the point of
CompPoly’s computable multivariate layer
This is the main alternative we explicitly do not want.
Comparison with other computer algebra systems
The chosen design is best understood as a sparse distributed representation.
Sparse distributed systems
Examples:
- Sage generic fallback
- Maple POLY
- Mathematica’s polynomial-facing
CoefficientRules view
- likely the practical storage view in Singular
CompPoly is closest to this family, with one extra twist:
- monomials themselves are also stored sparsely
So instead of full exponent tuples, we store only the nonzero support of each monomial.
Why this is good
- supports arbitrary ordered
σ
- works naturally for infinite variable types
- avoids ambient scans
- keeps support-driven operations efficient
Cost
- bridge proofs to
MvPolynomial are harder than if we stored full tuples
Recursive sparse systems
Examples:
- FriCAS
- Maxima CRE polynomial form
These privilege a main variable and recurse on the coefficient side.
This can be a good design for elimination-heavy algorithms, but it is not the best fit for:
- arbitrary index types
- symmetric variable operations
- an
MvPolynomial-aligned public surface
Recursive dense systems
Example:
This is the wrong fit for CompPoly’s goals:
- poor for sparse support
- poor for infinite variable types
- poor fit for an explicitly sparse computable design
Current blockers
The main blocker is not the runtime representation itself. The runtime direction is reasonably clear.
The blocker is the proof story around normalization.
The current normalization pipeline
Right now the sparse monomial normalization path is effectively:
- sort entries by variable
- merge adjacent duplicates
- drop zero exponents
In the current implementation branch this is expressed through:
Array.qsortOrd
Array.mergeAdjacentDups from Batteries
filter
This is fine computationally, but the theorem surface is weak.
Why this blocks the bridge
To finish the MvPolynomial bridge cleanly, we need monomial roundtrip lemmas such as:
toFinsupp_single
toFinsupp_add
toFinsupp_ofFinsupp
ofFinsupp_toFinsupp
Those in turn need semantic correctness lemmas for normalization:
- normalization preserves the exponent-map semantics
- normalization is canonical
- normalization is identity on already canonical input
That is exactly where the current proof burden spikes.
Sorting algorithm choice: qsort vs List.mergeSort
This is the immediate engineering decision.
Option 1: stay with Array.qsortOrd
Pros
- runtime-friendly
- natural for the current array-backed representation
- already in stdlib
- likely the best constant factors among the obvious pure Lean options
Cons
- theorem API is currently very thin
- proving
qsort correctness from current public API is nontrivial
- Batteries merge helpers also have very little semantic theorem surface
- this makes monomial normalization proofs expensive
We spun off a branch specifically to probe this:
That branch contains a small exploratory module:
CompPoly/Data/Array/QSortApiProofs.lean
The conclusion from that probe is:
- a small theorem slice is easy
- the real permutation/sortedness API for
qsort is feasible in principle
- but a clean upstream story likely requires exposing/supporting induction over the recursive
qpartition / qsort helpers and also upstreaming some small Perm.extract-style lemmas
Lean already has a historical draft in this direction:
qsort_grind branch
- proof file:
tests/lean/run/grind_qsort.lean
So the qsort route is attractive, but it is a larger proof project.
Option 2: switch normalization proofs to List.mergeSort
Pros
- much better existing theorem surface
- standard lemmas already available:
- permutation
- membership preservation
- pairwise sortedness
- identity on sorted input
- much easier to prove canonicalization and roundtrip lemmas
- likely the shortest path to finishing the
MvPolynomial bridge
Cons
- normalization becomes list-based in the proof-critical path
- may lose some constant factors relative to the direct array path
- requires converting
Array to List and back in normalization
This is currently the most pragmatic path if the priority is:
- finishing the generalized bridge
- reducing proof burden
- keeping the runtime representation as
Array
In that design, we would still store monomials as arrays, but define normalization through:
entries.toList
List.mergeSort
- a repo-local
collapseSorted
toArray
Current recommendation
The representation choice itself is sound:
- monomials: canonical sparse arrays
- polynomials:
ExtTreeMap on canonical monomial keys
- variables: arbitrary ordered type
σ
Finsupp: boundary only
The open design question is not “what should a monomial be?” anymore.
The open question is:
- how should we implement and prove normalization?
At this point the two realistic next steps are:
-
Pragmatic route
- switch normalization proofs to
List.mergeSort
- finish the
MvPolynomial bridge
- keep array runtime storage
-
Upstream / ecosystem route
- push on
qsort / Batteries merge theorem infrastructure
- then come back and finish the CompPoly bridge on top of stronger upstream APIs
Given current project priorities, I think (1) is the better immediate route for CompPoly, while (2) is a worthwhile ecosystem contribution in parallel.
Questions for follow-up
- Do we want to prioritize shipping the generalized
CMvPolynomial σ R bridge, or investing first in upstream qsort / merge theorem APIs?
- Do we want to keep the current existential notion of monomial lawfulness, or move to a more explicit canonical-form predicate for proof friendliness?
- Should we separate storage order from algebraic monomial order now, or defer that until Gröbner-style algorithms become a concrete priority?
Posted by Cursor assistant (model: GPT-5.4) on behalf of the user (Quang Dao) with approval.
This design direction arose out of discussion with Julian Sutherland (@Julek), Katy Hristova (@katyhr), and Derek Sorensen (@dhsorens).
Summary
We should generalize
CMvPolynomialfrom the current finite-arity surfaceto a variable-indexed surface
for an arbitrary ordered index type
σ, possibly infinite.The key design constraint is that this remains a computable multivariate polynomial representation. In particular, we do not want to simply redefine the core in terms of
Finsupp, because that defeats the purpose of this library’s computable sparse runtime representation.The design direction described below is the one currently implemented in the in-progress branch:
(σ × Nat)entriesStd.ExtTreeMap (CMvMonomial σ) ROrd/TransOrd/LawfulEqOrd), notLinearOrderFinsuppused only at the boundary forMvPolynomialinteroperabilityThis issue documents:
qsortvsList.mergeSortGoal
We want the computable multivariate polynomial layer to support arbitrary variable types
σ, including possibly infinite ones, while preserving:Mathlib’sMvPolynomial σ RToday, much of the existing core is built around
Fin nand dense exponent vectors. That is workable for fixed finite arity, but it bakes ambient-dimension assumptions into many operations and creates friction with theMvPolynomialsurface.The generalized core should instead support:
X : σ → CMvPolynomial σ RdegreeOf,vars,degrees,restrictDegree,eval,rename,bind₁σthat may be infiniteMvPolynomial σ Rat the boundarywithout sacrificing computability of the core representation.
Chosen design
Monomials
A monomial is represented as a sparse finite
Nat-valued map on variablesσ, where:0Concretely, the runtime representation is:
(σ × Nat)entriestotalDegreeThe current implementation branch models this through a raw/lawful split:
RawMonomial σstores:totalDegree : Natentries : Array (σ × Nat)CMvMonomial σis the canonical lawful wrapperThis preserves the semantic model of a finite
Nat-valued map while keeping the concrete runtime representation sparse and efficient.Polynomials
A polynomial remains a sparse outer map from monomials to coefficients:
Unlawful σ R := Std.ExtTreeMap (CMvMonomial σ) RLawful σ R := { p : Unlawful σ R // no zero coefficients }CMvPolynomial σ R := Lawful σ RSo the overall design is sparse at both levels:
Typeclass assumptions on variables
We want to stay at comparator granularity rather than requiring
LinearOrder.The intended contract is:
[Ord σ][TransOrd σ] [LawfulEqOrd σ]This is the right fit for the stdlib ordered-map world we are already using.
Finsupponly at the boundaryFinsuppis still useful and appropriate for the Mathlib bridge:CMvMonomial.toFinsuppCMvMonomial.ofFinsuppCMvPolynomial.support : Finset (σ →₀ ℕ)MvPolynomial σ RBut
Finsuppshould not become the core runtime representation.Why this design
1. It preserves computability
This is the core reason.
A direct redefinition in terms of
Finsuppwould make the representation mathematically convenient, but it would give up the explicit computable sparse runtime representation thatCompPolyis trying to provide.The chosen design keeps the runtime model explicit:
2. It supports arbitrary and infinite variable types
Dense exponent vectors fundamentally assume a finite ambient dimension. That is exactly what we want to move away from.
With sparse support arrays:
σbecomes perfectly naturalThis is a major improvement for:
degreeOftotalDegreevarsdegreesrestrictDegreeevalMonomialrename3. It is symmetric in variables
A recursive sparse representation would privilege one “main variable” at each layer. That is attractive for some algorithms, but it is the wrong default for the surface we want.
The array-of-support design keeps variables symmetric and fits better with:
rename : (σ → τ) → ...bind₁MvPolynomial σ R4. It is a good performance compromise
We considered tree-backed sparse monomials as well, but monomials are also keys of the outer
ExtTreeMap. That makes the concrete monomial representation very important.A sparse monomial stored as a canonical sorted array is preferable to a tree-backed monomial because:
So the current choice is:
That looks like the right split.
Alternative designs we considered
A. Keep dense exponent tuples (
Vector ℕ n-style monomials)This is closest to the old design.
Benefits
MvPolynomialbridgeFin nworkloadsDrawbacks
This does not meet the goal of a genuinely generic
CMvPolynomial σ R.B. Represent monomials as
ExtTreeMap σ NatThis is the most obvious “sparse map” design if one reasons only semantically.
Benefits
Drawbacks
We concluded that a tree-backed monomial is the wrong concrete runtime representation, even though the semantics are indeed “finite
Nat-valued map plus invariant”.C. Make the whole polynomial recursively sparse
This is closer to FriCAS / Maxima style.
Benefits
finSucc/ split-variable equivalences become more structuralDrawbacks
MvPolynomial σ RWe do not think recursive sparse should be the core representation for this migration.
D. Use an outer sorted term array rather than
ExtTreeMapThis is closer to a Singular-style sorted term list.
Benefits
Drawbacks
This remains a plausible future alternative for performance experiments, but it is not the right move for the current cutover.
E. Rebuild the core around
FinsuppThis would maximize parity with
MvPolynomial.Benefits
Drawbacks
CompPoly’s computable multivariate layerThis is the main alternative we explicitly do not want.
Comparison with other computer algebra systems
The chosen design is best understood as a sparse distributed representation.
Sparse distributed systems
Examples:
CoefficientRulesviewCompPoly is closest to this family, with one extra twist:
So instead of full exponent tuples, we store only the nonzero support of each monomial.
Why this is good
σCost
MvPolynomialare harder than if we stored full tuplesRecursive sparse systems
Examples:
These privilege a main variable and recurse on the coefficient side.
This can be a good design for elimination-heavy algorithms, but it is not the best fit for:
MvPolynomial-aligned public surfaceRecursive dense systems
Example:
dmpThis is the wrong fit for CompPoly’s goals:
Current blockers
The main blocker is not the runtime representation itself. The runtime direction is reasonably clear.
The blocker is the proof story around normalization.
The current normalization pipeline
Right now the sparse monomial normalization path is effectively:
In the current implementation branch this is expressed through:
Array.qsortOrdArray.mergeAdjacentDupsfrom BatteriesfilterThis is fine computationally, but the theorem surface is weak.
Why this blocks the bridge
To finish the
MvPolynomialbridge cleanly, we need monomial roundtrip lemmas such as:toFinsupp_singletoFinsupp_addtoFinsupp_ofFinsuppofFinsupp_toFinsuppThose in turn need semantic correctness lemmas for normalization:
That is exactly where the current proof burden spikes.
Sorting algorithm choice:
qsortvsList.mergeSortThis is the immediate engineering decision.
Option 1: stay with
Array.qsortOrdPros
Cons
qsortcorrectness from current public API is nontrivialWe spun off a branch specifically to probe this:
quang/qsort-api-proofs-spikeThat branch contains a small exploratory module:
CompPoly/Data/Array/QSortApiProofs.leanThe conclusion from that probe is:
qsortis feasible in principleqpartition/qsorthelpers and also upstreaming some smallPerm.extract-style lemmasLean already has a historical draft in this direction:
qsort_grindbranchtests/lean/run/grind_qsort.leanSo the
qsortroute is attractive, but it is a larger proof project.Option 2: switch normalization proofs to
List.mergeSortPros
MvPolynomialbridgeCons
ArraytoListand back in normalizationThis is currently the most pragmatic path if the priority is:
ArrayIn that design, we would still store monomials as arrays, but define normalization through:
entries.toListList.mergeSortcollapseSortedtoArrayCurrent recommendation
The representation choice itself is sound:
ExtTreeMapon canonical monomial keysσFinsupp: boundary onlyThe open design question is not “what should a monomial be?” anymore.
The open question is:
At this point the two realistic next steps are:
Pragmatic route
List.mergeSortMvPolynomialbridgeUpstream / ecosystem route
qsort/ Batteries merge theorem infrastructureGiven current project priorities, I think (1) is the better immediate route for CompPoly, while (2) is a worthwhile ecosystem contribution in parallel.
Questions for follow-up
CMvPolynomial σ Rbridge, or investing first in upstreamqsort/ merge theorem APIs?