Zulip Chat Archive
Stream: lean4
Topic: naming hypotheses when using do-notation
Jonathan Protzenko (Jan 27 2023 at 22:20):
I looked into the documentation (doc/do.md) but I didn't find anything... still using fact
as my running example, consider the two styles below:
def checked_sub (n: Nat) (m: Nat): Option Nat :=
if n >= m then
some (n - m)
else
none
def fact3 (x: Nat): Option Nat :=
if x = 0 then
return 1
else
do
let y <- checked_sub x 1
fact3 y
termination_by fact3 x => x
decreasing_by
simp_wf
sorry
def fact4 (x: Nat): Option Nat :=
if x = 0 then
return 1
else
match h:checked_sub x 1 with
| some y =>
fact4 y
| none =>
none
termination_by fact4 x => x
decreasing_by
simp_wf
sorry
I am now in a monad (this is obviously a simplified version of what I'm trying to achieve)... the recursion is not really relevant to this example
for fact4
, I have a hypothesis h: checked_sub x 1 = some y
in my context (after simp_wf
), but for fact3
, I don't have that crucial bit of information in my context
is there any way to bind the hypothesis when using do <- notation?
Sebastian Ullrich (Jan 28 2023 at 15:35):
Hi Jonathan! I don't see how this could work for an arbitrary monad. But for specific cases like Option
we can transfer the hypothesis inside a subtype.
def Option.attach : (o : Option α) → Option { x : α // o = some x }
| .some x => .some ⟨x, rfl⟩
| .none => .none
... let ⟨y, hy⟩ <- checked_sub x 1 |>.attach
Jonathan Protzenko (Jan 28 2023 at 18:05):
hey Sebastian! ok, this is nice -- I had gone that route (@Son Ho's suggestion) but eventually realized that the refinement prevented the operator from satisfying the signature of the Bind typeclass... but your solution is nice and compact... maybe I could even define a custom operator (<--) that combines both in one go, but I don't know yet how to do that... thanks!
Sebastian Ullrich (Jan 28 2023 at 21:31):
Oh yes, you can do something like
macro "let" h:ident " : " e:term " <-- " f:term : doElem =>
`(doElem| let ⟨$e, $h⟩ ← Option.attach $f)
... let hy : y <-- checked_sub x 1
and then optionally abstract attach
into a typeclass
Jonathan Protzenko (Jan 28 2023 at 21:39):
ah that's excellent, thanks so much Sebastian!
Jonathan Protzenko (Feb 09 2023 at 04:13):
actually I'm reviving this thread... I finally got around to using this in real code, but then I have an universes issue:
def checked_sub (n: USize) (m: USize): Option USize :=
sorry
def Option.attach : (o : Option α) → Option { x : α // o = some x }
| .some x => .some ⟨x, rfl⟩
| .none => .none
def termination (T : Type) (key : USize) (v: T) :=
(key, v)
def foo (T: Type) (k: USize) (v: T) : Option USize :=
if h: k = 0 then
.some 0
else do
let k <- checked_sub k (USize.ofNatCore 1 (by
match USize.size, usize_size_eq with
| _, Or.inl rfl => decide
| _, Or.inr rfl => decide))
let ⟨ k, h ⟩ <- foo T k v |> Option.attach
.some k
termination_by foo T k v => termination T k v
decreasing_by sorry
here, the termination measure is rejected, on the basis that:
type mismatch
termination T k v
has type
USize × T : Type
but is expected to have type
?β : Sort ?u.9354
bear in mind that this code is auto-generated, so the termination measure always, by default, contains a tuple of all the arguments
Jonathan Protzenko (Feb 09 2023 at 04:13):
this is of course a minimal repro, the actual use-case is more involved, but this is the gist of the issue
Jonathan Protzenko (Feb 09 2023 at 04:14):
if I get rid of the option.attach and the h
, then all goes well
Jonathan Protzenko (Feb 09 2023 at 04:14):
so I don't understand why this particular usage gives rise to a universe problem
Mario Carneiro (Feb 09 2023 at 04:24):
that is a strange error message, since the type should unify with that target type
Mario Carneiro (Feb 09 2023 at 04:25):
the only thing I can think is that one of those metavariables is not assignable at the current mvar depth, or is not allowed to reference those local variables or something along those lines
Mario Carneiro (Feb 09 2023 at 04:25):
or else the error message is just lying
Mario Carneiro (Feb 09 2023 at 04:26):
do you have an MWE not using intlit
?
Jonathan Protzenko (Feb 09 2023 at 04:26):
oh sorry yes of course
Jonathan Protzenko (Feb 09 2023 at 04:26):
I thought it was self-contained
Mario Carneiro (Feb 09 2023 at 04:27):
sorry
seems to work to reproduce
Jonathan Protzenko (Feb 09 2023 at 04:27):
ok glad to know you were able to reproduce
Mario Carneiro (Feb 09 2023 at 04:32):
using (k, v)
directly causes a similar error message:
termination_by foo T k v => (k, v)
application type mismatch
(k, v)
argument
v
has type
T : Type
but is expected to have type
?m.864 : Type ?u.859
which makes me think that it is indeed an issue of the metavariable not being allowed to reference T
Mario Carneiro (Feb 09 2023 at 04:34):
This probably warrants a bug report, but you can likely work around it by using termination_by'
instead
Mario Carneiro (Feb 09 2023 at 04:35):
although, in this example how do you intend to prove termination? T
is an arbitrary type, you don't have a well founded instance on it
Mario Carneiro (Feb 09 2023 at 04:38):
For this particular example, I guess you don't want v
to participate in the well founded measure
Jonathan Protzenko (Feb 09 2023 at 05:37):
this is just to demonstrate the issue, in this example, I could conceivably show that the first argument of the pair is always decreasing, I guess
Jonathan Protzenko (Feb 09 2023 at 05:38):
thanks, I'll file an issue on the bug tracker
Mario Carneiro (Feb 09 2023 at 05:39):
I mean that in general, it doesn't make sense to have v
in there because T
is a generic argument. You would have to change the type of the function foo
to add a well founded measure
Jonathan Protzenko (Feb 09 2023 at 05:41):
do you mean that if v
appears in the termination_by
, then there is no hope of proving termination, even though the first argument is decreasing?
Mario Carneiro (Feb 09 2023 at 05:46):
I mean that there is no hope of making the termination hint typecheck, because even after you get past the current error message there is a secondary goal (solved by typeclass inference) to find an instance of WellFoundedRelation
for the type of the expression you gave (here USize x T
)
Mario Carneiro (Feb 09 2023 at 05:46):
and that will definitely fail because there is no instance of WellFoundedRelation T
in the context
Mario Carneiro (Feb 09 2023 at 05:47):
furthermore, if you fix this issue by not including variables with generic types in the tuple, then the current issue will also go away
Mario Carneiro (Feb 09 2023 at 05:49):
An alternative way to solve that issue would be to change the type of foo to
def foo (T: Type) [WellFoundedRelation T] (k: USize) (v: T) : Option USize :=
at which point arguably it should work to use (k, v)
as the termination hint, and this is what I regard as the bug in this case
Mario Carneiro (Feb 09 2023 at 05:53):
Lean is supposed to be doing some preprocessing to determine the arguments that don't change in the recursion (here both T
and v
would be eligible) and excluding them from the termination context, so that the goal becomes T, v |- WellFounded USize
instead of |- WellFoundedRelation ((T : Type) ×' (_ : USize) ×' T)
(which is more awkward to work with). Specifically, I believe termination_by
is trying to use invImage (fun (T, k, v) => <your hint>) _
as the termination relation, and this is a problem because the type of <your hint>
cannot depend on T,k,v
here since docs4#invImage requires a non-dependent function
Mario Carneiro (Feb 09 2023 at 05:57):
If you use termination_by'
you can take the construction of the termination relation into your own hands, which will likely lead to more stable behavior compared to termination_by
's heuristics
Mario Carneiro (Feb 09 2023 at 06:03):
BTW, if your termination hint is just the tuple of everything I think you can leave it off, since that's the default
Jonathan Protzenko (Feb 09 2023 at 06:14):
thanks, in my case I do expect and anticipate that the user will refine the termination measure -- the termination
definition is in a separate file that is not auto-generated and that the user will have to provide... Aeneas currently generates a "template" where the default termination measure is a tuple of all the arguments, and the termination proof is just sorry
-- the user takes that template as a base, then replaces the definitions
Jonathan Protzenko (Feb 09 2023 at 06:14):
so it's just a confusing error message for the default measure that in any case would not have worked anyhow
Mario Carneiro (Feb 09 2023 at 06:24):
Note that lean's actual default does not suffer from this issue, if you use decreasing_by sorry
alone you can see in the input tactic states some references to
invImage (fun a => PSigma.casesOn a fun T snd => PSigma.casesOn snd fun x snd => sizeOf x) instWellFoundedRelation
which is to say, lean is using the equivalent of termination_by T k v => sizeOf k
Jonathan Protzenko (Feb 09 2023 at 18:47):
I made progress, but I'm still getting errors... thanks for your patience as I try to work this out
def Option.attach {α: Type} (o : Option α): Option { x : α // o = .some x } :=
match o with
| .some x => .some ⟨x, rfl⟩
| .none => .none
def f
(T : Type) (ls : List T) :
(Option (List T))
:=
match ls with
| .cons _ tl =>
do
have h: List.length tl < List.length ls := by sorry
let ⟨ tl0, h ⟩ <- f T tl |> Option.attach
.none
| .nil =>
.none
termination_by f ls => List.length ls
here, notice that I have gotten rid of all of the decreases_by business, and I'm merely trying to get the termination proof to work out in the style you suggested (with a have
right before the recursive call)
I thought this would work, but I'm getting:
failed to prove termination, possible solutions:
- Use `have`-expressions to prove the remaining goals
- Use `termination_by` to specify a different well-founded relation
- Use `decreasing_by` to specify your own tactic for discharging this kind of goal
tl: List T✝
h: List.length tl < List.length ls✝
ls: List T
⊢ List.length ls < Nat.succ (List.length tl)
if I replace ⟨ tl0, h ⟩
with x
, then this succeeds...
Jonathan Protzenko (Feb 09 2023 at 21:09):
the other thing I'm noticing as I look back at this: I'm not even sure how the proof is supposed to succeed... this goal is really confusing
James Gallicchio (Feb 09 2023 at 21:33):
It's also fixed if you move the recursive call outside the do notation :joy: this seems to be one of those places where do notation destroys all evidence of where values come from
James Gallicchio (Feb 09 2023 at 21:34):
let me see if i can make a simple MWE to show why...
James Gallicchio (Feb 09 2023 at 21:50):
oh dear, I am quite confused. somewhat simpler MWE:
-- this works
def g (T : Type) (ls : List T) : (Option (List T)) :=
match ls with
| _::tl =>
let res := g T tl
res |> Option.attach |>.bind fun ⟨x,h⟩ => x
| [] => .none
-- this does not
def g' (T : Type) (ls : List T) : (Option (List T)) :=
match ls with
| _::tl =>
g' T tl |> Option.attach |>.bind fun ⟨x,h⟩ => x
| [] => .none
James Gallicchio (Feb 09 2023 at 21:51):
maybe one of the elaboration experts can give some insight here ???
James Gallicchio (Feb 09 2023 at 21:54):
I suspect it has something to do with the fact that g' T tl
appears in the type of Option.attach res
, which breaks (?) the structural induction??
Jonathan Protzenko (Feb 09 2023 at 21:56):
nice repro, thanks!
James Gallicchio (Feb 09 2023 at 21:56):
ooh, even more strange:
-- works
def g' (T : Type) (ls : List T) : (Option (List T)) :=
match ls with
| _::tl =>
let res := Option.attach (g' T tl)
res.bind fun x => x.val
| [] => .none
-- doesn't
def g'' (T : Type) (ls : List T) : (Option (List T)) :=
match ls with
| _::tl =>
let res := Option.attach (g'' T tl)
res.bind fun ⟨x,h⟩ => x
| [] => .none
Jonathan Protzenko (Feb 09 2023 at 21:57):
this matches what I observed earlier, which it is the unpacking of the subtype via the \langle, \rangle that causes the error
James Gallicchio (Feb 09 2023 at 21:57):
I had thought it was unrelated :D I was wrong
Jonathan Protzenko (Feb 09 2023 at 21:57):
so I'm wondering if having very dependently-typed hypotheses in the context is somehow leading to a poor unification choice (e.g. with variables that later on would escape their scope), and thus ultimately to a failure
Mario Carneiro (Feb 10 2023 at 05:58):
This is a bug for sure. I assume it has something to do with the fact that match
generates a matcher function which generalizes over a bunch of things in the context, and it might be picking g''
itself as one of the things to generalize, meaning that the call to fun ⟨x,h⟩ => x
(which macro expands to fun x => match x with | ⟨x,h⟩ => x
) turns into something like fun x => g''._match g'' T tl res x
and now g''
is being passed unapplied to a helper function and this is no longer structurally recursive
Jonathan Protzenko (Feb 10 2023 at 19:28):
thanks, I filed a bug https://github.com/leanprover/lean4/issues/2102
Jonathan Protzenko (Feb 15 2023 at 22:51):
So far, with everyone's help, I have a workaround for all of the issues above (notably, introducing extra let-bindings to defeat issue #2102)... but I am now hitting performance problems (which surfaced via errors referring to "heartbeats", as I mentioned in another thread). Here's a succinct description of the issue: if my definition uses the <-
monadic bind operator, it is type-checked instantly and the interactive mode returns to me in a snap, but if I use the <--
(see above, the one that pipes the result through Result.attach), I need to multiply max heartbeats by 10 and the definition type-checks in a little under 3 minutes. This is a closed term, so in theory, it should be able to reduce in a call-by-value, efficient fashion, but clearly, it's not happening here.
I could not reproduce the issue with Option, so unfortunately the repro involves cloning the Aeneas repo...
Here's what I tried:
- I traced Compiler.simp to see what was happening with reduction, and I did see that with my special <-- operator, there were a lot of packing / unpackings of
Subtype
- interestingly, the elaboration seems to "figure out" that with <-, some monadic let-bindings had no dependency upon a lot of earlier variables, and thus could be hoisted, this did not seem to happen with the <-- version
- I tried to reproduce with Option.bind, to no avail, so I looked up the definition of Option.bind, and I noticed that it was marked as @[inline], so I marked my own Result.attach and Result.bind as @[inline]
- with the fix above, I was able to see that eventually, the <- version (fast) and the <-- version (slow) reduce to the same term (size: 132), but I don't seem to have access to the intermediary states of reduction (the output is trimmed to the most recent few messages)
in case anyone has the good will and time to take a look, the repro is here: https://github.com/AeneasVerif/aeneas/blob/8a986a9c61e62ad3cc1955a71e3c14de00411f60/tests/lean/hashmap_on_disk/HashmapMain/Funs.lean#L558-L632, the repository should be clone-able easily, and it suffices to open this file in an editor to witness the issue (or run lake build on the command-line)
Jonathan Protzenko (Feb 15 2023 at 22:53):
my closed term is not huge, so I'm wondering if the presence of hypotheses triggers a bad complexity somewhere... but I don't have enough expertise to figure out what it might be
Last updated: Dec 20 2023 at 11:08 UTC