ADR-005: Scope of the 𝓞 / 𝓥 universe-level variables¶
Status¶
Accepted — 2026-06-06 (M4-3).
Context¶
𝓞 and 𝓥 are not generic level variables. They are semantically charged: throughout the library 𝓞 denotes the universe level of a signature's operation-symbol type and 𝓥 the level of its arity type. The convention is load-bearing — Signature 𝓞 𝓥, Op, Term, the ov shorthand, and every signature-parametrized module read against it — and the style guide already states that "using 𝓞 or 𝓥 for anything else is a bug."
They are currently declared once, as a public (non-private) variable 𝓞 𝓥 : Level in Overture.Signatures, co-located with the Signature type whose two levels they name, and re-exported through the Overture umbrella. Signature-parametrized modules pull them in by name (open import Overture using ( 𝓞 ; 𝓥 ; Signature )). Being public, they are also in scope wherever Overture is opened without a using filter — which the style guide confines to umbrella aggregators. The convenience is real, but a public variable can be shadowed by a careless local declaration, and a newcomer may not immediately know where an unqualified 𝓞 came from.
A decision is forced now (M4-3) by a second, concrete problem: the handling is not uniform. Although the style guide says "never re-declare them," 𝓥 is in fact re-declared as a private variable in Overture.Operations and Setoid.Relations.Discrete (and in several frozen Legacy/Base/* modules), drifting from the canonical declaration. Issue #269 asks that 𝓞 / 𝓥 be handled uniformly and the rule recorded. Three options were on the table:
- Keep
𝓞/𝓥as a single publicvariableinOverture.Signatures; document the rule and import them by name. - Make them
privateinOverture.Signatures; every downstream module re-declares its ownvariable 𝓞 𝓥 : Level. - Move them to a dedicated
Overture.UniverseLevelsmodule, imported explicitly.
Decision¶
Adopt Option 1. 𝓞 and 𝓥 remain a single public variable 𝓞 𝓥 : Level declared in Overture.Signatures, co-located with the Signature type whose levels they name; downstream modules import them by name from Overture (or Overture.Signatures) and never re-declare them. Using either name for anything other than its charged meaning remains a bug.
Applying this uniformly (issue #269's second acceptance criterion) means converting the live-tree drift sites — the private variable … 𝓥 … declarations in Overture.Operations and Setoid.Relations.Discrete — to import the canonical 𝓥 instead. The frozen Legacy/Base/* tree is out of scope per ADR-001 and the M4 audit; its local re-declarations are left untouched. The style-guide section "The 𝓞 / 𝓥 convention" is updated to drop the "tracked in M4-3 / interim" hedge and state the rule as settled.
Consequences¶
- One canonical declaration, co-located with its meaning. A reader who lands on
Signature 𝓞 𝓥finds the two levels, their charged semantics, and the prose explaining them in the same module — the "one canonical form per concept" principle applied to a convention rather than a definition. - Lowest churn of the three options, and it ratifies the de-facto rule. The style guide already mandates Option 1's behavior as an interim rule, so this decision only removes the "interim" hedge and fixes the two live drift sites; no
using ( 𝓞 ; 𝓥 ; Signature )import is touched. - The common case stays a one-line import. Signature-parametrized modules — the majority — need
Signaturetogether with its two levels, so a singleopen import Overture using ( 𝓞 ; 𝓥 ; Signature )is exactly right. - The public
variableis not leak-proof (negative). It stays in scope under a bareopen import Overture, so a local declaration can shadow it. This is mitigated, not eliminated: theusing-list discipline confines bare opens to umbrella aggregators, and the "never re-declare / off-convention-use-is-a-bug" rules become settled policy instead of an open question. - A levels-only consumer acquires a
Signaturesdependency (negative). A module that needs𝓥but notSignature—Overture.Operations, whoseOpindexes by an arity level — must import the level fromOverture.Signatures, coupling two small foundational modules (acyclically). Option 3 would have avoided this edge.
Alternatives considered¶
- Option 2 —
privateinOverture.Signatures, re-declare downstream. Rejected. It fragments a single charged convention across dozens of modules, contradicting "one canonical form per concept"; it reverses the existing "never re-declare" rule; it carries the widest blast radius (boilerplate in every signature-parametrized module, and everyusing ( 𝓞 ; 𝓥 )import removed); and it makes "off-convention use is a bug" harder to police once the names are declared locally everywhere. Its one merit — matching stdlib's per-moduleprivate variableidiom — does not transfer, because stdlib'sa/ℓare generic whereas𝓞/𝓥carry fixed meaning. - Option 3 — dedicated
Overture.UniverseLevelsmodule. Considered; the closest runner-up. It keeps one source of truth, makes the import self-documenting (open import Overture.UniverseLevels using ( 𝓞 ; 𝓥 )), and lets a levels-only consumer avoid depending onOverture.Signatures. Rejected as the primary choice because it decouples the𝓞/𝓥semantics from theSignaturetype they describe (where co-location most helps a reader); it makes the common signature-parametrized case a two-import affair rather than one; it does not actually remove the leak unless theOvertureumbrella also stops re-exporting the new module — trading the leak for churn at every call site and a less convenient API; and a whole module for twovariabledeclarations is marginal over-engineering. It remains the natural fallback if the decoupling or full leak-elimination is later judged worth the cost.
References¶
- Issue M4-3 — Design discussion: scope of
𝓞and𝓥. docs/STYLE_GUIDE.md— sections "The𝓞/𝓥convention" (the interim rule this ADR settles) and "Other level variables".src/Overture/Signatures.lagda.md— the declaration site and the co-located semantics prose.- ADR-001 — Setoid as canonical tree; the
Legacy/Base/freeze that scopes Legacy out of the uniformity sweep.