Skip to content

fel + smt: internal assertion (ecSmt.ml assert has_locals) when the Pr event mentions res #1090

Description

@sociofabric

Summary

smt raises an internal assertion, rather than failing or erroring, on the
subgoal fel produces when the target Pr[...] event mentions res:

[critical] anomaly: File "src/ecSmt.ml", line 909, characters 5-11: Assertion failed

Reproduced on main at 87b6f8b (2026-07-25), built from a clean clone with
opam install ./easycrypt.opam --deps-only && dune build. Plain module type
throughout — no quantum extensions involved.

Reproducer

require import AllCore Distr List FelTactic.
require import StdBigop StdOrder.
(*---*) import Bigreal.BRA StdOrder.RealOrder.

type T.
op [lossless] d : T distr.
op q : int.
axiom q_ge0 : 0 <= q.

module type O = { proc f(x : T) : bool }.
module type A (P : O) = { proc go() : unit { P.f } }.

module G (Adv : A) = {
  var bad : bool
  var c   : int

  module P : O = {
    proc f(x : T) : bool = {
      var r;
      if (c < q) {
        c <- c + 1;
        r <$ d;
        if (r = witness) { bad <- true; }
      }
      return bad;
    }
  }

  proc main() : bool = {
    bad <- false;
    c   <- 0;
    Adv(P).go();
    return bad;
  }
}.

section.
declare module Adv <: A { -G }.

lemma L &m : Pr[G(Adv).main() @ &m : res] <= q%r * mu1 d witness.
proof.
fel 2 G.c (fun _ => mu1 d witness) q G.bad [G(Adv).P.f : (G.c < q)] (G.c <= q).
admit.
smt().          (* <-- anomaly *)
admit.
admit.
admit.
admit.
qed.
end section.

What isolates it

variation result
as above anomaly
Pr[... : res]Pr[... : G.bad], one token, nothing else changed accepted
smt()admit. accepted

So the six goals fel produces are well-formed — the failure is in the SMT
translation of one of them — and the trigger is specifically res, not fel
and not the surrounding development. (fel's first goal is the arithmetic sum
bound and smt discharges it without complaint.)

It also reproduces identically on the older deploy-quantum-upgrade branch
(3cef12a), where the same assertion is at ecSmt.ml:900.

Diagnosis

src/phl/ecPhlFel.ml builds the second subgoal as

forall &m0, ev{m0} => inv{m0} => bad{m0} /\ cntr{m0} <= q

and quantifies the memory using the memtype of the ambient memory of the
Pr:

let pr_m = oget (Memory.byid pr.pr_mem env) in
...
let m = (EcIdent.create "&hr", snd pr_m) in

That memtype has no locals. When ev mentions res — a local — the
translation reaches trans_mem's assert has_locals (src/ecSmt.ml:909) and
aborts.

Substituting the Pr memtype, which declares res and which trans_pr
already uses for exactly this purpose, removes the anomaly:

-    let m = (EcIdent.create "&hr", snd pr_m) in
+    let m = (EcIdent.create "&hr",
+             snd (EcEnv.Fun.prF_memenv (EcIdent.create "&hr") f env)) in

Built against 87b6f8b, the reproducer then fails with an ordinary
cannot prove goal (strict) instead of the assertion.

What the right fix might be instead

I am not proposing that diff as a fix — it is offered as evidence for the
diagnosis. Even with it, the goal is not provable when the event is res: in
the quantified memory res is an unconstrained local, so nothing links it to
bad. fel appears to want a global bad event, which is what every use
in examples/ does (examples/Upto.ec, examples/ChaChaPoly,
examples/prg-tutorial). If that is the intended contract, rejecting the
input up front with a message to that effect would be more useful than either
the assertion or an unprovable subgoal — the assertion is what cost the time
here, because an internal error reads as "the tactic is broken" rather than
"the goal is stated wrongly".

Workaround

State the bad event over module globals. The same bound then goes through with
all six goals discharged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions