Skip to content

Copatterns: defining observations, over primitive corecursion - #71

Open
Russoul wants to merge 2 commits into
mainfrom
co-pattern-matching
Open

Copatterns: defining observations, over primitive corecursion#71
Russoul wants to merge 2 commits into
mainfrom
co-pattern-matching

Conversation

@Russoul

@Russoul Russoul commented Aug 28, 2026

Copy link
Copy Markdown
Owner

What

Basic copattern matching for Nova — a def into a ν-type states its one observation and the elaborator synthesizes everything else — built on two coordinated strengthenings of the coinductive scheme in Foundation.

1. Primitive corecursion + coinduction up to equality (theory)

  • el-nu-i/-beta/-eta: the corecursor is now the apomorphism — the coalgebra fills each hole with ν 𝔽 ⊎ a, where inj₁ stops at an existing element and inj₂ continues from a seed. β mediates through the stop-or-continue copair [𝕚𝕕 ‖ hᵉˡ], so a stopped element is released bare, judgementally (the inductive side already ships full dependent elimination; this restores parity — the old scheme was dual to mere iteration).
  • el-nu-coind's relator hole clause is now up to equality: lift_𝕏(R) u v ≜ ∥R ⊎ (u ≡ v ∈ ν 𝔽)∥.
  • Both are admissible, with derivation/subsumption notes extended in NovaFoundation.txt; the setoid model validates them on the nose (equality ⊆ the largest bisimulation).
  • Payoff visible before copatterns even land: cons/csucc become one-stop machines, and tlCons — previously the deliberately η-needing example proven by hand-rolled coinduction — demotes to a β-lemma.

2. The copattern item (docs/NovaElaboration.txt, "Defining observations")

def cons : {a : 𝕌} → a → stream a → stream a using (stream.stream.unfold)
  | out (cons x s) ≔ x, s

An item macro, the clausal def's dual: it asserts the observation determines the definiendum and expands into the batch naming that assertion's pieces — existence (a synthesized primitive corecursor, the body read against the polynomial's shape: literal constructors down to the holes, calls continuing at the varying-columns seed, f-free elements stopping), the Π-closed observation lemma consOut (λ…. ⋆, β in every case), and pointwise uniqueness consEta (coind at the pure graph invariant; stops close in the relator's up-to-equality leg). Tiers degrade exactly as at the clausal def — witness-supplied existence keeps the lemma ⋆ an ordinary obligation, non-fragment bodies demote to declarations, never a wall. No new Foundation rule or kernel capability beyond layer 1.

3. Implicit columns — both macros

LHS and calls follow the ordinary term syntax — implicits elided or {x}-spelled, never spelled as explicit arguments. Placement is resolved by the macro from the item's own type; the values of elided call arguments are verified against the insertion-resolved body by a snapshot-rolled-back probe (conversion to the corecursor happens after implicit placement is resolved), degrading with a spell-it remedy if an elided argument varies. Generated spines apply the item through {…} overrides and a candidate variable fully and plainly; generated lemma binders mirror the item's implicitness.

The pattern-clause macro takes the same conventions (second commit): clause LHSs align against the item's columns with {x}-spelled or elided implicits, recursive calls elide likewise (elided arguments read as the ambient columns, verified by the clause lemmas' β-discharge), and the item-level using-clause rides along on every generated item — so def hd : {a : 𝕌} → stream a → a using (…) | hd t ≔ (out t) .π₁ works, ℕ-splits over implicit-parameterized items included (nth). The bespoke structural-recursion rewriter is replaced by a generic spine-rewriting traversal shared with the copattern respell.

Reviewer notes

  • Three latent engine gaps surfaced and fixed while making the discharges silent: matchElemP had no coinductive-former clauses (an out-headed rewrite candidate could never fire); observation equations are growing rewrites, now admitted as observation-shaped candidates (the el-nu-beta counterpart of the clause-shaped admission, SearchlessElaboration.md §12); and the certificate-bearing rewriter (rewriteElemS) emitted kernel-untypable steps under branch binders — it now stays at kernel-determinable positions, scrutinee-first.
  • One subtle invariant worth knowing: the Σ-entry name index (sigEntryIx) caches positively, sound because Σ only extends within a run. The macro's probe therefore pre-declares the item under a machine name — a probe declaration cached under the real name would shadow the later definition and silently disable its unfolds. Recorded in SearchlessElaboration.md.
  • Also fixes a latent weakening bug in liftPoly's sum clause (sub-polynomials' embedded codes weren't weakened under the two ⊎-elim binders; never fired because no corpus polynomial had ambient-referencing codes under a ⊎).
  • Corpus: conat.nova (czero/cinf/csucc) and stream.nova (cons/iterate/map) are copattern items, hd/tl clausal ones, all in canonical distill form; streamEq/streamBisim needed zero changes. The migrated ν goldens have byte-identical expected outputs (evaluation through stops included).
  • Gates: 163/163 tests, 94/94 corpus elaborations, distill round-trip and canonical-form verified.

