Skip to content

Integer-indexed types - #1065

Draft
strub wants to merge 60 commits into
mainfrom
indexed-types
Draft

Integer-indexed types#1065
strub wants to merge 60 commits into
mainfrom
indexed-types

Conversation

@strub

@strub strub commented Jul 2, 2026

Copy link
Copy Markdown
Member

Adds integer-indexed types to EasyCrypt: type constructors parameterised by both type variables and natural-number indices, with a small index language and decidable index equality. This lets you express size-carrying types like 'a vec<:n> and track index arithmetic through operators, lemmas, and cloning.

strub added 30 commits January 30, 2026 16:02
Mechanical migration of every call site to the new
ty_params = {idxvars; tyvars} record and targs = {indices; types}
record introduced in b15e335. Repairs the broken gen_op/mk_op
bodies in ecDecl and the syntax error in ecInductive (c413f37 WIP).

No semantic change: indices are uniformly empty, tyvars carry the
existing behaviour. Phase-0 design choices and the rest of the
roadmap are documented in memory.md.
tindex equality and hashing now go through a canonical
sum-of-monomials normalisation, so n+1 and 1+n (and (n+m)^2 and
n^2+2nm+m^2) are recognised as equal. Coefficients are EcBigInt with
the natural-number invariant; canonical_const refuses negative
TIConst.

ecUnify now compares indices (with the previous polarity bug fixed)
and ecReduction.for_targs no longer skips them.

Memory.md updated with the Phase-1 deliverables and what was
deferred (no TIUnivar / UF participation yet — gated on Phase 3
needs).
tindex_subst is no longer a no-op. It consults fs_loc / sb_flocal
(indices share the formula-locals namespace), reinterprets the bound
formula as a polynomial via the new tindex_of_form recogniser, and
panics if the binding is non-polynomial.

targs_fv (and so ty_fv on Tconstr) now folds over indices, so the
per-formula short-circuits in Fsubst correctly fire for types with
TIVar occurrences.

is_ty_subst_id now also checks fs_loc emptiness — a formula
substitution that touches an int-typed local can affect any type
whose Tconstr carries a TIVar of that local. Cost is a wider
substitution walk; correctness comes first here.

