Viewing as: Guest  |  Log in to post 🟣 14 users online  |  Recent Posts  |  + New Thread

🌌 Universe Polymorphism in Rocq — impredicativity crimes and how to avoid them

👤 Started by ProposNyan 📅 14 Mar 2025, 02:47 💬 27 replies 👁 1,482 views 🔥 Hot Thread Type Theory Rocq/Coq Universes
Status: Open
Last reply: CoCKitty · 22 Mar 2025
Pinned: No
Solved: Partially
Post #1  ·  OP

ok so i've been hitting my head against universe errors for three days and i need to talk about it with people who understand my pain 😭🐱

let me set the scene. Rocq (yes i'm using the new name, deal with it) is based on the predicative Calculus of Inductive Constructions (pCIC). The hierarchy is built from an impredicative sort Prop and an infinite tower of predicative Typei universes. This is actually really elegant on paper but in practice? Crimes. Crimes everywhere.

The classic crime: trying to self-apply the identity function.

rocq / gallina
(* Monomorphic identity — the trap *) Definition identity {A : Type} (a : A) := a. (* Now try to self-apply... *) Fail Definition selfid := identity (@identity). (* Error: The term "@identity" has type "forall A : Type, A -> A" while it is expected to have type "?A" (unable to find a well-typed instantiation for "?A": cannot ensure that "Type@{identity.u0+1}" is a subtype of "Type@{identity.u0}") *)

By default, constant declarations are monomorphic — the identity function declares a single global universe identity.u0 for its domain. Trying to pass @identity to itself requires identity.u0+1 ≤ identity.u0, which is obviously impossible. A universe variable would need to be less than itself in the hierarchy — instant inconsistency.

The fix: Set Universe Polymorphism (or prefix with Polymorphic). This lets definitions bind their own local universe variables instead of polluting the global constraint graph.

rocq / gallina
Set Universe Polymorphism. Definition identity {A : Type} (a : A) := a. (* Now this works! Each use gets a fresh universe instance *) Definition selfid := identity (@identity). (* selfid : forall A : Type, A -> A ✓ *)

Now my actual question: I'm building a generic container that needs to work at multiple universe levels, and I keep running into headaches when mixing polymorphic and monomorphic code. What are your strategies for avoiding the pain? Do you just slam Set Universe Polymorphism. at the top of every file and pray? 😿

-- ProposNyan :: Prop ≠ False (usually) :: rocq-prover.org enjoyer
Post #2
↩ ProposNyan wrote:
Do you just slam Set Universe Polymorphism. at the top of every file and pray?

Lmao, essentially yes, but there are caveats 😹

The key insight is that universe polymorphism gives local bindings for universes and constraints, so generic definitions become reusable at different levels. It provides the same kind of code reuse you get from ML-style parametric polymorphism, except the structure of the universe hierarchy is way more complex than bare polymorphic type variables.

The big gotcha when you "slam polymorphism everywhere": there's actually a performance penalty. Using polymorphism is at least linear in the number of fresh universe variables produced during a proof — benchmarks showed around a 10% increase in compilation time on the standard library when all primitive types were made polymorphic.

My actual strategy:

1. Use Polymorphic prefix selectively on definitions that genuinely need it
2. Use Monomorphic prefix for things that should have global constraints
3. Use Set Printing Universes. liberally when debugging — you need to see the @{u} annotations to understand what's happening

rocq — explicit universe declaration
(* Explicit universe bounds — very explicit, very safe *) Polymorphic Definition le@{i j} (A : Type@{i}) : Type@{j} := A. (* Print le reveals the generated constraint: i <= j *) Print le. (* le@{i j} = fun A : Type@{i} => A : Type@{i} -> Type@{j} (* i j |= i <= j *) *)

When you declare universe bounds explicitly, Rocq shows you exactly what constraints were generated. This makes debugging SO much easier than staring at an inconsistency error with no context.

-- CoCKitty :: Mod of CGPA :: "A universe inconsistency is just a constraint you forgot" ✨
Post #3

Can we talk about impredicativity crimes specifically? Because I think OP is dancing around the real danger zone: Prop vs Set vs the Type hierarchy.

The Calculus of Inductive Constructions has Prop as an impredicative sort — you can quantify over arbitrarily large types inside Prop and the result still lives in Prop. This is what makes classical logic encodings work.

rocq — impredicativity of Prop
(* Prop is impredicative: ∀ (A : Type), ... stays in Prop *) Check (∀ (P : Prop), PP) : Prop. (* ✓ fine *) (* Type is predicative — Type@{i} contains Type@{j} only if j < i *) Check (∀ (A : Type), AA). (* : Type@{u+1} (must be strictly above the quantified universe) *)

Now, the historical crime: impredicative Set. In old versions of Coq, Set was impredicative by default. Later versions made Set predicative to avoid inconsistency with classical axioms. In particular, you have to watch out when using impredicative Set with axioms of choice — combined with excluded middle or predicate extensionality, you get inconsistency.

⚠ CRIME SCENE
-impredicative-set flag: still exists, still dangerous.
Impredicative Set + excluded middle + predicate extensionality = 💥 inconsistency
Almost all Rocq developments get by fine without it.

The reason Type can't be impredicative is even more fundamental — if it were, you could encode Girard's System U in Rocq, and System U is known to be inconsistent (via Hurkens' paradox). The encoding lives at Logic/Hurkens.v in the standard library if you want to witness the crime in situ.

-- RocqCatFan :: "impredicativity bad (except Prop, Prop is based)" 🐾
Post #4
↩ RocqCatFan wrote:
you could encode Girard's System U in Rocq, and System U is known to be inconsistent

Adding to this: a predicative system enforces the constraint that, when an object is defined using some sort of quantifier, none of the quantifiers may ever be instantiated with the object itself. Russell's paradox is exactly what happens when you break this — "the set of all sets that do not contain themselves" is a self-referential quantification nightmare.

OK but here's the practical thing OP probably needs: the parameters vs indices trick. This bit me HARD last week.

rocq — parameters save universe levels
(* GOOD: A and B as parameters — level is max(u_A, u_B) *) Inductive prod (A : Type) (B : Type) : Type := | pair : ABprod A B. (* Fine! prod lives at max universe, not max+1 *) Check (nat, (Type, Set)). (* (nat, (Type@{Top.44}, Set)) : Set * (Type@{Top.45} * Type@{Top.46}) ✓ *) (* BAD: A and B as constructor arguments — forces strictly less-than *) Inductive prod' : TypeTypeType := | pair' : ∀ A B : Type, ABprod' A B. Fail Check (pair' nat (pair' Type Set)). (* Error: Universe inconsistency (cannot enforce Top.51 < Top.51) 💀 *)

The rule: parameters introduce constraints while constructor arguments introduce < constraints. That strict inequality is what causes the "cannot enforce X < X" crimes. Always prefer parameters when possible!

-- FormalFelinae :: "dependent types are just types that care about you" 💖
Post #5  ·  OP
↩ FormalFelinae wrote:
parameters introduce ≤ constraints while constructor arguments introduce < constraints

WAIT this is the thing I was missing 😱🐱 okay that actually explains SO much. I had a whole polymorphic record that was inducing strict inequalities when it didn't need to.

So I rewrote my container using cumulative inductive types and now it's actually sane:

rocq — polymorphic cumulative list
Set Universe Polymorphism. Set Polymorphic Inductive Cumulativity. Set Printing Universes. Polymorphic Cumulative Inductive mylist {A : Type} := | mnil : mylist | mcons : Amylistmylist. Print mylist. (* Inductive mylist@{u} (A : Type@{u}) : Type@{max(Set,u)} := mnil : mylist@{u} | mcons : A -> mylist@{u} -> mylist@{u}. (* *u |= *) ← irrelevant universe, cumulativity from any level ✓ *)

The *u annotation means the universe u is irrelevant — so mylist@{i} A and mylist@{j} B are convertible as long as A and B are. This is exactly what I needed for my generic container! 🎉

Now I just need to understand cumulativity variance annotations (=, +, * for invariant, covariant, irrelevant) and I think I'll be set. Any resources beyond the Rocq manual?

-- ProposNyan :: Prop ≠ False (usually) :: rocq-prover.org enjoyer
Post #6  ·  MOD

Since this thread is getting good, let me give you the full variance lecture 📖🐈

For cumulative inductive types, each universe parameter has a variance: either invariant (=), covariant (+), or irrelevant (*). You can annotate these explicitly:

rocq — explicit variance annotations
(* a: auto-inferred (irrelevant), *b: forced irrelevant, +c: covariant, =d: invariant *) Polymorphic Cumulative Inductive Dummy@{a *b +c =d} : Prop := dummy. About Dummy. (* Dummy@{a b c d} : Prop (* *a *b +c =d |= *) Dummy is universe polymorphic *) (* Monad example: monad universe is INVARIANT because m appears on left side of arrow — can't coerce *) Polymorphic Cumulative Record Monad@{u} := { m : Type@{u}Type@{u}; (* left of arrow → invariant *) ret : ∀ {A : Type@{u}}, Am A; bind : ∀ {A B : Type@{u}}, m A → (Am B) → m B }. (* Monad@{u} is invariant in u: you cannot lift a monad from a lower to a higher universe. *)

The invariant case is the most restrictive and often surprises people. If your type constructor appears on the left side of a function arrow, it induces an invariant constraint — you can't coerce between different universe levels. This is why monadic code sometimes explodes when you try to compose things at different universe levels.

🐱 Practical tip

Use Show Universes. inside proofs to inspect the current universe context. And Set Printing Universes. globally to see all the @{...} annotations. Without these, you're debugging blind.

-- CoCKitty :: Mod of CGPA :: "A universe inconsistency is just a constraint you forgot" ✨
Post #7

Something I want to add to this thread about sort polymorphism, which is a newer feature and I feel like nobody talks about it.

Universe polymorphism handles Type@{u} levels, but it originally couldn't instantiate universe parameters with Prop or SProp. If you tried:

rocq — sort polymorphism
Polymorphic Definition type@{u} := Type@{u}. Fail Check type@{Prop}. (* Error: Universe instances cannot contain non-Set small levels, polymorphic universe instances must be greater or equal to Set. *) (* Solution: sort polymorphism with explicit sort quality variables *) Polymorphic Definition sort@{s | u |} := Type@{s|u}. (* Now you can instantiate with Prop, SProp, or Type *) Eval cbv in sort@{Prop|Set}. (* = Prop ✓ *) Eval cbv in sort@{Type|Set}. (* = Set ✓ *)

Definitions which quantify over sort qualities are called sort polymorphic. It lets you write a single definition that works uniformly across Prop, SProp, and Type universes. All sort quality variables must be explicitly bound between the two | separators in the universe declaration.

When no explicit instantiation is provided or _ is used, a temporary variable is generated — useful for local reasoning without committing to a specific sort.

-- RocqCatFan :: "impredicativity bad (except Prop, Prop is based)" 🐾
Post #8

Resources for OP and anyone else deep in this rabbit hole:

📖 Adam Chlipala's CPDT — Chapter on Universes — old but still accurate for the fundamentals
📄 Sozeau & Tabareau ITP 2014 — the original universe polymorphism paper for Coq
📚 Rocq Reference Manual — Polymorphic Universes — dry but comprehensive
🧵 CGPA thread on Hurkens' paradox — if you want to see what happens without predicativity constraints

Also one more practical crime to be aware of — when you mix polymorphic and monomorphic code, unification can force universe identifications that you didn't intend:

rocq — mixing poly/mono is dangerous
(* If myList is polymorphic but you use it monoMorphically... *) (* listi True = listProp True will force i = Prop, even if i was intended to be strictly higher — inconsistency! *) (* Safe pattern: use explicit Type to raise the level *) Definition safe_lift (A : Type) : Type := A. (* The extra indirection gives cumulativity room to breathe *)

The subtle thing is that unification of a polymorphic inductive type instantiated at the same parameters but different universes will force their identification. This mainly bites you when mixing polymorphic and monomorphic code, and can always be avoided with explicit Type annotations to raise the lowest level using cumulativity.

-- FormalFelinae :: "dependent types are just types that care about you" 💖
Post #9  ·  OP

ok i fixed my container. here's the post-mortem in case anyone finds themselves in the same situation 🐱

The original problem: I had a polymorphic record holding a monad-like structure, and I was trying to reuse it at two different universe levels in the same proof. The universe unification was forcing u1 = u2 when I needed u1 ≤ u2.

Root cause: My record had a field whose type appeared on the left of an arrow, making the universe invariant rather than covariant. One small refactor to move that to a parameter and add Cumulative fixed it completely.

Lessons:
✅ Use Set Printing Universes. first, always, before anything else
✅ Prefer parameters over constructor arguments
✅ Mark definitions as Polymorphic explicitly rather than using the global flag
✅ Watch out for invariant universes from left-of-arrow appearances
❌ Don't mix polymorphic and monomorphic code carelessly
❌ Don't use -impredicative-set unless you know exactly what you're doing

thank you everyone, this forum is the only reason i haven't abandoned formal verification for interpretive dance 🐾💜

-- ProposNyan :: Prop ≠ False (usually) :: rocq-prover.org enjoyer

✏️ Post a Reply

You must log in to reply. Not a member? Register here — it's free and we have cats 🐱
Login to post

🔗 Related Threads in Type Theory & Formal Verification

Hurkens' Paradox and why Type cannot be impredicative CoCKitty · 43 replies
Prop vs SProp — when do you actually want strict propositions? FormalFelinae · 19 replies
Migrating from Coq to Rocq — what actually changed? RocqCatFan · 31 replies
How does Mathlib handle universe polymorphism at scale? ProposNyan · 22 replies
HoTT vs CIC — univalence and the universe hierarchy CoCKitty · 67 replies
Cumulative inductive types — a deep dive into variance FormalFelinae · 15 replies
Classical axioms in Rocq — what's safe to add? RocqCatFan · 28 replies