Deferred (future-work list updated): observation splitting / defined observations (wants the codata item macro), the probe verification of elided call arguments for the pattern macro (copattern-only today), course-of-values ↔ futumorphism.

Three layers, spec-first throughout.

Foundation strengthens the ν-scheme to PRIMITIVE corecursion (the
apomorphism): the coalgebra fills each hole with ν 𝔽 ⊎ a — inj₁ STOPS
at an existing element (released bare by el-nu-beta, via the
stop-or-continue copair [𝕚𝕕 ‖ hᵉˡ]), inj₂ CONTINUES from a seed — and
el-nu-coind's relator goes UP TO EQUALITY (lift_𝕏(R) u v ≜
∥R ⊎ (u ≡ v ∈ ν 𝔽)∥). Both admissible: the derivation note extends by
lift-reflexivity-on-equals, the subsumption note by the copy-leg
encoding of the apomorphism. Kernel replay, elaborator and corpus
migrate; cons/csucc become one-stop machines and tlCons demotes from
the deliberately η-needing coinduction to a β-lemma.

On top of that, the COPATTERN item (docs/NovaElaboration.txt,
'Defining observations') — the clausal def's dual:

  def cons : {a : 𝕌} → a → stream a → stream a using (…)
    | out (cons x s) ≔ x, s

expands to the contractibility batch: EXISTENCE (a synthesized
primitive corecursor — the body read against the polynomial's shape,
calls continuing at the varying-columns seed, f-free elements
stopping), the OBSERVATION LEMMA fOut (λ…. ⋆ — β in every case), and
UNIQUENESS fEta (coind at the pure graph invariant; stops close in
the relator's up-to-equality leg). Tiers degrade as at the clausal
def: witness-supplied existence keeps the lemma ⋆ an ordinary
obligation, non-fragment bodies demote to declarations. The one
Σ-consultation — the ν-head's polynomial shape — is read by a
snapshot-rolled-back probe under the item's using licenses.

IMPLICIT columns follow the term syntax everywhere: LHS binders and
call arguments elided or {…}-spelled, placement resolved from the
item's own type, elided call values verified against the
insertion-resolved body by the probe (declared under a machine name —
a probe declaration cached under the real name would shadow the later
definition in the Σ-entry name index). Generated spines apply the
item through {…} overrides, a candidate variable fully and plainly;
generated lemma binders mirror the item's implicitness.

Landing the discharge exposed three latent engine gaps, now fixed:
matchElemP had no coinductive-former clauses (an out-headed rewrite
candidate could never fire); observation equations are growing
rewrites, admitted now as OBSERVATION-SHAPED candidates (the
el-nu-beta counterpart of the clause-shaped admission); and the
certificate-bearing rewriter emitted kernel-untypable steps under
branch binders — it now stays at determinable positions,
scrutinee-first, so eliminator collapse surfaces branches at depth 0.

Corpus: conat's czero/cinf/csucc and stream's cons/iterate/map are
copattern items (implicits elided, canonical distill form);
streamEq/streamBisim unchanged. New goldens: elab-copattern,
elab-copattern-implicit, elab-copattern-degrade, distill-copattern,
item-copattern; the ν goldens migrate to stop-tagged coalgebras with
byte-identical expected outputs. 162/162 tests, 94/94 corpus
elaborations, distill round-trip and canonical form verified.
The clausal-def macro adopts the copattern item's term-syntax
conventions wholesale. Clause LHSs align against the item's leading
Π-columns: an implicit column consumes a {x} pattern if one is next
and is ELIDED otherwise (its binder named by the type, invisible to
the RHS); an explicit column consumes the next pattern; constructor
patterns never sit at implicit columns. Recursive calls elide or
{…}-spell f's implicit arguments likewise — an elided argument reads
as the ambient column, verified by the clause lemma's β-discharge
(residue with a spell-it remedy, never a wrong acceptance).

The item-level using-clause now rides along on every generated item
(and the declaration tier elaborates under the item's scope), so
observation-flavored clausal defs work:

  def hd : {a : 𝕌} → stream a → a using (stream.stream.unfold)
    | hd t ≔ (out t) .π₁

Generated statements follow the spelling conventions: f's spines
carry {…} overrides at implicit positions, the uniqueness candidate
g (a variable — never inserts) applies fully and plainly with its
clause hypotheses REBUILT for that spelling (mkLemTyG replaces the
head substitution), and the generated lemmas' Π-binders mirror the
columns' implicitness.

Mechanically, the bespoke structural-recursion rewriter (rwE and its
Ty/Poly mirrors) is replaced by a generic spine-rewriting traversal
(mapSpinesE) plus alignment-aware call rewriting (rwCalls) — the
same traversal the g-respell and the copattern fallback use.

Corpus: stream.nova's hd and tl are clausal items over the implicit
column. New golden elab-clauses-implicit covers the no-split form,
ℕ-splits with elided and {a}-spelled implicits, and a plain item
alongside. 163/163 tests, 94/94 corpus elaborations, distill
round-trip and canonical form verified.
@Russoul
Russoul force-pushed the co-pattern-matching branch from 3030d51 to acff830 Compare September 1, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant