Skip to content

Deploy tc#633

Draft
strub wants to merge 221 commits intomainfrom
deploy-tc
Draft

Deploy tc#633
strub wants to merge 221 commits intomainfrom
deploy-tc

Conversation

@strub
Copy link
Copy Markdown
Member

@strub strub commented Sep 26, 2024

No description provided.

strub added 30 commits May 4, 2026 15:09
Update the inline comment in TcRing.ec to reflect investigation
findings: bugs #1 and #2 are fixed for concrete-carrier use cases
(e.g. [bigA] / [bigM] on [int]), but section-internal proofs with a
Tvar carrier and multi-parent inheritance hit a [TCIAbstract.lift]
representation limit — the [lift] field is a single integer that
can't disambiguate among multiple parent walks of different lengths
to the same target ancestor TC.

Practical effect: declaring a class as [comring <: addgroup &
(mulmonoid with ...)] would WORK for [bigA]/[bigM] on concrete
carriers but not for proofs of generic comring lemmas inside a
section. Until the witness representation supports multi-parent
walks (path-encoded lift), [comring] stays single-parent with
multiplicative axioms re-stated.
Five framework pieces, fixing what the multi-parent factory port of
[Ring.ec] was hitting at every step, plus the resulting tcalgebra port.

[1] [tcwitness.lift : int list] (was [int])

  Multi-parent inheritance reaches the same ancestor TC via different
  parent walks; collapsing the path to a step count (single int) loses
  which parent edge was taken at each step. [lift] is now a list of
  parent-edge indices through the DAG. [resolve_lifted] in [ecEnv]
  walks [tci_parents] step-by-step; [bind_uni] / [strip_lift] in the
  unifier handle path suffixes (since [bump_lift] appends).

[2] Matcher η-reduction

  [apply addmA] for [associative ((*)<:t>)] was failing with
  [IncompleteInference] because the abbrev expansion of [(*)] gives
  the goal as [Fquant(λ x y, (+) x y)] while the lemma is
  [Fop((+))] — these match only via [is_conv], which silently
  ignores etyargs on [Fapp] heads. Added [try_etared] in the
  matcher's strategy list: when matching a bare [Fop p] against
  [fun x y. h x y], η-reduce the lambda before recursing, restoring
  structural [unify_etyarg] (which binds witnesses).

[3] Op-name-aware path filtering

  [(+)<:comring>] where [comring <: addgroup & (mulmonoid with (+) =
  (*))]: two paths reach [monoid], but only the addgroup walk leaves
  [(+)] unrenamed. [with_lift] in [ecUnify] now returns paths paired
  with the cumulative ancestor→child renaming; the op-typing site
  (after [opentvi]) calls [disambiguate_op_witnesses], which filters
  candidate paths by [op_preserved] on the op name and pins the
  unique survivor. With this, the [comring] class declaration
  (multi-parent factory) accepts [mulrDl : left_distributive (*)
  (+)<:comring>] without ambiguity.

[4] Subsumption-based TC/concrete op-resolution filter

  Old policy ("always prefer concrete over TC") silently picks
  [Int.(+)] for [(+)] in [right_id zero<:t> (+)], causing
  downstream type-unification to fail. New policy: drop a TC
  candidate iff [tc_reduce] succeeds on the resolved carrier and
  yields a head op already among the concrete candidates (the TC is
  just an indirection), or if the carrier is concrete with no
  applicable instance. Otherwise both candidates survive and the
  typer's [MultipleOpMatch] retry uses surrounding context to
  disambiguate. One existing test ([monoidtc.ec]) needed three
  qualified references — the user-accepted trade-off.

[5] Tydef-witness substitution + chain pre-registration

  [sb_tydef p ↦ (params, body)] couldn't carry the witness list for
  [p]'s declared TCs, so substituting [`Abs p; offset; lift] just
  rewrote the path mechanically — leaving the offset pointing at TC
  slots [body] doesn't have. Widened to [(params, body, tcwitness
  list)], symmetric with [sb_tyvar : etyarg]. The [`Abs] case of
  [subst_tcw] now mirrors the [`Var] case via [bump_lift lift
  tcs[offset]].

  Then [add_generic_instance]:
   - pre-computes all chain instance paths up front so the tydef
     bindings can reference them as forward [TCIConcrete] witnesses
     (the class body's [`Abs anc.tc_name; offset = 0] is a
     self-reference to the to-be-registered instance);
   - registers the chain in the env BEFORE [check_tci_axioms], so
     the substituted obligation's concrete witnesses resolve
     through the freshly-bound [tci_parents];
   - auto-skips a chain entry's axioms when a previously-declared
     instance for the same (TC name, carrier, op-symbols) triple
     already proves them — matched against existing
     [TcInstance.get_all] entries, excluding our own
     just-registered chain. This lets [instance addmonoid with int]
     succeed without re-stating [addmA / addmC / add0m] when
     [instance monoid with int] is already on the books, and lets
     [instance idomain with int] discharge by [smt()] alone instead
     of leaving placeholder [int.\`1^N] obligations.

[tcalgebra port]

[examples/tcalgebra/TcRing.ec]: complete port of [theories/algebra/
Ring.ec]'s abstract hierarchy under TCs:
 - addgroup (ZModule) full;
 - comring (ComRing) full incl. exp/expr*, intmul × multiplication
   (mulrnAl etc.), squaring, lreg/regularity, fracrDE;
 - boolring, idomain, field, additive, multiplicative;
 - top-level [(^)] abbrev for any comring carrier.

[examples/tcalgebra/TcInt.ec] (new): canonical [int] instance.
Single [instance idomain with int] declaration — synthesises every
ancestor in the chain, all axioms by [smt()] / [smt(@CoreInt)].
Plus the int-specific corollaries [intmul_int], [poddX], [oddX].

[examples/tcalgebra/sandbox.ec] (new): regression for the multi-
parent factory case — Tvar carrier with a factory-renamed
mulmonoid parent, [apply addmA / addmC / add0m] on
[associative (*)<:t>] / [commutative (*)<:t>] /
[left_id one<:t> (*)<:t>].

[Regression]: stdlib + examples — no new failures (the two pre-
existing baseline failures, [DynMatrix.eca] and [qselect.ec],
unchanged).

[Known limitation, not addressed here]: [ecTheoryReplay]'s
[Inline] mode for [type t <: tc] clones still passes [] for the
witness list. Stdlib doesn't trigger it; left as a [FIXME:TC].
The [Inline] mode of clone replay (`ecTheoryReplay`) was passing [] as
the witness slot when registering [add_tydef t_path → (params, body)].
This was the FIXME left in the previous commit — symmetric with the
hole that [add_generic_instance] needed: when the cloned source is
[type t <: tc1 <: tc2 …] and the user substitutes [t <- body],
witnesses for [`Abs t_path; offset = i; lift] inside cloned axioms
need a [tcwitness] entry per declared TC of [otyd], looked up against
[body]'s view in the instance database. Without them the substitution
just renames the path and leaves the offset pointing at TC slots
[body] doesn't have.

[ecTypeClass]: new [witnesses_for_body env body tcs] — calls [infer]
per tc; on lookup failure, falls back to a [`Abs body_path] /
[`Var a] placeholder, preserving the pre-fix shape for cases that
worked by accident before. Non-failing on purpose so the change is
backward-compatible.

[ecTheoryReplay] (`Inline` arm): when [otyd.tyd_type] is
[`Abstract tcs], call [witnesses_for_body env body tcs] and feed the
result into [add_tydef]. For non-TC clones (the stdlib case)
[tcs = []] and the result is [], so no behavioural change there.

Smoke test: cloning [type t <: addgroup] with [t <- int] now resolves
inherited lemmas through int's registered addgroup instance — the
substitution lands on concrete [int] / [CoreInt.add] / [CoreInt.opp]
rather than opaque [int.\`1^N] ghosts.

[Regression]: stdlib + examples, no new failures (the two pre-existing
baseline failures unchanged).
The [TcMonoid] hierarchy ships generic notation abbrevs like

  abbrev ( * ) ['a <: mulmonoid] (x y : 'a) = (+)<:'a> x y

that, applied at a [comring] carrier, expand to the same [Fop] as
[comring]'s own [( * )] TC operator. Inside the defining file's
scope, [by_current] drops the abbrev (different prefix); but in
external clients the two compete and [select_op] reports
[MultipleOpMatch].

Add a new [drop_shadowed_notation] filter: drop OB_nott candidates
whose basename matches a TC-op (OB_oper (Some (OP_TC _)))
candidate's. Run AFTER [drop_subsumed_tc] so the abbrev survives
when the TC candidate has just been eliminated (e.g. concrete
carrier with no registered instance — [Int.( <= )] vs the new
[tcrealdomain.( <= )]).

While here, also dereference the carrier through the candidate's
per-call [subue] inside [drop_subsumed_tc]'s [carrier_is_concrete]
check — at filter time the [etyargs] field still holds the
pre-unification [Tunivar] even after [select_op] bound it to
something concrete via [unify env subue top texpected].

[Regression]: stdlib + examples — no new failures.
Mirrors [theories/algebra/Number.ec:RealDomain] as a TC class:
[tcrealdomain <: idomain] with the 5 ordering/norm ops ([|.|],
[(<=)], [(<)], [minr], [maxr]) and 10 axioms ([ler_norm_add],
[addr_gt0], [norm_eq0], [ger_leVge], [normrM], [ler_def],
[ltr_def], [real_axiom], [minrE], [maxrE]).

The two helper preds [homo2] / [mono2] and the [mono2W] /
[monoLR] / [monoRL] lemmas from the [Number.ec] preamble are
ported verbatim — they're TC-free.

Phase 2 — porting the ~280 [RealDomain] derived lemmas — left as
TODO. Initial smoke testing showed the original proof scripts
make rewrite-pattern assumptions ([!mulr1] expecting both [_ *
oner] and [oner * _] occurrences, etc.) that interact with the
TC abstraction differently from the [clone include Ring.IDomain]
form. Each batch will need targeted review.

[ger0_def], [subr_ge0], [oppr_ge0] are ported as a Phase-2
starter (3-line proofs, no rewrite-pattern surprises).
Framework changes driven by completing the algebra port through
[examples/tcalgebra/TcBigalg.ec]:

* [src/ecTcCanonical.ml] (new): per-(carrier_tcs) canonical-paths
  table via BFS-first walk through the parent DAG. Used as the
  source of truth for abstract-witness canonical encodings.

* [src/ecCoreEqTest.ml] for_tcw and [src/ecUnify.ml] unify_tcw's
  structural fallback canonicalise both sides — the convertibility
  layer for tcwitnesses, analogous to type-alias unfolding inside
  for_type.

* [src/ecEnv.ml] new tc_reduce_abstract_via_rename: walks a
  TCIAbstract witness's lift, finds a parent edge that renames the
  requested op, returns the renamed class op with the truncated
  witness. Plumbed through tc_reducible / tc_reduce so factory
  renames fire on abstract carriers (not just concrete instances).

* [src/ecMatching.ml] try_delta gains two clauses for tc_reducible
  ops, calling new doit_tc_reduce — so f_match_core unfolds
  rename-equivalent heads during apply / pf_form_match.

* [src/ecProofTerm.ml] kmatch in pf_find_occurence accepts heads
  that tc_reduce to the pattern's key path — needed because
  rewrite's keyed pre-filter would otherwise drop rename-equivalent
  positions before pf_form_match runs.

* [src/ecTyping.ml] carrier_is_concrete refined: Abstract _ →
  Abstract (_ :: _), so primitive types like int (Abstract []) are
  classified as concrete and drop_subsumed_tc can dedup TC ops
  against non-TC alternatives. New drop_tc_bounded_notation filter:
  drops a notation whose tparams have non-empty TC bounds when a
  same-basename non-TC-bounded alternative is also a candidate.

* [src/ecScope.ml] add_generic_instance gains a diamond coherence
  check: when an existing instance for the same (anc, ty) is in
  scope, the new chain entry's symbol map must agree on every op,
  else hierror with both decl sites.

* [src/ecUnify.ml] match_tc_offset returns the canonical
  (offset, lift) via the new table; bind_uni reverts to plain
  structural suffix-strip (no class-validation / canonical-bind
  workarounds needed once paths are canonical).

Algebra port:

* [examples/tcalgebra/TcRing.ec] adds Additive / Multiplicative
  morphism predicates and helper sections.

* [examples/tcalgebra/TcNumber.ec] tcrealfield as a multi-parent
  factory class, full RealDomain port, normr0_eq0, canonical
  int : tcrealdomain instance directly in this file.

* [examples/tcalgebra/TcBigop.ec] adds bigiA / bigiM range-indexed
  flavor abbrevs.

* [examples/tcalgebra/TcInt.ec] intmul_int → intmulz.

* [examples/tcalgebra/TcBigalg.ec] (new): port of Bigalg.ec —
  BigZModule, BigComRing.BAdd, BigOrder. 36/37 lemmas with
  original proofs; only divr_suml deferred (field-only).

Test: [tests/tc-ko/diamond-coherence.ec] exercises the
instance-time coherence error path.
[divr_suml] is in [BigComRing.BAdd] (the additive view of comring),
not field — [(/) (x y : t)] on a comring abbreviates [x * invr y]
and is already declared at TcRing.ec:236. Original proof replays
verbatim. Bigalg port now lemma-complete (37/37).
[subtype X = { x : T | P x }] declared inside [section. declare type
c <: ...] previously stayed monomorphic at section close — [poly] in
[Subtype]-cloned form was bound with [tyd_params = []], so even
though the cloned auto-axioms ([to_polyN], [of_polyP], etc.) and
the [to_poly]/[of_poly] ops correctly gained a [c] tparam, [poly]
itself didn't. The result was an inconsistent state where the
operations were polymorphic over [c] but returned a single shared
[poly] type for all carriers — a soundness gap flagged by the
[FIXME:SUBTYPE] marker at [add_subtype].

This patch threads the carrier and predicate of the subtype through
the type declaration so [tydecl_fv] picks up their dependency on
section-declared free types, and the existing
[generalize_tydecl] machinery adds the right tparams at close.

[ecDecl.{ml,mli}]: add [tyd_subtype : (ty * form) option] to
[tydecl]. [None] for non-subtype declarations.

[ecScope.ml] [add_subtype]: compute carrier+pred before [bind] and
populate [tyd_subtype = Some (carrier, pred)] on the placeholder.

[ecSection.ml] [tydecl_fv]: union in the carrier+pred fv when the
field is set, so [generalize_tydecl] sees the section-declared
types the subtype depends on. [generalize_tydecl] preserves the
field on the produced [Th_type].

[ecSubst.ml] [subst_tydecl]: substitute through the carrier and
predicate when present.

Other [tydecl] builders ([ecHiInductive], [ecTheoryReplay], the
class-stub builder in [ecScope]) are updated to set the new field
to [None]. After this, [subtype poly = { p : int -> c | ispoly p }]
inside a [section. declare type c <: comring.] generalises to
[type 'c poly] at close, and the cloned ops/axioms type-check
consistently against [poly<:'c>].
Three fixes in [add_generic_instance] that together let
[instance C with ['a <: D] (T 'a)] (parametric carrier) coexist with
intermediate registrations on the same carrier.

1. [symbols_of_tc] no longer freshens tparams. The ops it returns are
   later resolved against a unienv seeded with the instance's original
   tparams, so freshening produced [c_fresh] in the expected type but
   [c_orig] in the resolved op — printing identically as ['a] but
   rejected by [EqTest.for_type]. Build the substitution binding each
   tparam to itself with abstract TC witnesses on the original ident.

2. Proof obligations carry the instance's tparams. [check_tci_axioms]
   now takes [?tparams], threaded from [add_generic_instance] as
   [fst ty]; the synthesised [ax] for each axiom inherits them
   instead of [[]], so the obligation can be discharged in a context
   where the carrier tparam is in scope.

3. Self-witness etyargs match the registered instance. For a chain
   entry registered with the user's tparams, the [TCIConcrete]
   self-witness must re-apply those tparams as etyargs (each carrying
   abstract TC witnesses) — not [], or [tc_reduce] hits a
   [tci_params]/[etyargs] length mismatch when the witness is later
   consulted.

4. Chain-entry reuse. [add_generic_instance] previously allocated a
   fresh path for every ancestor in the chain, which would silently
   register a duplicate instance when an intermediate ancestor was
   already in scope (e.g. registering [comring] on a carrier already
   bound at [addgroup] would create a second addgroup instance,
   diverging witnesses and violating one-canonical-instance-per-
   (class, carrier)). [find_existing_chain_entry] now looks up an
   existing matching instance ((tc_name, carrier, op-symbols) all
   agree); if found, the chain entry's path is reused and no
   re-registration happens. [chain_self_paths] is filtered to
   freshly-allocated paths so [already_discharged] still recognises
   the reused instance as the discharger of the corresponding axioms.

Validates on the existing tcalgebra suite (TcMonoid/TcRing/TcInt/
TcBigop/TcBigalg/TcNumber/TcPoly) and unblocks parametric polynomial
instances over [comring] coefficients.
Port [theories/algebra/Poly.ec:PolyComRing] (lines 1-516) to the TC
framework as a parametric instance over a [comring] coefficient.

Key structural difference from the original: instead of cloning
[Ring.ZModule] then [Ring.ComRing] in two phases (and re-cloning
[Bigop]/[Bigalg] inside each), we declare a single [comring with
['c <: comring] ('c poly)] instance. Once registered, every TcBigA
/ TcBigZModule / TcBigComRing lemma polymorphic over the relevant
class applies at carrier ['c poly] with no further cloning. The
chain-entry reuse fix in [ecScope.ml] makes the explicit Phase-3
[addgroup] declaration coexist with the Phase-5 [comring] one (no
duplicate registration).

[unit] / [invr] follow the [Ring.ec:ComRingDflInv] pattern:
[poly_unit p := exists q, q * p = poly_one] and
[poly_invr p := choiceb (fun q => q * p = poly_one) p]. The three
obligations [mulVr] / [unitP] / [unitout] discharge from
[choicebP] / [choiceb_dfl] alone — no ring axioms needed. The
structural "constant with invertible coefficient" characterisation
only holds when [c : idomain] (deg multiplicativity requires no
zero divisors); that bridge will live in a future idomain phase
as a separate [unitE]/[polyVE] equivalence.

Phases covered:
- Phase 1: [poly] subtype + [deg] / [_.[_]] / [lc].
- Phase 2: [polyC] / [polyXn] / [polyD] / [polyN] / [polyM] / [polyZ]
  constructors, [ispoly] closure lemmas, coefficient formulas
  ([coeffE], [polyME], [polyMXE], [polyZE]), scaling ([scale*p]).
- Phase 3: [addgroup with ['c <: comring] ('c poly)].
- Phase 4: [polyM_mulrA] / [mulrC] / [mul1r] / [mulrDl] / [oner_neq0]
  via the standard bigop expansion of [polyM]'s convolution.
- Phase 5: [comring with ['c <: comring] ('c poly)] with choiceb
  defaults for [unit]/[invr].

Higher-level theory (mul_lc, deg arithmetic, evaluation, polyXn
exponents — Poly.ec lines 522-847) and the idomain extension
(Poly.ec lines 855+) are deferred to follow-up commits.
Port of [theories/algebra/Poly.ec] lines 296-415 into a fresh
section over [c <: comring]: [degC] / [degC_le] / [lcC] / [lc0] /
[lc1] / [deg0] / [deg1] / [deg_eq0] / [degX] / [nz_polyX] / [lcX] /
[deg_ge1] / [deg_gt0] / [deg_eq1] / [lc_eq0]; [degN] / [lcN] /
[degD] / [degB] / [degDl] / [lcDl] / [degDr] / [lcDr].

Most proofs port verbatim. Three places need adaptation:
- [lc_eq0] [argmin_min] precondition discharged manually (the
  [smt()] in the original was opaque to the TC-elaborated form).
- [degDr] / [lcDr] use [polyD_addrC<:c>] explicitly, since
  inferring the [addgroup] witness for [c poly] from a bare
  [addrC] in this section did not resolve.
- [lcDr]'s premise typo in the original ([deg q < deg p] in both
  the goal and conclusion-by-symmetry) is corrected to
  [deg p < deg q]; the conclusion becomes [lc q] (mirroring [degDr]).
[EcTypeClass.infer] previously consulted only the registered
instance database, so its recursion in [check_tcinstance] (when
matching a parametric instance, line 144) could not discharge a
tparam constraint whose carrier was a section-declared abstract
type. Concretely:

  section.
  declare type c <: comring.
  lemma _ (a b : c) : a + b = b + a.
  proof. apply (addrC<:c> a b). qed.

failed with "type c does not satisfy typeclass constraint
addgroup", because [check_constraints]' fallback to
[abs_satisfies] is only a yes/no check and is not propagated
through the recursive [infer] call inside parametric-instance
matching.

[EcUnify] handled this case via [strat_abs_via_decl] (Mode #6)
in its strategy dispatch, but [EcTypeClass.infer] is called from
several other paths (proof-term TVI elaboration, theory-replay,
reduction) that don't go through the unifier dispatch.

This adds [infer_via_abs_decl] alongside [check_tcinstance] and
makes [infer] fall back to it after the instance-database
search. The fallback walks [tcs] (the carrier's declared
class bounds) looking for one whose ancestor DAG reaches [tc],
and produces a [TCIAbstract { support = `Abs p; offset; lift }]
witness — same shape as Mode #6 in [EcUnify].

Validates: the suite (TcMonoid/TcRing/TcInt/TcBigop/TcBigalg/
TcNumber/TcPoly) passes unchanged; new test
[apply (addrC<:c> a b)] inside [section ; declare type c <:
comring] now succeeds.

Note: this fix completes constraint resolution for non-
parametric Path B uses (e.g. [addrC<:c>]). The parametric case
[addrC<:c poly>] now passes constraint resolution but the
proof-term and goal witness encodings can still mismatch (the
unifier produces a [TCIUni] with [lift] that doesn't always
collapse to the goal's resolved form). That's a separate
issue, untouched by this commit.
Chain-entry reuse (commit fdb522c) used [EcReduction.EqTest.for_type]
to compare carrier types, which requires ident-equality on tparams.
But existing parametric-carrier instances have their tparams freshly
generated at registration time with new [EcIdent] uids, so a later
[instance C with ['c <: D] (T 'c)] declaration whose own [c] has a
different uid would fail to match the existing instance — and the
chain synth would create a duplicate registration.

Concrete failure mode: declaring [instance addgroup with ['c <: comring]
('c poly)] then [instance comring with ['c <: comring] ('c poly)]
produced two distinct [addgroup] instances on [poly], because the two
['c]s had different idents. [infer_all] then returned 2 matches,
[strat_carrier_is_ambiguous] flagged the resolution as ambiguous, and
the [TcCtt] constraint stayed deferred.

Fix: compare carriers via [EcTypeClass.ty_match] with the existing
instance's tparams as pattern variables. This is alpha-equivalent
matching — ['c poly] (existing) matches ['c poly] (new) regardless of
the tparam idents. Exports [ty_match] and [NoMatch] from [ecTypeClass]
for use here.

Validates: tcalgebra suite (TcMonoid/TcRing/TcInt/TcBigop/TcBigalg/
TcNumber/TcPoly) passes; reproducer's [INFER_ALL: tc=addgroup -> 2
match] is now gone.

Note: the parametric Path B (e.g. [apply (addrC<:c poly> p q)]) still
fails because the unifier's [TcCtt] resolution doesn't reach the
proof-term print — the [TCIUni] placeholder is committed in the
resolution map but the [tw_assubst] used by [concretize_env] doesn't
seem to follow it. That's a separate, deeper issue.
Port [theories/algebra/Poly.ec] lines 522-589 (multiplicative
degree on poly): [mul_lc] / [degM_le] / [degM_proper] /
[lcM_proper] / [degZ_le] / [degZ_lreg] / [lcZ_lreg].

Most proofs port verbatim. Adaptations:
- [mul_lc]'s [poly0] base cases use the structural [polyM_mul0r]
  / [polyM_mulrC] / [poly0E] chain instead of the original's
  [!(mul0r, poly0E)] (which relied on the polyComRing clone
  having [poly] under the comring [( * )] so [mul0r] applied at
  the poly carrier — Path B in the TC port, currently routed
  through structural lemmas).
- [degM_proper] proof restructured to derive the lower bound
  via forward [have] reasoning instead of the original's
  [eqz_leq + split + bracket-or].
Port [Poly.ec] lines 600-669: [polyCX] / [degXn_le] /
[degXn_proper] / [lcXn_proper] / [deg_polyXn] / [lc_polyXn] /
[deg_polyXnDC] / [lc_polyXnDC] / [polyXnE] / [lreg_lc].

These lemmas use [exp p i] at carrier [c poly] (via the comring
instance), exercising the parametric-Path-B path in the
elaborator. With the [infer-via-abs-decl] (commit 0dd7d21) and
chain-reuse alpha-match (commit c182895) fixes, lemma
statements typecheck; proof bodies rely on structural lemmas
([polyM_mulrC] in [polyXnE] instead of original's
[PolyComRing.mulrC]) where Path-B-via-class-lemma would fail.

[degXn_le]'s [(IntID.addrC 1)] in the original uses the integer
[addrC] from the cloned [IntID]; the TC port uses [addzC 1] (the
TcInt analogue) directly.
Port [Poly.ec] lines 681-720 (the [BigPoly] block contents):
[polysumE] / [polyE] / [polywE] / [deg_sum].

The original had [theory BigPoly. clone include BigComRing with
theory CR <- PolyComRing. ... end BigPoly] re-exporting BigA/BigM
on poly under names PCA/PCM. The TC port skips that scaffolding —
[bigA<:c poly>] resolves directly through the registered comring
instance. Replaces [PCA.big] -> [bigA] / [bigi] throughout.

Single workaround: [polywE]'s tail [rewrite addr0] hits the
parametric Path-B (addr0 = [forall 't <: addmonoid (x : 't), x +
idm = x] applied at carrier [c poly]); replaced with explicit
[apply/poly_eqP] + coefficient-level [poly0E]/[addr0] (the latter
on coefficient [c], non-parametric Path B works).
Port [Poly.ec] lines 726-779: [peval] / [root] / [prepolyL] /
[isprepolyL] / [polyL] / [polyLE] / [degL_le] / [degL] /
[inj_polyL] / [surj_polyL]. Closes the Phase 6 section.

Adaptations:
- [prepoly] is the Phase-1 section-local subtype's underlying
  fun-type; outside that section it's parametric in [c]. The
  Phase 6 section uses the synonym [int -> c] explicitly for
  [prepolyL]'s codomain rather than spelling out [c prepoly].
- [polyL]'s result type spelled as [c poly] (parametric form)
  instead of the section-local [poly] (Phase 1's bare alias).

Skipped from this chunk: [finite_for_poly_ledeg] /
[dpoly]-distribution lemmas (lines 782-844 of original) — these
need [is_finite_for] / distribution theory that hasn't been
TC-ported, and they're not on the critical path for the rest
of the algebra port. Will revisit if downstream needs them.
Port [Poly.ec] lines 850-973 (the [Poly] abstract theory built
over [c : idomain]):

- [degM]: deg(p·q) = deg p + deg q − 1 for non-zero p, q
  (multiplicativity of deg, requires no zero divisors).
- [lcM]: lc(p·q) = lc p · lc q.
- [polyM_mulf_eq0]: polynomials over an idomain inherit the
  no-zero-divisor property — the [mulf_eq0] axiom one would
  feed to a hypothetical [instance idomain with ('c poly)]
  registration.
- [unitE]: bridges the choiceb-based [poly_unit] (committed at
  Phase 5 over [c : comring]) to the structural form
  [deg p = 1 /\ unit p.[0]] available when [c : idomain]. Both
  directions proved: (=>) uses [degM] to force both factors of
  any inverse pair to have degree 1; (<=) builds the explicit
  inverse [polyC (invr a)].
- [polyVE]: structural value of [poly_invr (polyC a)] when
  [unit a] — equals [polyC (invr a)] via uniqueness of the left
  inverse modulo [polyC a]'s left-regularity (which holds iff
  [a] is non-zero, which holds when [a] is a unit in an idomain).

[unitE]/[polyVE] do NOT redefine the registered [poly_unit] /
[poly_invr] (those were committed at Phase 5 and are fixed for
all 'c poly carriers). They expose the structural shape under
the additional [c : idomain] assumption — downstream proofs
rewrite with these to argue structurally about invertibility
without unfolding [choiceb].

Skipped: [degV] (poly's deg of polyV equals deg p) and
[finite_for_poly_ledeg] / [dpoly] distribution lemmas. The
former depends on [polyV]'s structural form which doesn't
match our [poly_invr] (poly_invr is choiceb-based, not the
[if deg p = 1 then polyC (IDCoeff.invr p.[0]) else p] form);
the latter need finite/distribution scaffolding not TC-ported.
Smoke test exercising the parametric polynomial library at carrier
[int] (which is registered as [idomain] via TcInt). Confirms each
phase of TcPoly resolves correctly through the chain of TC instances
([int : idomain : comring : addgroup : addmonoid : monoid] plus the
[int poly : comring]-via-Phase-5 chain) at a concrete carrier:

- Phase 1-2: [polyCE] / [polyXE] coefficient formulas.
- Phase 4: [polyM_mulrA] / [polyM_mulrC] structural lemmas.
- Phase 6a: [degC] / [deg0] / [deg1] / [degX] degree arithmetic.
- Phase 6c: [deg_polyXn] / [lc_polyXn] X^i theory.
- Phase 6e: [polyLE] polyL constructor.
- Phase 7: [degM] / [lcM] / [polyM_mulf_eq0] idomain extension.
- Concrete: [polyM (X+1) (X−1) .[0] = -1] computes through the
  polyM convolution.

Caveat: tests use [polyM]/[polyD]-spelled forms instead of the [+]/
[*] abbrevs — the abbrev resolution at carrier [int poly] hits the
parametric-Path-B witness encoding gap (recorded in memory). The
underlying lemmas all apply correctly via structural names. Once
that gap is closed, the abbrev forms will work too.
Closes the parametric-Path-B witness encoding gap that left class-
lemma applications at parametric carriers ([apply (addrC<:c poly>)]
inside [section ; declare type c <: comring]) unusable.

**The bug.** [opentvi] creates a [TCIUni] for each opened tparam's TC
bound and posts a [`TcCtt] problem, but doesn't itself drain the
queue. The TCIUni stays parked in [tcenv.problems] until something
else triggers [unify_core]. Meanwhile a proof-term carrying
[(+)<:c poly[TCIUni #a [0;0]]>] arrives at the matcher's [try_delta],
which tries [Op.tc_reducible env (+) tys] — and [tc_core_reduce]
raises [NotReducible] on TCIUni witnesses, since it can only walk
[TCIConcrete]/[TCIAbstract] forms. So [try_delta] falls through to
[default]'s [is_conv], which doesn't TC-reduce either; matching
fails, [pf_form_match] raises MatchFailure, [t_apply] reports "the
given proof-term proves: ... it does not apply to the goal".

**The fix.** Two changes:

1. [EcUnify.UniEnv.flush_tc_problems env ue] (new): runs
   [Unify.unify_core] on a trivial-true [`TyUni] problem, which
   re-pushes every parked [`TcCtt] in [tcenv.problems] and lets the
   strategy dispatcher resolve them. After the call, the resolution
   map contains a witness for every [TCIUni] that any strategy
   (Modes #1..#6) could pin.

2. In [EcMatching.f_match_core]'s [try_delta]: before destructuring
   the heads, call [flush_tc_problems env ue] and re-normalise both
   sides via [norm]. The substitution machinery in [tcw_subst]
   (ecCoreSubst.ml:209) dereferences resolved TCIUnis through
   [fs_tw_uni], so after [norm] both forms carry the concrete
   witness; [tc_reducible] then succeeds and [doit_tc_reduce]
   produces the renamed structural op (e.g. [polyD] for poly's
   addgroup-via-class-(+)), which conv'ing against the goal
   succeeds.

Combined with the earlier framework fixes [0dd7d21] (infer-via-
abs-decl) and [c182895] (alpha-equivalent chain reuse), this lets
[apply (addrC<:c poly> p q)] and [apply (mulrA<:c poly> p q r)]
inside a [c <: comring] section discharge directly, without
requiring users to fall back to the underlying [polyD_addrC<:c>] /
[polyM_mulrA] structural lemmas. The TcPoly port's structural-form
workaround in Phase 6 / Phase 7 / smoke test stays as-is for the
already-written code, but new code can use the natural class form.

Validates: tcalgebra suite (TcMonoid/TcRing/TcInt/TcBigop/TcBigalg/
TcNumber/TcPoly/TcPolySmokeTest) all pass; the parametric Path B
reproducer at /tmp/repro_pathb.ec now closes [test_path_b] via
[apply (addrC<:c poly>)] without admit.
…c carrier

Add [test_addrC_at_int_poly] / [test_addrA_at_int_poly] /
[test_mulrC_at_int_poly] using [apply (addrC<:int poly>)] etc.
With the [try_delta] flush fix (commit 5e04bf6), these now work
directly — the TCIUni parked at [opentvi] gets resolved before
[tc_reducible] is checked.

[*] at carrier [int poly] still has a parser-level ambiguity
(both the section-local abbrev [Top.TcPoly.*] and the comring class
[Top.TcMonoid.*] match). Test uses [polyM] explicitly there. That
ambiguity is orthogonal to TC inference — it'd want a
disambiguation policy at the parser level.
[gen_select_op]'s [drop_subsumed_tc] classifies candidates by the
[op_kind] of their declared path — but after typing, abbrev
candidates are inlined and their bodies' heads are what actually
appear in the elaborated term. So an abbrev whose body is itself a
TC-op invocation escapes the filter even when, post-inline, it's a
TC-op-headed term that reduces to the same head as another candidate.

Concrete trigger: writing [p * q] with [p, q : int poly] yielded
[MultipleOpMatch] because three [*] candidates survived:
- [Top.TcPoly.*]   (abbrev → body head [polyM], non-TC)
- [Top.TcMonoid.*] (abbrev → body head [(+)], TC op of monoid)
- [Top.TcRing.*]   (TC class op → reduces to [polyM])

[drop_subsumed_tc] correctly drops [Top.TcRing.*] (TC op subsumed by
[polyM] in concrete_paths), but it leaves [Top.TcMonoid.*] alive
because its declared kind is [OB_nott] (abbrev), not [OB_oper(OP_TC)].
The body's head [(+)] is a TC op, but the filter never inspects it.

Compare to [+] at the same carrier: only two candidates, the second
being the [Top.TcMonoid.+] class op directly. [drop_subsumed_tc] sees
its declared kind as [OB_oper(OP_TC)], runs [tc_reduce], drops it.
The asymmetry is just that TcMonoid ships an [abbrev (*) ['a <:
mulmonoid] = (+)<:'a>] but no analogous abbrev for [+].

Fix: add [drop_subsumed_by_post_inline_head], a sibling of
[drop_subsumed_tc] that operates on the post-inline body head rather
than the declared op_kind. For each candidate with a body, force the
[sbody] lazy and extract the body's head op. Collect the non-TC heads
as [concrete_heads]. Then drop any candidate whose post-inline head
is a TC op that [tc_reduce]s to a head already in [concrete_heads].
Run after [drop_tc_bounded_notation] so the existing pre-inline
filters dedupe what they can first.

After the fix, [p * q] at carrier [int poly]:
- [Top.TcPoly.*]   body head [polyM]      — kept
- [Top.TcMonoid.*] body head [(+)] (TC)   — tc_reduce → [polyM] in
                                            concrete_heads → dropped
- [Top.TcRing.*]   already dropped by [drop_subsumed_tc]
=> single candidate, parses cleanly. [apply (mulrC<:int poly>)] now
discharges directly. Smoke test ([test_mulrC_at_int_poly],
[test_mulrA_at_int_poly]) covers both.

Validates: tcalgebra suite (TcMonoid/TcRing/TcInt/TcBigop/TcBigalg/
TcNumber/TcPoly/TcPolySmokeTest) all still pass; [+]/[-]/[**] paths
unaffected (their candidate sets don't trigger the new pass).
With the post-inline subsumption filter (commit c47657a), [p * q]
at carrier [int poly] resolves uniquely without needing the [polyM]
disambiguation. Replace the [polyM]-based test with the natural
class-form, and add [test_mulrA_at_int_poly] to cover associativity.

The mulmonoid [*] abbrev that previously caused the [MultipleOpMatch]
ambiguity is now dropped post-inline, because its body head [(+)]
[tc_reduce]s to [polyM] which is already represented (concretely) by
the section-local [Top.TcPoly.*] abbrev's body.
[pf_find_occurence]'s key extraction takes the pattern's literal head
op-path. For a pattern like [addrC<:int poly> p q] proving
[(+)<:int poly[wit]> p q = (+)<:int poly[wit]> q p], the key would be
[(+)] — but the goal, after abbrev expansion at [int poly], usually
has [polyD] (the realisation). Different keys → keyed filtering
rejects every position → "nothing to rewrite" even though the goal
*does* contain a position equivalent to the pattern under TC
reduction.

Fix: when the pattern's head [Fop p tys] is a TC op that
[tc_reduce]s at the supplied [tys] to a concrete op, key on the
*reduced* head instead of the literal one. Matches how the
keycheck-side already handles the symmetric case (goal's head being
a TC op that reduces to the pattern's key).

Apply already worked for class-form lemmas at parametric carriers
after the [try_delta] flush fix (commit 5e04bf6). This commit
extends parity to [rewrite].

[examples/tcalgebra/TcPoly.ec] simplifications enabled:
- [degDr] / [lcDr]: [polyD_addrC<:c>] → [addrC<:c poly>] (rewrite at
  parametric carrier).
- [polyXnE]: [polyM_mulrC] → [mulrC<:c poly>].

Other structural-name uses ([polyM_mul0r] in [mul_lc] base cases,
the instance-obligation proofs) remain — those reach into goals
where the rewrite-engine's pattern would need to match e.g.
[zero<:c poly>] against [polyC zero<:c>] (poly0's abbrev expansion),
and the head-bridge alone doesn't span that distance. Path-B for
*apply* is solid; *rewrite* through deeper structural re-encoding
is more delicate.
Two printer fixes that make TC-elaborated goals readable:

1. [pp_tyvarannot] uses [pp_tyvar_ctt] instead of dropping the TC
   constraint list. So [print poly0] now shows
   [abbrev poly0 ['c <: comring] : 'c poly = polyC idm<:'c[...]>.]
   instead of [abbrev poly0 ['c] : ...] (which hid the comring
   bound). The sibling [pp_tyvar_ctt] already knew how to format
   ['c <: comring]; [pp_tyvarannot] just wasn't using it.

2. [try_pp_notations] chases the abbrev's tparam univars through
   the matched unienv, using [ov.args] (the [etyarg list] from
   [opentvi], which carries both the type univar AND any TC-witness
   univar) plus [f_op_tc] (etyarg-aware) instead of bare tvars +
   [f_op] (which built empty witnesses). Effect: a goal like
   [p = poly0] inside [section ; declare type c <: comring] now
   prints as [p = poly0<:c[c.`1]>] with the carrier and TC witness
   visible, instead of [p = poly0<:#a>] where [#a] was a stale
   univar reference left behind because [ov.subst] (the fresh-univars
   map from [opentvi]) was applied without then chasing through the
   matched unienv's substitution.

Without these, the user typing [have h: p = poly0] inside a comring
section would see a goal carrying a fresh-univar [#a] that looked
like an unresolved type, even though the matcher had already pinned
it to the section's [c]. The univar was a print-side ghost — the
underlying form was fully ground.
[pf_find_occurence]'s [kmatch] keyed filter previously only accepted
goal positions whose head matched the pattern's head literally (or
[tc_reduce]d to it). For a TC-op pattern like [mul0r<:int poly>]
with [( * )<:int poly[wit]>] applied, the goal's [polyM] head
(which is the registered structural realisation) was rejected by
the keyed filter, even though it's the carrier-specific structural
form of the same op.

This commit adds the reverse direction: when the pattern's key is
a TC class op [tcop] and the goal's head [op'] is a registered
realisation of [tcop] in some instance's symbol map, accept the
position. The actual unification still has to bridge the heads
downstream — typically by [tc_reduce] firing once the pattern's
witness is resolved (Path-B fix paths handle this).

Adds [EcEnv.Op.tc_op_realised_by env tcop concrete] as the
syntactic check, used by [ecProofTerm]'s [kmatch].

Effect: rewrite patterns with class-form heads (e.g. [( * )<:c>])
no longer get filtered out at goal positions whose head is the
structural realisation. Doesn't on its own fix the bare-rewrite
case ([rewrite mul0r] without TVI still fails because the
unification can't pin the pattern's univar carrier; see followup
notes), but it's a strict improvement on the keyed-filter side
and unblocks several positions that were silently rejected.
Mirror [Poly.ec:degXn_le]'s [(IntID.addrC 1)] disambiguation step
with the TC equivalent [(addrC<:int> 1)]. Both proofs invoke a
*qualified* commutativity at the int level to pin the rewrite
to the integer ring (vs the polynomial ring whose [addrC] is also
in scope at this point). Poly.ec qualifies via the cloned-module
path [IntID.addrC]; the TC port qualifies via TVI [addrC<:int>] —
direct semantic translation.

The earlier port used [addzC] (an int-specific lemma in TcInt's
module) as the disambiguator; replacing it with [addrC<:int>]
(a) brings the proof closer to [Poly.ec]'s structure, and
(b) demonstrates that TVI-based qualification is the principled
way to disambiguate class lemmas in TC — same role as
[Module.lemma] in clone-based theories.
When the pattern head is a TC class op (e.g. [*]<:comring>) with the
carrier still a univar and the goal head is a registered concrete
realisation of that class op (e.g. CoreInt.mul), force unification of
the carriers so the TC resolver can pin down the witness. Without this,
[rewrite mul0r] on a goal at a parametric or concrete carrier could not
find a position whose head was the realisation rather than the class op.
Add a [reducible] modifier on [instance] declarations. Reducible-marked
instances participate in strict TC reduction: when [/=] (cbv) hits a TC
class op whose witness resolves through such an instance, it folds the
op to its concrete realisation. Non-reducible instances (e.g. polynomial
instances over an abstract carrier) stay opaque so users keep reasoning
through algebraic lemmas.

Matcher and [is_conv] are unaffected — both look through any concrete
witness regardless of the flag, so unification and convertibility stay
maximal. Strict mode is opt-in via [Op.tc_reducible ~strict:true] /
[Op.tc_reduce ~strict:true].

Plumbing:
- [tcinstance.tci_reducible] field; threaded through manual and
  synthesised-along-the-class-chain instance declarations.
- [REDUCIBLE] keyword in the [instance] grammar.
- [Op.tc_core_reduce] gains [?strict]; raises [NotReducible] when the
  resolving instance is not marked reducible. Abstract-rename folding
  also gated by strict.
- [EcCallbyValue.reduce_user_delta] op-step calls [Op.tc_reduce
  ~strict:true] and recurses on the result.
- [EcReduction.fold_reducible_tc] normalises a form by folding every
  reducible-marked TC op recursively. Called by [t_rewrite] after
  position substitution so the post-rewrite goal carries underlying
  core ops rather than verbose [op<:T[Conc(...)]>] heads.

Standard-library usage:
- TcInt: [instance idomain with int reducible].
- TcBigop: drop legacy [import Ring.IntID] (was a workaround for the
  missing fold; the int instance now folds at [/=]).
- TcPoly: drop [(addrC<:int> 1)] TVI workaround in [degXn_le].
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.

3 participants