memory.md updated with deliverables, design choices (no eager
re-canonicalisation, fs_eloc not consulted), and the remaining risk
(audit f_bind_local callers for the polynomial invariant).
Tydecls, operators, predicates and axioms can now declare integer
index parameters; type-constructor applications can supply index
arguments.

  type [n m] ('a, 'b) vec.
  op  f [n 'a] (xs : 'a vec<:n>) : 'a vec<:n+1>.
  pred p [n 'a] : 'a vec<:n>.
  axiom A [n 'a] : true.

Index binders use plain (no apostrophe) identifiers in `[...]`. Index
applications are framed by `<:...>` rather than `[...]` to avoid a
shift/reduce conflict with codepos brackets in module-update
syntax. Index expressions inside the framing are restricted to the
polynomial fragment (`+`, `*`, non-negative literals, identifiers).

Datatype/record indexed types and cloning of indexed declarations
are refused with clean errors — useful index-instantiation at op
call sites still requires the deferred TIUnivar / polynomial-with-
univars unification work, also flagged in memory.md.

A regression test lives at tests/indexed-types.ec.
Adds the TIUnivar machinery Phase 3 deferred. Indexed ops can now be
called: each idxvar of the op being applied is freshened to a TIUnivar
and unified against the call site via polynomial-normal-form equality.

  op concat [n m 'a] (xs : 'a vec<:n>) (ys : 'a vec<:m>) : 'a vec<:n+m>.
  op cons [n 'a] (x : 'a) (xs : 'a vec<:n>) : 'a vec<:n+1>.

  op test [n m 'a] (x : 'a) (ys : 'a vec<:n>) (zs : 'a vec<:m>)
    : 'a vec<:n+(1+m)>          (* canonically equal to (n+1)+m *)
    = concat (cons x ys) zs.

The typecheck of `test` works because `cons`'s `?u` unifies with `n`,
`concat`'s `?u_n` unifies with `(n+1)`, `?u_m` with `m`, and the
inferred return type `(n+1)+m` is canonically equal to the annotated
`n+(1+m)`.

MVP scope: handles "naked TIUnivar = arbitrary polynomial" (with
occurs check) and canonical equality after resolution. Refuses
genuine polynomial unification (e.g. `?u + 1 = n` would need
subtraction-inversion) with a clean IndexMismatch error.

Also fixes a lurking bug in `ty_subst` where the `Tconstr` case fell
through to `ty_map`, which preserves indices verbatim — silently
dropping op-application index substitution.
  clone T as T2 with type [k] 'a vec = body

The optional [k] mirrors the tydecl binder syntax. body may
reference both k and the type binders; when vec<:e> appears in T,
the substitution binds k to e in body.

ty_override_def is widened to (idxvars, tyvars, body); subst gains
sb_idxvar; subst_ty's Tconstr-with-tydef branch binds both source
binder lists to the call-site indices/types before substituting
through body. The previous CE_IndexedNotYetSupported is replaced
with CE_IdxArgMism so the user gets a precise arity message.

Three clone cases land in the regression: drop the index (= int),
propagate (= 'a coll<:k>), and a polynomial of the binder
(= 'a coll<:k+1>). 77 declarations now compile.

Two gaps are flagged in memory.md but kept out of scope: reaching
into a cloned theory's ops whose signature got touched (looks
orthogonal to indexed types), and explicit index-instantiation
syntax at op call sites.
The two assert (List.is_empty *.indices) panics in ecSmt.ml become
raise CanNotTranslate, and check / execute_task catch the exception
to skip the goal cleanly. The user now sees a warning
("SMT: skipped goal containing constructs not yet exported to Why3
(e.g. indexed types)") followed by "cannot prove goal", instead of
an anomaly crash.

Translating indexed types to Why3 stays out of scope per the
original Phase-0 punt.
Errors and transcripts now print 'a vec<:n> instead of 'a vec;
pp_tindex handles variables, univars (?#N), constants, and the
polynomial *- and +-forms with standard precedence.

Two vestigial Phase-0 asserts become clean failures:
- ecReduction: indexed-op heads in a user rewrite rule raise
  NotReducible instead of crashing (they just don't match).
- ecMatching: Fop vs Fop in pattern matching uses tindex_equal plus
  a length check, failing cleanly rather than asserting.

The two asserts in ecInductive's positivity check are intentional
"should never happen" guards — Phase-3 Slice-A already refuses
indexed binders on datatype/record, so they aren't reachable.

Closes the roadmap in memory.md. Remaining gaps are documented (op
call-site index instantiation syntax, SMT translation of indexed
types, indexed datatypes) but out of scope.
E — extend index-binder support to abbreviations and notations.
Both rules now accept mixed_tyvars_decl (the bracket binder list
that splits into idxvars and tyvars). pabbrev / pnotation gain
*_idx fields; ecHiNotations threads them via the new
~idxparams parameter on transtyvars.

  abbrev my_alias [n 'a] : 'a vec<:n+1> = ... .
  notation %"..."% [n 'a] (...) = ... .

D — investigation of the Phase-4 "T2.make_vec unknown" report
showed it was a misuse of the alias `=` operator instead of the
inline `<-` operator (alias creates a new name; inline propagates
the body). Both modes work correctly. While tracing, fresh_tparams
was discovered to freshen tyvars but not idxvars — fix this so
op_tparams alpha-renaming includes both.
  op count [n 'a] : int.
  op test : int = count[:5]<:int>.

New lexer token LBRACKETCOLON (matches `[:` glued). Grammar adds
`f[:idx]`, `f[:idx]<:ty>`, alongside existing `f<:ty>`.

Parsetree TVIunamed and ecUnify tvar_inst.TVIunamed are widened to
(indices, types). Producers updated mechanically (PFrecord, PTHO_*,
inductive constructors, scope's tycinstance loop, printer's
op_symb resolution, transtvi). EcUnify.openidx now consumes user-
supplied indices when given, falling back to fresh TIUnivars
otherwise. select_op's filter validates either side independently
when non-empty.

Useful when an op's idxvar is unreachable by argument-type
inference. Phase-3.5 inference still handles the common case where
the index can be derived from an argument's type.

The test file gets three new cases (size[:5] xs, count[:5]<:int>,
inferred-only baseline). 91 declarations now compile.
Document the plan and effort estimates for the three documented-but-
unscheduled gaps from the original A-F plan. Order: B (polynomial
unification beyond naked TIUnivar) -> C (non-refining indexed
datatypes/records) -> F (SMT translation via per-index monomorphize).
Generalise the index unifier so [?u + k = poly] is solved when ?u
appears with net coefficient ±1 and the residual stays non-negative.
Previously only the naked-univar special case [?u = poly] worked, so
e.g. [tail xs : 'a vec<:n>] (where tail expects [vec<:n+1>]) failed
to unify against a caller-supplied [vec<:5>].

The new tindex_solve_for_univar walks the signed difference of the
two canonical polynomials, accepting only equations where:
  - exactly one TIUnivar has non-zero net coefficient,
  - that coefficient is ±1,
  - every monomial mixing univars with other variables (or with a
    univar at degree > 1) cancels to zero on net,
  - the resulting value of ?u has non-negative coefficient on every
    remaining monomial and constant.
The MVP scope deliberately excludes multi-univar Diophantine and
cases like [?u + 1 = n] for free n (no symbolic guarantee n >= 1).
Lift the Phase-3 Slice-A refusal that blocked index binders on
datatype/record declarations. trans_datatype and trans_record now
take an optional ~idxparams; ecScope's tydecl path threads it
through the existing ~idxparams plumbing of transtyvars.

Constructor and projector signature construction in ecEnv builds
the result type via tconstr ~indices ~tyargs so e.g. INil is
registered as 'a vec<:n>, not 'a vec. The positivity checker drops
its assert (List.is_empty args.indices); indices play no role in
positivity since the recursion is on the type and indices carry no
embedded type information.

Match elaboration in trans_branch (ecTyping) and the matchfix twin
in ecHiInductive previously broke for 0-field constructors of
indexed datatypes: opentys would allocate fresh index univars that
appeared in no unified type, leaving them dangling at closed-check
time. Fix: prepend a hand-built result type to the opened list so
the freshly allocated univars are anchored to a type that
participates in the subsequent unification against the scrutinee.

Index refinement on match is deliberately out of scope: a vec<:0>
scrutinee still admits an ICons-shaped pattern at the type level.
Matches OCaml/Haskell parametric ADT semantics.

Test file grows to 139 declarations covering constructor
application, plain match, matchfix, indexed records, and field
projection. Non-indexed datatypes/records continue to work
unchanged.
Constructors of an indexed datatype are universally quantified over
the index just like over type variables: INil has type
forall n 'a. 'a ivec<:n>. Per-constructor result indices (GADT
style) require both per-ctor result-type syntax AND index refinement
on match, the latter being a much bigger dependent-typing feature.
…phization

Replace the two CanNotTranslate raise sites in ecSmt for indexed
Tconstr / Fop with a monomorphisation path. New helper
EcAst.tindex_to_int reduces a tindex to a closed integer when
possible (no free vars, no leftover univars); the SMT pipeline uses
it to key two new caches (te_ty_idx, te_op_idx) by
"path<:i,j,...>".

trans_pty_idx / trans_tydecl_idx substitute idxvars by TIConst via
EcCoreSubst.f_subst_init ~idx and emit fresh Why3 sorts named
<path>_<i>_<j>... For indexed datatypes / records, all per-index
constructor and projector variants are populated as a side-effect.

trans_op_idx checks op_kind first: constructors / projectors /
record-makers force their carrying type's monomorphisation (which
populates te_op_idx); plain indexed operators get a fresh abstract
Why3 symbol via create_op_idx. Bodies of plain indexed ops are
dropped in this MVP — sound (treats the op opaquely) but limits
SMT's unfolding across index instances.

Goals with free index variables (e.g. an axiom binding [n]) still
hit CanNotTranslate, preserving the per-goal skip behaviour: the
existing try/catch around init / make_task emits the warning and
the lemma falls through to "no provers" without a crash.

Verified via 4 new SMT-discharge lemmas (160 declarations total in
tests/indexed-types.ec) and a smoke test confirming non-indexed
SMT goals are unaffected.
Lock in that lemma headers accept index params via the existing
[n 'a] mixed_tyvars_decl syntax (shared with op binders).
Discovered no fix was needed — the original "lemma binders don't
accept index params" finding was a syntax confusion: separate
brackets ['a] [n] are not supported (only the combined ['a n]
form), and <:n> is the type-application framing while [:n] is the
op-call framing.

Two new lemmas exercise: a parametric proof using [trivial], and a
quantified form using [move => ; trivial]. SMT discharge of goals
with bound (non-closed) indices remains correctly skipped.
The printers for type declarations, operators, predicates,
abbreviations, axioms and added-ops only emitted [tparams.tyvars],
silently dropping the index binders. So [type [n] word.] would
print as [type word.], hiding the index parameter from the user.

New helper [pp_paramsannot ppe fmt (idxvars, tyvars)] prints the
combined-bracket form [n 'a] matching the input syntax. All five
printers now consult both lists; the per-kind operator printers
(pp_opdecl_op / _pr / _nt) take a [ty_params] record instead of a
bare tyvars list, and the dispatch in pp_opdecl passes
op.op_tparams.

Verified against /tmp/print_idx.ec: [type [n] word.],
[type [n m] 'a vec.], [op cons [n 'a] : ...], [pred ix_pr [n 'a]],
and [axiom ix_ax [n 'a]] all print their index binders.
The op grammar accepts an optional bracket-before-name for the
opacity tags (opaque, smt_opaque). When users write
[op [n] "_.[_]" (w : word<:n>) : bool] hoping to bind an idxvar
[n], the parser greedily consumed the [n] as tags and silently
discarded it, leaving [n] unbound in the type signature.

Add a [disambiguate_op_brackets] helper invoked from both operator
rule alternatives. If every entry in the leading bracket is in the
known-tag whitelist, treat as tags (existing behaviour preserved
for [op [opaque] foo], [op [opaque smt_opaque] foo], etc.).
Otherwise reinterpret the bracket as a pure idxvar binder; if both
the leading bracket and an after-name binder are present, raise a
clear parse error rather than guessing.

This fixes the canonical infix-style indexed-op declaration:
  type [n] word.
  op [n] "_.[_]" (w : word<:n>) : bool.
which now parses with [n] correctly bound as an idxvar.

Verified: full regression (182 decls), the original report case,
and theories/datatypes/FMap.ec (heavy [opaque] tag user) still
compile unchanged.
Indices and type variables now use distinct bracket families:
  - {n m} for index binders
  - ['a 'b] for type-variable binders
Indices come first when both are present.

Why: the previous mixed bracket [n 'a] was overloaded with the
op-leading [opaque] tags bracket, requiring a content-based
disambiguation that confused users when an unrecognised tag was
silently rewritten as a binder. With braces vs. brackets, the parser
disambiguates lexically and there is no overlap with op tags.

Parser:
- idxvars_decl now matches LBRACE lident+ RBRACE.
- mixed_tyvars_decl and bucket_mixed are gone, replaced by a single
  ix_ty_binder rule that takes idxvars_decl? then tyvars_decl? and
  returns (idxvars, tyvars_opt).
- The Gap-fix disambiguate_op_brackets helper is removed; the op
  rules now read tags from the leading [...] and the binder from
  the after-name {...} [...] pair without any reinterpretation.
- All consumers (operator x2, pred x2, inductive, notation, abbrev,
  lemma_decl) switched to ix_ty_binder.

Pretty-printer:
- pp_paramsannot emits {idx} for indices and ['a] for tyvars,
  separated by a single space when both are non-empty.
- pp_typedecl uses curly braces for the leading idx binder.

Tests: tests/indexed-types.ec (159 declarations) migrated to the new
syntax. Round-trip print produces text that re-parses unchanged.
FMap.ec (heavy [opaque] tag user) still compiles.
When an axiom / lemma / op / pred / abbreviation / notation binds
an idxvar [n] via {n}, the same ident now also resolves as an
int-typed local in the body of that declaration. Previously [n]
was only reachable in tindex positions like vec<:n>; using it as
an integer term (e.g. mkseq f n) failed with "unknown variable n".

This realises the Phase-2 design choice that idxvars and
formula-locals share a namespace: an idxvar is exactly an integer
binding, and the indexer / formula machinery agree on the ident.

New helper EcTyping.bind_idx_locals env ue pulls the idxvars out
of the unienv's tparams and binds each as a (id, tint) local in
env. Called immediately after every transtyvars ~idxparams site:
ecScope.add_r (axiom/lemma), ecScope op processing,
ecHiPredicates.trans_preddecl_r, ecHiNotations.trans_notation_r,
and ecHiNotations.trans_abbrev_r.

Verified with the original report case and a new regression in
tests/indexed-types.ec exercising [size (id_bits[:n] v) = n + 0].
Two related fixes that together let `rewrite L` and `apply L` work on
lemmas declared over indexed types.

1. PTGlobal carries indices.
   The proof-term head `PTGlobal of EcPath.path * (ty list)` becomes
   `PTGlobal of EcPath.path * (tindex list) * (ty list)`. The
   constructor `ptglobal` and the alias `paglobal` gain an optional
   `?idxs` argument (default `[]`, so non-indexed call sites are
   source-compatible). `EcEnv.Ax.instantiate` also gains `?idxs`,
   substituting the lemma's idxvars in the spec. Without this, the
   proof checker re-instantiated only tyvars and the residual idxvars
   in the body broke conversion against the goal.

2. Closing a unienv now substitutes index-univars too.
   New helper `EcUnify.UniEnv.close_subst : unienv -> f_subst` builds
   a complete `f_subst` carrying both `~tu` (type-univars, as before)
   and `~iu` (index-univars). Used at the axiom-saving and op-saving
   sites in ecScope. Previously `Tuni.subst (close ue)` left every
   `TIUnivar` in the saved AST untouched, so even after the unifier
   resolved `?u_n := n_lem` the operator-type signatures inside the
   axiom's body still carried `?u_n`. Two `bits w` nodes that printed
   identically had different fresh univars and failed `is_conv`.

Supporting infra:
- `EcUnify.UniEnv.openidx` is exposed in the .mli (was private). Both
  `pt_of_uglobal_r` and `process_named_pterm` in ecProofTerm now open
  the lemma's idxvars to fresh `TIUnivar`s alongside its tyvars and
  thread both maps through `f_subst_init` to substitute the spec.
- `EcMatching.MEV.assubst` adds `~iu:(iu_assubst ue)` so concretize
  resolves index-univars in the proof term and its formula together.
- `EcMatching` `Fop` matching uses `unify_idx` for index lists
  (exposed in `EcUnify`) instead of structural `tindex_equal`.

Verified: the user's `bits_cat` rewrite, `exact (test_eq w)` apply,
and a new regression in tests/indexed-types.ec all work; full
regression (184 decls) passes; non-indexed lemmas/rewrites unchanged.
…p targs

Three intertwined fixes for the user's [bits_cat] case:

  lemma catE {m n} (wm : word<:m>) (wn : word<:n>) (i : int) :
       0 <= i < m + n
    => (wm ++ wn).[i] = if i < m then wm.[i] else wn.[i-m].
  proof.
  move=> rgi @/"_.[_]".
  rewrite bits_cat.

1. Op application records call-site indices in Fop targs.
   `EcUnify.openty_r` now returns `(subst, ixs, tvs)` (was
   `(subst, tvs)`); `select_op` returns
   `(path, idxs, tys) * top * subue * sbody` (was `(path, tys)`);
   `EcTyping.OpSelect.opsel.\`Op` and `opmatch.\`Op` carry
   `path * tindex list * ty list`. `form_of_opselect` builds
   `f_op p ~indices:ixs ~tyargs:tys ty`. Without this the call-
   site indices were lost on Fop nodes — every `Fop` carried
   `targs.indices = []` regardless of how the op was applied.

2. Op unfolding substitutes both tyvars and idxvars in the body.
   `EcEnv.Op.reduce` was substituting only `tparams.tyvars`; now
   it builds an `f_subst` with both `~tv` and `~idx` maps so that
   every nested `Fop` in the unfolded body has its `targs.indices`
   and `f_ty` rewritten to use the call-site indices.

3. Matcher does not unify Fop indices on the head.
   The polynomial-against-polynomial `bits` head match
   (`?u_m + ?u_n` against `m + n`) is genuinely ambiguous when
   considered in isolation — multiple multi-univar Diophantine
   solutions. The Fop matcher now only unifies type arguments and
   trusts the surrounding `Fapp` arg matching to constrain indices
   via per-arg f_ty unification (matching `(++) wm wn` first sets
   `?u_m := m, ?u_n := n` individually, then `bits`'s polynomial
   head trivially matches by reduction).

Every consumer of the new triple form was updated: ecHiInductive,
ecPrinting, ecScope (3 sites), ecTyping (4 sites), ecUserMessages.
Verified: full regression (202 decls) + new `unfold_then_rewrite`
test exercising the bug pattern.
…apply

When a lemma or op binds [{n}] and uses [n] both as a tindex AND as
an int term in its body (e.g. [size_bits {n} (w : word<:n>) :
size (bits w) = n]), substitution must reach BOTH namespaces:
- the tindex side ([TIVar n_lem] in [bits]'s targs), and
- the formula-local side ([Flocal n_lem] on the RHS).

Without the second part, opening the lemma at index [m] leaves a
dangling [Flocal n_lem] in the rewrite RHS, the goal becomes
[size (bits wm) = Flocal n_lem] (printed misleadingly as [... = n]
since both n_lem and m_caller share the name "n"), and the proof
cannot close. Same issue for [Op.reduce] when an op's body uses an
idxvar as int.

Fixes:
1. New [EcCoreFol.f_of_tindex : tindex -> form] projects a tindex
   into the int-formula world. [TIVar id -> Flocal id : int],
   [TIConst k -> f_int k], [TIAdd/TIMul -> f_int_add / f_int_mul].
   Asserts on residual [TIUnivar].
2. [EcEnv.Op.reduce] (op-unfolding) and [EcEnv.Ax.instantiate]
   (lemma application) now also bind [n_lem -> f_of_tindex idx] in
   [fs_loc] alongside the existing [fs_idx] binding.
3. [EcProofTerm.pt_env] gains a [pte_idx_link] field recording each
   lemma's [(idxvar ident, fresh tindex univar uid)] pairs;
   [concretize_env] uses it to bridge the two namespaces during
   proof-term concretization (when [?u_pat] resolves to [TIVar
   m_caller], the corresponding [Flocal n_lem] in the body gets
   bound to [Flocal m_caller]).

Verified: the user's [size_bits] / [bits_cat] / [catE] proof works,
plus a new regression case in tests/indexed-types.ec covering the
"idxvar used as int term in lemma RHS" pattern (214 decls total).
The previous fix for [pt_of_uglobal_r] (the no-instantiation lemma
opener) wasn't carried over to [process_named_pterm] (the explicit
[lemma[:idx]] / [lemma<:ty>] opener). Result: [have := mkK[:m + n]]
on a lemma whose body uses [n] as an int term left a dangling
[Flocal n_lem], breaking the proof checker with InvalidGoalShape.

[process_named_pterm] now mirrors [pt_of_uglobal_r]:
- For idxvars whose [openidx] returned a concrete [tindex] (because
  the user supplied [[:idx]]), bind [Flocal n_lem -> f_of_tindex idx]
  in [fs_loc] directly. The substitution flows into the formula
  immediately.
- For idxvars whose [openidx] returned a fresh [TIUnivar] (the
  no-instantiation case), record [(n_lem, ?u)] in [pte_idx_link] so
  [concretize_env] can synthesise the form binding once unification
  resolves the univar. Same mechanism as [pt_of_uglobal_r].

Verified with the user's [have := mkK[:m + n]] case, plus a new
regression in tests/indexed-types.ec exercising the explicit-index
[have :=] pattern (227 decls total).
The previous fix made the matcher's Fop case skip index unification
entirely, since polynomial-against-polynomial unification with
multiple univars (e.g. [bits[:?u_m + ?u_n]] vs [bits[:m + n]]) is
genuinely ambiguous in isolation. But that broke the simpler
single-univar case: [rewrite mkK] (where mkK has one bound idxvar)
no longer constrains [?u_pat] from the [mk[:?u_pat]] head, so the
univar stays unresolved and the matcher concludes "nothing to
rewrite".

Make the index unification best-effort: try to unify each pair, and
if a particular pair fails (multi-univar case), silently continue
and let arg matching constrain the residual univars later. The
single-univar case (handled by Gap-B's naked-univar fast path) goes
through normally.

Type unification on Fop heads stays mandatory.

Verified: the user's [rewrite mkK] case now works, alongside the
earlier [bits_cat] / [catE] cases (240 decls).
…ivars

[Ax.instantiate], [Op.reduce], and [process_named_pterm] all bind
[Flocal n_lem -> f_of_tindex idx] alongside the [TIVar n_lem -> idx]
tindex substitution. But the call site can supply an [idx] that
still contains an unresolved [TIUnivar] — happens when the matcher
invokes [Ax.instantiate] before the surrounding unification has
pinned the univar (e.g. on a chain like [apply: inj_bits; rewrite
bits_cat. rewrite bits_cat]). The asserting [f_of_tindex] then
crashes with the Phase-2 assert.

New [EcCoreFol.f_of_tindex_opt : tindex -> form option] returns
[None] when [ti] still contains [TIUnivar]. The three substitution
sites use it to silently skip the form-side binding in that case;
the form-side then gets resolved later by [pte_idx_link] at
[concretize_env] time, once the univar is pinned.

The asserting variant [f_of_tindex] stays for callers that have
proven all univars are resolved.

Verified: the user's [catA] / [apply: inj_bits; rewrite bits_cat]
proof works (255 decls in tests/indexed-types.ec).
[h_tvar] is a [ty_params] record carrying both [tyvars] and
[idxvars], but the goal/hyps printers only displayed the type
variables. Lemmas with index binders (e.g. [{m n}]) showed an empty
"Type variables: <none>" line and no clue that [m, n] were in scope
as int-typed indices.

[pp_goal1] and [pp_hyps] now register the idxvars in the printer
env (via [PPEnv.add_locals]) and emit an "Index variables: m, n"
line above the existing "Type variables:" line whenever there is
at least one idxvar. The line is omitted entirely when no idxvars
are bound, so non-indexed lemmas look unchanged.

Verified: full regression (255 decls) passes; the line appears
during interactive proofs of any lemma with [{...}] binders.
Idxvars are non-negative integers by Phase-2 design. Expose this in
proofs by wrapping the goal — at proof-start time, not at lemma-save
time — with one [0 <= n_i =>] implication per idxvar. The user
introduces them on demand via [move=> Hn_i].

The wrapping is pushed INSIDE the lemma's [pa_vars] forall (not at
the very top) so the existing auto-introduction of [pa_vars] still
fires. So [lemma foo {n} (xs : T<:n>) : P] still auto-intros [xs];
the [0 <= n =>] hypothesis appears next, available via [move=>].

The implications never leak into the saved [ax_spec]: only the
proof goal sees them. Lemma application by other lemmas does not
require discharging [0 <= n] — the indexed-type discipline
guarantees it.

Also extend [EcSmt.lenv_of_tparams_for_hyp] to register each idxvar
as a top-level int constant in [te_lc], so [smt()] can talk about
the bound idxvars (else [trans_app]'s [Flocal] case would hit
[oget None]).

Verified: full regression (271 decls) plus three new test cases —
[idx_ge0_simple] (no [pa_vars]), [idx_ge0_smt] (multi-idxvar,
discharged via [smt()]), and [idx_with_args] ([pa_vars] auto-intro
still works alongside the new implications).
strub added 2 commits July 2, 2026 22:42
Resolves the divergence between the indexed-types work (integer-indexed
type parameters: targs = {indices; types}, op/tydecl tparams as
{idxvars; tyvars} records, index unification) and origin/main's new
features (exceptions, subtypes, tyd_clinline inline-clone, PPAny match
patterns, classify_application-based op-selection diagnostics, hoare
hsi_m/hsi_inv split).

Conflict resolutions (12 files):
- ecTypes/ecUnify/ecTyping/ecTheoryReplay/ecScope/ecSection/ecHiGoal/
  ecPrinting/ecParser/ecThCloning/ecPhlPrRw: combine HEAD's record-shaped
  targs/ty_params + index threading with origin/main's new features.
- select_op result now carries call-site indices ((path, ixs, tvs), ...);
  threaded through ecTyping constructor/record-projection paths.
- ecTheoryReplay: keep origin/main's fix-989 (ctor-remap HACK lifted out of
  the Inline-only branch) atop the record-shaped params.
- ecPrinting try_pp_notations: use origin/main's result-typed op form
  (f_op p ~tyargs:tv rty) rather than the function-typed variant, so infix
  operators print correctly again.
- ecFol.proj_distr_ty: keep origin/main's lenient "unary constructor"
  match so distr projections in outline/encryption theories don't assert.
- xop_override/preoperator `Direct payloads use EcIdent.t list (origin/main
  semantics) now that EcDecl.ty_params is a record.
- op-application-errors.ec: one expect-fail case updated to the richer
  diagnostic HEAD's unifier emits (shows the inferred `'a = int`).

Validated with -no-eco: dune build clean; stdlib 125/125; unit 86/86;
exception+circuit 21/21; tests/ 65/65; branch's indexed-types.ec passes.
Picks up origin/main's `defer failed-application classification (perf)`
(e1a1e84) and auto-unfold of some Logic operators (88096fb).

Only conflict was src/ecUnify.ml select_op_outcomes: origin/main split
op-selection into a unify-based `select` (OK via mk_ok) plus a lazy
`KO (lazy (classify ...))` for deferred error classification. Adopted
that structure and threaded the indexed-types call-site indices through
it (mk_ok now carries ixs into OK ((path, ixs, tvs), ...); classify uses
op_tparams.tyvars).

Validated with -no-eco: build clean; stdlib 125/125; tests/ 65/65;
expect / op-application-errors / indexed-types pass.
@oskgo

oskgo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

It seems to me like unification of indexed types with unification variables in the indices is equivalent to solving systems of polynomial equations over the positive integers. This is an intractable problem in general.

What are the limitations of the unification algorithm you implement?

strub added 5 commits July 3, 2026 17:07
Add ambient natural-number index parameters to sections, mirroring
`declare type t` for type variables.  `declare index {n}` introduces an
index `n` in scope for the rest of the section — usable binder-free in
`word<:n>` positions and as an int value — and generalized back to a `{n}`
binder on section close, on-demand (only on items that actually use it).

`index` is a keyword but is also admitted as an identifier (added to the
`_lident` rule), so `List.index` etc. keep working.

Because indices are ℕ, each proof in the section gets `0 <= n` injected as a
top-assumption of the goal (via the same `f_int_le f_i0` path the `+`
marker uses), for the indices the lemma uses; the proof intros it
explicitly.  It never enters the saved `ax_spec`, so generalized statements
stay clean.

- EcEnv: `env_idxdecl` field + `declared_indices` / `lookup_declared_index`
  / `push_declared_index`.
- lexer/parser/parsetree: `index` keyword (also a lident); `declare index {n}`
  -> `Gdeclidx`.
- EcCommands/EcScope: `Index.declare`; `start_lemma` augments the proof
  `tparams` with the used ambient indices and injects their `0 <= n`.
- EcTyping.transtindex: fall back to the env's declared indices (rigid).
- EcSection: `SC_decl_idx` + `add_decl_index`; `add_declared_idx`,
  `generalize_extra_idx`, `form_idx_fv` wired into generalize_{opdecl,axiom}.

No regression: stdlib 127/127, unit 89/89.
Length-indexed array and bit-word theories, indexed counterparts of
Array.ec and BitWord.eca, developed with `declare index {n}`:

- IArray: `'a array<:n>` by reflection to lists; full Array.ec lemma
  surface (get/set, get_set(_if), set_out/neg/above, set_set_*,
  eq_from_get, offun, map) at symbolic n.
- IWord: `word<:n>` = false-defaulting view of `bool array<:n>`, mirroring
  BitWord.eca: bitwise ops, the `bwordE` hint set, and the Boolean ring
  laws via wordP.

The derived layer lives in a `declare index {n}` section (ops/lemmas drop
their `{n}`; `0 <= n` is a top-assumption in each proof).  tests/ exercise
symbolic n and concrete widths, plus a per-width Ring.BoolRing instance.
…x-univar fix

Two related changes that let algebraic-structure instances (Ring/Field)
be generalized over section-declared indices, so a single instance covers
a whole family of nonzero widths.

Index-parametric instances:
- ecDecl: `ring` record gains `r_indices : tindex list` — the index
  arguments shared by every ring op (empty for non-indexed rings, so those
  are byte-identical). Threaded through ecSubst.subst_ring and
  ecTheoryReplay.
- ecTyping.get_ring/get_field: derive the concrete `r_type`/`r_indices`
  from the matched carrier, using `close_subst` (resolves both type- and
  index-univars) so a stored `word<:n+1>` instance comes back at the goal's
  concrete width.
- ecAlgebra.rapp / ecAlgTactic.inject_indices: emit ring ops at the
  carrier's indices when building reflected terms and instance axioms.
- ecSection.generalize_instance: on section close, move the declared
  indices the carrier depends on into the instance's ty_params; on_instance
  no longer crashes on the synthetic `General` zmodule/ring/idomain
  cross-links (they point at non-existent paths).
- ecScope.ring_of_symmap: capture the carrier's indices into `r_indices`.
- EcAst.tindex_normalize: canonical (normal) form of an index.

Predicate index-univar fix (ecHiPredicates.close_pr_body):
- Predicate bodies were closed resolving only type-univars (`assubst`),
  leaving index-univars in the body. A `pred P (w : word<:n+1>) = w = onew`
  stored `onew<:?u>` instead of `onew<:n+1>`, so unfolding it produced a
  goal not convertible to the real `onew<:n+1>` (an InvalidGoalShape on
  apply/exact). Now closes with `close_subst` (type + index univars), like
  the operator path. No-op for non-indexed predicates.

No regression: unit 89/89, stdlib 129/130 (DynMatrix:1047 is a pre-existing
sandbox SMT flake, unaffected by these changes).
…bols

The indexed-SMT translation had only two modes: monomorphise a concrete
index to a fresh Why3 sort/symbol (Gap-F), or `raise CanNotTranslate` for
anything else — which aborted the whole task. A symbolic index had no
representation at all, so a single `w : word<:k>` in scope skipped an
otherwise-pure-int goal ("constructs not yet exported to Why3").

Replace that with a uniform encoding where the index is a proper symbol:

- tindex -> an int term. `trans_app`'s `Fop` case maps each index through
  `EcCoreFol.f_of_tindex` and feeds it to `trans_form`, so idxvars resolve
  through the same `te_lc`/`le_lv` int-symbol machinery as any int local.
- indexed type `T<:i>` -> one Why3 sort per (path, tyvars); the index is
  carried at the term level, not the sort level. `trans_ty` drops it.
- indexed op `f<:i>` -> one Why3 function with the indices as explicit
  leading `int` arguments (`create_op` prepends them; `apply_wop` gains an
  `~idx` parameter that supplies the concrete index terms). Indexed ops are
  kept opaque (no body) — sound.
- a polymorphic lemma's idxvars -> universally-quantified Why3 int
  variables (`trans_axiom` binds them; `add_axiom` `?qvars` closes over
  them). Type variables keep relying on Why3's type polymorphism.

This subsumes the concrete case, so Gap-F is removed
(`trans_{pty,tydecl,op}_idx`, `create_op_idx`, `idx_key`, `idx_suffix`,
`tindices_to_ints`, and the `te_ty_idx`/`te_op_idx` caches): -206 lines net.

Sound abstraction: abstract sorts + uninterpreted functions add no axioms,
every width-dependent operator threads its index, and a cross-width
equation is ill-typed in EC so never reaches Why3. A false symbolic-width
goal (`to_uint w = 0`) stays unprovable.

Symbolic-width `smt()` now works (e.g. `to_sint_cmp`, `to_uint w < 2^k`,
pure-int goals with an indexed local in scope). No regression: unit +
prelude + stdlib 220/220.
Develop theories/datatypes/IWord.ec into a Tier-1 machine-word surface:

- bitwise `orw` + simp lemmas;
- arithmetic ℤ/2ⁿ ring: ops `(+)`/`oppa`/`(*)`, the `to_uint`
  homomorphism laws (to_uintD/M/N), the `of_int` laws, and a
  `ComRingDflInv` clone `WRingA` over `word<:n+1>`;
- unsigned/signed comparisons `\ule`/`\ult`/`\sle`/`\slt` + Ngt/Nge;
- shifts/rotates `>>>`/`<<<`/`sar`/`ror`/`rol` with bit-laws;
- shift <-> arithmetic: `int_bitMP`/`int_bitDP` (reusable pure-integer
  bit-shift lemmas), `shlMP`/`shrDP`, and `to_uint_shl`/`to_uint_shr`.

Tests: iword_num (numeric round-trips, smt-through-symbolic-index),
iword_arith (arithmetic/comparison/shift surface), iword_ring (boolean
ring tactic at symbolic/concrete widths); drop the superseded
iword_ring64.
@alleystoughton

Copy link
Copy Markdown
Member

It would be good to allow

op f {n m} ['a, 'b] : 'a -> 'b -> bool.

op g = f[:n = 3, m = 4]<:'a = int, 'b = real>.

@alleystoughton

Copy link
Copy Markdown
Member

Should the operator added messages and operator printing show indices?

op f {n m} ['a, 'b] : 'a -> 'b -> bool.

op g = f[:3, 4]<:int, real>.
(*
+ added operator g : int -> real -> bool
*)
print g.
(*
* In [operators, predicates or exceptions]:

op g : int -> real -> bool = f<:int, real>.
*)

@alleystoughton

Copy link
Copy Markdown
Member

This error message could be more specific.

op f {n m} ['a, 'b] : 'a -> 'b -> bool.

op g = f<:int, real>.
(* this operator type contains free type variables *)

@alleystoughton

Copy link
Copy Markdown
Member

And this is pretty confusing:

op f {n m} ['a, 'b] : 'a -> 'b -> bool.

op g = f[:3]<:int, real>.
(* unknown variable or constant: `f' *)

@alleystoughton

Copy link
Copy Markdown
Member

At the moment, there is no way to override indexed operators in cloning. We need:

type {n} 'a vec.
op cons {n} ['a] (x : 'a) (xs : 'a vec<:n>) : 'a vec<:n+1>.

theory U.

type {n} 'a foo.

op f {n} ['a] : 'a -> 'a foo<:n> -> 'a foo<:n+1>.

end U.

clone U as U' with
  type {n} 'a foo = 'a vec<:n>,
  op f {n} ['a] (x : 'a) (xs : 'a vec<:n>) = cons[:n] x xs.

strub added 3 commits August 17, 2026 08:39
# Conflicts:
#	src/ecProofTerm.ml
#	src/ecProofTerm.mli
#	src/ecScope.ml
#	src/ecSubst.ml
#	src/ecTheoryReplay.ml
#	src/ecTyping.ml
- adapt main's new sty_subst call sites to record-shaped ty_params
  (.tyvars/.types), missed from the merge commit
- drop now-unused 'open EcTypes' in ecHiPredicates (fatal under
  --profile=ci)
Allow naming index arguments at instantiation sites, independently of
the positional/named choice made for type arguments:

  op f {n m} ['a, 'b] : 'a -> 'b -> bool.
  op g = f[:n = 3, m = 4]<:'a = int, 'b = real>.

- parsetree/unify: the index side of an instantiation becomes its own
  positional/named sum (IXunamed/IXnamed), carried by both TVIunamed
  and TVInamed; the parser's 'cannot mix explicit indices with
  named-tyvar syntax' restriction is gone (all four combinations are
  legal).
- named index instantiation may be partial: unnamed idxvars fall back
  to fresh index univars and are inferred (positional instantiation
  still requires the full arity).
- unknown names now error instead of being silently ignored:
  opentvi/openidx raise on a name binding no formal (safety net;
  surface paths validate earlier), op selection filters candidates by
  index-name subset, and pf_check_tvi checks lemma instantiations
  ('unknown index variable', 'wrong number of index parameters') the
  same way it already checked type variables.

Requested by Alley Stoughton in #1065 (comment 1); the partial form
also gives a principled route around the confusing partial-positional
error (comment 4).

Tests: tests/named-index-instantiation.ec (all four combinations,
out-of-order names, partial op/lemma inference, five expect-fail
diagnostics). No regression: unit + prelude + stdlib green; ci
profile (warnings-as-errors) clean.
@strub

strub commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

It would be good to allow op g = f[:n = 3, m = 4]<:'a = int, 'b = real>

Done in 9460bc0: index and type instantiation can each be positional or named, in any combination. Named indices may be partial (cat[:n = 3] u v infers m), and unknown names are now rejected with a proper error.

Explicit index instantiations were dropped when printing operator
references: `op g = f[:3, 4]<:int, real>` printed back as
`f<:int, real>` (PR #1065, Alley's comment 2).

- pp_opname_with_tvi gains an index component: prints
  `f[:3, 4]<:int, real>`.
- pp_opapp threads the Fop/Eop indices (callers pass the full targs
  record instead of .types).
- suppression mirrors the type-argument policy: a new ixs_dominated
  (over a free-idxvar-of-type collector) hides indices inferable from
  the printed arguments' types; the showtvi pragma forces them.

Everything prints through pp_form -> pp_opapp (pp_expr converts to a
form), so goals, bodies, print and search output are all covered.
Declaration printing (print f / added-operator messages) already
showed {n m} binders.

Tests: expect-by-print assertions in tests/named-index-instantiation.ec
(shown / suppressed-inferable / printed-non-inferable). No regression:
unit + prelude + stdlib green (incl. the print-asserting tests
expect.ec / print-proc.ec / clone-type-inline.ec); ci profile clean.
@strub

strub commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Should the operator added messages and operator printing show indices?

Yes — fixed in 4908019: print g now shows f[:3, 4]<:int, real> (indices follow the same display policy as type arguments: hidden when inferable from the printed arguments, shown otherwise). The added-operator message already prints index binders when the operator has them (added operator f {n m} ['a, 'b] : ...); for g there is nothing to add since it has no index parameters and added messages don't show bodies.

strub added 2 commits August 17, 2026 13:23
PR #1065, Alley's comments 3 and 4. An explicit instantiation
incompatible with an operator produced "unknown variable or constant:
`f'" (the tvi check ran as a silent candidate pre-filter, so the
failed-application classifier never saw the candidate), and omitted
indices died at declaration close as "this operator type contains
free type variables".

- ecUnify.select_op_outcomes: the tvi compatibility check moves from
  the EcEnv.Op.all pre-filter into the selection loop; an incompatible
  candidate now yields a classified KO (new op_failure variants
  OF_idx_arity / OF_idx_unknown / OF_tv_arity / OF_tv_unknown) before
  open/unify (openidx's arity fallback would otherwise let a
  wrong-arity candidate unify). Selection semantics unchanged.
- op_instance becomes {oi_tys; oi_ixs}: application failures now
  report "where the index parameters were inferred as: n = 3"
  alongside the type parameters; failure-report types resolve BOTH
  univar kinds and normalize indices (display independent of
  constraint-solve order). pp_tindex joins the PrinterAPI.
- uninferred indices are reported as such: UniEnv.closed splits into
  closed_tv/closed_iu; the close-check sites (op/pred/formula in
  ecScope, clone overrides in ecTheoryReplay, tyerror sites in
  ecProofTyping via new FreeIndexVariables) say "cannot infer all
  index parameters ...; supply them explicitly (e.g. `f[:n = 3]')"
  when only the index side is open.

Tests: 6 new expect-fail assertions (omitted / partial-positional /
unknown-named index, unknown-named / wrong-count tyargs, inferred-index
report); the unknown-named-index assertion updates from the old
"unknown variable or constant" text. No regression: unit + prelude +
stdlib green (op-application-errors.ec byte-identical); ci profile
clean.
tests/op-application-errors.ec is the file dedicated to asserting
operator-application diagnostics; move the five UnappliedOp assertions
from the named-index feature test there (own ixop/ivcat fixtures).
named-index-instantiation.ec keeps the feature tests and the
non-application diagnostics (duplicate named index, lemma-path
pf_check_tvi checks, uninferred-indices close error).

Also drop the index-normalization pass from resolve_ty_for_report: it
was inert — ty hashconsing compares indices canonically, so a rebuilt
vec<:8> IS the interned vec<:3+5> node and the display spelling is
whichever got interned first (deterministic per file). Keep the real
part (index univars resolved so reports never show ?#N) and document
the hashconsing behaviour.
@strub

strub commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

This error message could be more specific. / And this is pretty confusing:

Both fixed in f2074ba. Omitted indices now report

cannot infer all index parameters of this operator; supply them explicitly (e.g. `f[:n = 3]')

and an incompatible instantiation is classified per candidate instead of degenerating to "unknown variable or constant" — f[:3]<:int, real> gives

operator `Top.f' cannot be applied:
it takes 2 index parameter(s) but is given 1

(same treatment for unknown named arguments and wrong type-argument counts). Application failures also list inferred indices alongside inferred type parameters.

strub added 2 commits August 17, 2026 15:32
PR #1065, Alley's comment 5. Operator and predicate override clauses
in `clone with' now accept index binders, in both alias and inline
modes:

  clone U as U' with
    type {n} 'a foo = 'a vec<:n>,
    op   f {n} ['a] (x : 'a) (xs : 'a vec<:n>) = cons[:n] x xs,
    pred p {n} ['a] (xs : 'a vec<:n>) = nonempty xs.

Surface plumbing: idxvars_decl on the OP/PRED override productions
(nonneg markers rejected), opov_idxvars/prov_idxvars parsetree fields,
replay via transtyvars ~idxparams + bind_idx_locals, and a
NotSameNumberOfIdxParam incompatibility (index arity of an override
must match the overridden declaration).

The work exposed three latent index bugs in shared infrastructure,
all fixed:
- EcEnv.Ty.unfold substituted only type parameters: unfolding an
  indexed type alias leaked its formal index variable, so
  `foo<:3>' failed to unify with its own unfolding `vec<:3>'
  (independent of cloning).
- Compatible.for_ty renamed only the reference declaration's tyvars
  onto the override's; it now renames idxvars too.
- EcSubst.open_oper (and get_open_oper/get_open_pred) opened
  operators at type arguments only; it now takes ~indices.

Tests: tests/clone-indexed-override.ec (type-only, type+op+pred alias
with conversion/print checks, inline mode with axiom-inlining
assertion, index-arity-mismatch rejection) and
tests/indexed-type-alias.ec (alias unfolding at concrete/symbolic
indices, index arithmetic through the alias). No regression: unit +
prelude + stdlib green; ci profile clean.
IWord.ec's msbE closed with smt(to_uint_cmp half_modulus) under the
local z3 but not under the CI prover set (deterministic failure on
every CI stdlib run; CI's cvc5 answers are unparsed by why3, and its
z3 rejects the exponential goal).

Rework the proof so no prover ever sees an exponential: discharge the
2^(n-1) facts with half_modulus/gt0_half while concrete, generalize
the power to an opaque constant, and close the remaining
linear-plus-division goals with explicit IntDiv hints (divz_ge0,
ltz_divLR).
@strub

strub commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

What are the limitations of the unification algorithm you implement?

Right: the general problem (univars on both sides) subsumes solving polynomial systems over ℕ, so we do not attempt it. What is implemented is a small deterministic fragment, and the split matters. Checking index equality is complete: indices are compared by canonical polynomial normal form, which is decidable. Solving is restricted to (a) naked-univar assignment with an occurs-check, and (b) single-univar affine equations with net coefficient ±1 whose solution has non-negative coefficients, e.g. ?u + 1 = n + 5 yields n + 4. Constraints that are not solvable yet are deferred and retried as other assignments land; there is no search or backtracking. Everything outside the fragment is refused: multi-univar Diophantine equations, non-unit coefficients, nonlinear occurrences, and anything whose solution would need an unproven non-negativity assumption (?u + 1 = n is refused since ?u := n - 1 requires n >= 1). Refusal is conservative. The unifier never guesses, so incompleteness shows up as a type error asking for an explicit index (f[:n = 3]), never as a wrong instantiation.

strub added 5 commits August 18, 2026 10:25
… sort

Review findings 11 and 3 (both with confirmed derivations of false).
The Why3 translation erases indices at the sort level; the old
justification ('a cross-width equation is ill-typed in EC') is
insufficient: an axiom quantifying over ONE width, e.g.
forall (c : bool array<:0>), size (ofarr c) = 0, loses its width
restriction and constrains the whole erased sort — collapsing
array<:1> and deriving false. Independently, lemma idxvars were
quantified as an unguarded forall n:int, asserting more than EC
proved (smt(ge0_index) proved 0 <= n for every integer).

Make the index recoverable at the term level for VALUES, as it
already is for operations:
- per-family width observers size_k : ('a,..) t -> int (one per index
  position, polymorphic, declared on first use);
- quantifiers over head-indexed types are relativized:
  forall (x : t<:i>), P  ==>  forall x, size_k x = i => P
  (conjunct for exists; lambdas assert nothing). Guards are exact.
  Where an indexed constructor occurs where observers cannot reach
  (nested under another constructor / tuple / arrow, or as a type
  argument of an indexed head) the binder raises CanNotTranslate and
  takes the existing sound trans_gen fallback;
- lemma idxvars get 0 <= n premises (at the EC level, riding the
  normal translation); goal idxvars get 0 <= n facts;
- goal locals at head-indexed types carry size_k x = i facts;
- every indexed operator gets a typing-justified result-width axiom
  forall i xs, 0 <= i => size_k (f i xs) = e_k(i) — this is what keeps
  guarded lemmas usable, not just sound (the 0 <= i premises keep the
  union-of-widths model satisfiable at out-of-range indices; EC types
  are inhabited so every natural-width carrier is non-empty).

Soundness: interpret the erased sort as the disjoint union of the
per-width carriers and size_k as the width tag; every EC model
extends, so Why3 proofs under guards are EC-valid.

Tests: tests/indexed-smt-guards.ec turns both reviewer exploits into
fail-smt regressions and checks the positive direction (width-0
lemmas prove width-0 goals through the guards; symbolic-width smt
still fires). No regression: unit + prelude + stdlib + examples all
green; ci profile clean.
Review findings 5, 6, 10 shared one root cause: the branch introduced
a second univar kind (index univars, ue_iuf) and a combined closing
substitution (close_subst), but left the legacy type-only
close/assubst : unienv -> ty Muid.t exported — so ~30 call sites each
chose independently, and the wrongly-chosen ones silently dropped
resolved indices at persistence boundaries (dangling TIUnivar in proc
bodies, have-hypotheses, abbrev bodies).

Make the wrong choice inexpressible: close/assubst leave the public
UniEnv API (they remain internals); the exported closing API is
exactly close_subst (raising) and the new as_subst (non-raising),
both returning the f_subst that resolves BOTH univar kinds.
iu_assubst stays exported solely for the proof-term idx-link bridge.

The compiler then enumerated every consumer (~30 sites, 13 files):
ecProofTyping (finding 6: process_form/stmt/type/exp/poe,
tc1_process_stmt), ecHiNotations (finding 10: abbrev), ecTyping
(Pst_fun family, PSmatch, op-selection error paths — which now also
DISPLAY resolved indices), ecTheoryReplay (clone-override op/pred
closes — latent, unreported), ecScope (axiom close, subtype
predicate, ring/field instance carriers), ecMatching (f_match's
second component becomes the combined f_subst; its only caller
ignored the map), ecReduction (user rewrite-rule instantiation),
ecHiInductive, ecTypesafeFol, ecUserMessages, phl/ecPhlRwEquiv,
phl/ecPhlRwPrgm. Most sites get simpler (no Tuni.subst wrapping).

Tests: tests/indexed-univar-close.ec pins the three repaired
behaviors (abbrev body linked to its binder instead of a frozen ?#N,
proc bodies persisting resolved indices, have-hypotheses usable).
No regression: unit + prelude + stdlib + examples green; ci profile
clean.
Review finding 4 (confirmed derivation of false). Match-fix
iota-reduction substituted only the operator's TYPE parameters into
the body: the idxvar stayed dangling in both its namespaces (TIVar
positions and its int-formula occurrences, e.g. a branch body that
returns n), so f[:5] ... and f[:7] ... reduced to the SAME term and
'by cbv' proved their equality.

New EcFol.f_subst_tparams instantiates an operator body at explicit
targs the way EcEnv.Op.reduce already did correctly: tyvars via the
type substitution, idxvars via the ~idx map AND via f_bind_local on
their int-typed formula-local occurrences. Migrated four sites:
- ecReduction iota (the reported one),
- ecCallbyValue iota (its twin),
- both rewrite-/op delta-unfold sites in ecHiGoal, whose destructuring
  tuple discarded the indices at construction (same bug shape, found
  by sweeping for the pattern; the tuple now carries the full targs
  and ty_params).

Tests: tests/indexed-iota.ec — iota lands on the call-site index
(cbv and simplify paths), the old collapse is REFUTED
(f[:5] ... <> f[:7] ... by reduction), and rewrite /g on an indexed
plain op unfolds correctly. No regression: unit + prelude + stdlib +
examples green; ci profile clean.
…inks

Review findings 7, 8, 9 (all with confirmed repros).

LvMap (7): translvalue discarded the selected map-set operator's
inferred indices ((path * ty list) payload); i_asgn_lv then rebuilt an
index-erased op node whose arity contradicts the declaration. The
lvmap payload now carries the full targs and the rebuild passes
~indices.

Records + match desugar (8): trans_record opened the record type with
the types-only openty and Tvar.init, so PFrecord built ctor/projection
Fop nodes without indices, field/default types leaked formal idxvars,
and the proc-match desugar built the projection without the
scrutinee's indices. trans_record now opens via openty_r (combined
substitution), threads the instance indices to both builders, and the
match desugar reads the indices off the scrutinee's type.
SMT side: there is no sound erased encoding of indexed
datatypes/records yet (a width-erased nullary constructor plus the
size axioms would be inconsistent), so trans_tydecl raises
CanNotTranslate for them: goals degrade to the sound fallbacks
instead of crashing with Why3 arity errors. Full index-threaded
constructor encoding is left as documented follow-up. The 0 <= n
guards also move to the Why3 level (w3_ge0 via the known-ops table):
they no longer require the EC int theory to be in the file's scope.

Proof-term links (9): propagate_idx_link / concretize_env resolved a
link's univar with a single-hop map lookup, but ue_iuf is a chain map
(?u := ?a, ?a := 7 read as unresolved), and concretize_env only
handled literal TIVar/TIConst. Export UniEnv.repr_tindex (chain
resolution) and use it at both sites; the link binding generalizes to
any f_of_tindex-expressible index, so chains grounding in compound
indices (m+1) concretize.

Tests: tests/indexed-node-instantiation.ec (map-update wp proof,
record node conversion, fail-smt degradation, chained and
compound-chained proof-term application). No regression: unit +
prelude + stdlib + examples green; ci profile clean.
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