Zulip Chat Archive

Stream: lean4

Topic: Strange stack overflow / timeout


Ramon Fernandez Mir (Jul 15 2021 at 16:19):

I've been playing with Lean 4, trying to prove some basic facts about matrices and writing a few tactics. I encountered an issue that I can't quite explain and I'm not sure how to debug. Essentially, what I want is to get cases for every index and then prove inequalities between ground terms whose values come from matrices. I've seen different errors depending on the example I was working with anywhere from stack overflows, cores dumped, or simply timing out. Here's a #mwe that I believe illustrates my issue:

def m : (Fin 2)  (Fin 2)  Int :=
fun i j => #[#[52532234335, -234231224], #[-122223, 29338123232]][i][j]

lemma works :  (x : Fin 2), Int.natAbs (m x x)  10 ^ 80 := by {
  intros x;
  have h : x = 0  x = 1 := sorry;
  cases h;
  allGoals (subst x; dec_trivial)
}

lemma doesnot :  (x : Fin 2), Int.natAbs (m x x)  10 ^ 80 := by {
  intros x;
  obtain a, b from x;
  have h : a = 0  a = 1 := sorry;
  cases h;
  allGoals (subst a; sorry)
  -- allGoals (subst x; dec_trivial) -- stack overflow / timeout
}

I translated dec_trivial from what we had in Lean 3 and obtain I copied from an example somewhere in the docs:

def as_true (c : Prop) [Decidable c] : Prop :=
if c then True else False

lemma of_as_true {c : Prop} [h₁ : Decidable c] (h₂ : as_true c) : c :=
match h₁, h₂ with
| (isTrue h_c),  h₂ => h_c
| (isFalse h_c), h₂ => False.elim h₂

syntax (name := Parser.Tactic.triv) "triv" : tactic

@[tactic triv] def triv : Tactic := fun _ => closeMainGoal (mkConst `trivial)

macro "dec_trivial" : tactic => `(exact (of_as_true (by triv)))

macro "obtain " p:term " from " d:term : tactic =>
`(tactic| match $d:term with | $p:term => ?_)

So somehow, obtain seems to be breaking everything. But then if I make the numbers in the matrix small, everything is fine. Even just removing Int.natAbs in the doesnot lemma, the proof goes through... What am I missing??

Yakov Pechersky (Jul 15 2021 at 16:31):

dec_trivial is a kernel computation, so it evaluates using unary arithmetic, which will blow up with huge numbers. You want a norm_num there instead

Gabriel Ebner (Jul 15 2021 at 16:32):

kernel computation, so it evaluates using unary arithmetic

This was in Lean 3. In Lean 4, the kernel evaluation should use GMP as well.

Yakov Pechersky (Jul 15 2021 at 16:46):

Wonderful! Sorry for the FUD

Mac (Jul 15 2021 at 17:32):

Ramon Fernandez Mir Sorry, but I am having trouble reproducing this error. Here is the full code I am using:

import Lean
open Lean Elab Tactic

def as_true (c : Prop) [Decidable c] : Prop :=
  if c then True else False

theorem of_as_true {c : Prop} [h₁ : Decidable c] (h₂ : as_true c) : c :=
  match h₁, h₂ with
  | (isTrue h_c),  h₂ => h_c
  | (isFalse h_c), h₂ => False.elim h₂

syntax (name := Parser.Tactic.triv) "triv" : tactic

@[tactic Parser.Tactic.triv] def triv : Tactic :=
  fun _ => closeMainGoal (mkConst `trivial)

macro "dec_trivial" : tactic => `(exact (of_as_true (by triv)))

macro "obtain " p:term " from " d:term : tactic =>
`(tactic| match $d:term with | $p:term => ?_)

def m : (Fin 2)  (Fin 2)  Int :=
fun i j => #[#[52532234335, -234231224], #[-122223, 29338123232]][i][j]

theorem works :  (x : Fin 2), Int.natAbs (m x x)  10 ^ 80 := by {
  intros x;
  have h : x = 0  x = 1 := sorry;
  cases h;
  allGoals (subst x; dec_trivial)
}

theorem doesnot :  (x : Fin 2), Int.natAbs (m x x)  10 ^ 80 := by {
  intros x;
  obtain a, b from x;
  have h : a = 0  a = 1 := sorry;
  cases h;
  allGoals (subst a; admit)
  allGoals (subst x; dec_trivial) -- stack overflow / timeout
}

It works for me (the last line completes successfully and nothing breaks).

Mac (Jul 15 2021 at 17:35):

One a somewhat unrelated note, I do not understand why the definition of of_as_true was written to match on both h₁ and h₂ when it only really matches against is h₁. Also, the parentheses in the match seem unnecessary (but that could easily be an artifact from translating this from Lean 3).

Mac (Jul 15 2021 at 17:38):

Ah, now that I think about it. What version of Lean 4 are you using?

Mac (Jul 15 2021 at 17:38):

Have you tried testing this on the latest nightly?

Jannis Limperg (Jul 15 2021 at 18:03):

Mac said:

One a somewhat unrelated note, I do not understand why the definition of of_as_true was written to match on both h₁ and h₂ when it only really matches against is h₁. Also, the parentheses in the match seem unnecessary (but that could easily be an artifact from translating this from Lean 3).

I think this is to ensure that the case split on h1 gets propagated into the type of h2. Lean 4 has match (generalizing := true) for this afair.

Mac (Jul 15 2021 at 19:07):

Jannis Limperg said:

I think this is to ensure that the case split on h1 gets propagated into the type of h2. Lean 4 has match (generalizing := true) for this afair.

Ah. And, of I am not mistaken, generalizing := true is the default for Prop.

Mac (Jul 15 2021 at 19:09):

Regardless, in Lean 4, this definition does work:

theorem of_as_true {c : Prop} [h₁ : Decidable c] (h₂ : as_true c) : c :=
  match h₁ with
  | isTrue h_c  => h_c
  | isFalse h_c => False.elim h₂

Ramon Fernandez Mir (Jul 16 2021 at 10:21):

Mac said:

Have you tried testing this on the latest nightly?

Yes and still can't solve it...

Ramon Fernandez Mir (Jul 16 2021 at 10:22):

In general though, how can I debug an issue like this one? How can I get some sort of execution trace?

Mario Carneiro (Jul 16 2021 at 10:24):

One thing that helps is to minimize the example. Remove everything that doesn't contribute to the bug. If it has to do with kernel computation then you can probably get it down to example : ... := by dec_trivial

Mac (Jul 16 2021 at 15:22):

Ramon Fernandez Mir said:

Yes and still can't solve it...

Ah. Sorry that I can't help more since I can't reproduce it. One thing I would try is all positive or all negative numbers (especially since it might have something to do with Int.natAbs).

Ramon Fernandez Mir (Jul 16 2021 at 17:16):

Mario Carneiro said:

One thing that helps is to minimize the example. Remove everything that doesn't contribute to the bug. If it has to do with kernel computation then you can probably get it down to example : ... := by dec_trivial

So I managed to find an even simpler example that hangs:

def m : (Fin 2)  (Fin 2)  Int :=
fun i j => #[#[0, 0], #[0, 100000000]][i][j]

theorem simpler (h : 1 < 2) : Int.natAbs (m 1, h 1, h⟩)  100000000 :=
by dec_trivial

This is on a fresh project running on lean 4.0.0-nightly-2021-07-14. Also, if you make the numbers small, it works. If you replace the ⟨1, h⟩ by simply 1, it works. And removing Int.natAbs also makes it work. So quite mysterious altogether.

Mac (Jul 16 2021 at 17:36):

Yay, that worked. I can finally reproduce this! :)

Mac (Jul 16 2021 at 17:58):

I was able to get the stack overflow with this cut-down example as well:

def as_true (c : Prop) [Decidable c] : Prop :=
  if c then True else False

theorem of_as_true {c : Prop} [h₁ : Decidable c] (h₂ : as_true c) : c :=
  match h₁ with
  | isTrue h_c  => h_c
  | isFalse h_c => False.elim h₂

def n := 100000000

def m : (Fin 2)  (Fin 2)  Int :=
fun i j => #[#[0, 0], #[0, n]][i][j]

theorem simpler (h : 1 < 2) : Int.natAbs (m 1,h 1,h⟩)  n :=
of_as_true trivial -- stack overflow

Mac (Jul 16 2021 at 18:02):

The processing time increases as the order of magnitude of n increases. Thus, @Yakov Pechersky appears to be right, the kernel is for some reason doing unary arithmetic, causing the stack blow-up.

Mac (Jul 16 2021 at 18:06):

Given that this doesn't occur when Int.natAbs is absent or the ⟨1,h⟩ is replaced with 1, I would assume this has to do with the kernel not realizing that Int.natAbs (m ⟨1,h⟩ ⟨1,h⟩) is a computable Nat (and, more generally, that the less than is decidable through computation) and is thus relying on its logical representation (rather than its computable one).

Mac (Jul 16 2021 at 18:18):

Note that decide fails in with ⟨1,h⟩ but not with 1:

theorem decide1 (h : 1 < 2) : Int.natAbs (m 1,h 1,h⟩)  n :=
by decide
/--
expected type must not contain free or meta variables
  Int.natAbs (m { val := 1, isLt := h } { val := 1, isLt := h }) ≤ n
-/

-- the `h` parameter is irrelevant
theorem decide2 (h : 1 < 2) : Int.natAbs (m 1 1)  n :=
by decide -- works

Mac (Jul 16 2021 at 18:22):

However, decide still fails without Int.natAbs while the of_as_true trivial does not:

theorem decide3 (h : 1 < 2) : m 1,h 1,h  n :=
by decide
/--
expected type must not contain free or meta variables
  m { val := 1, isLt := h } { val := 1, isLt := h } ≤ Int.ofNat n
-/

theorem simpler3 (h : 1 < 2) : m 1,h 1,h  n :=
of_as_true trivial -- works

So it is clear that decide is getting caught up on the h.

Mac (Jul 16 2021 at 18:39):

My biggest confusion is thus why m ⟨1,h⟩ ⟨1,h⟩ ≤ n works (with of_as_true trivial). Based on the behavior with decide, I would expect it to break as well.

Mario Carneiro (Jul 16 2021 at 20:07):

The kernel representation can compute this though, so I would expect it to work

Mario Carneiro (Jul 16 2021 at 20:08):

That is, lean 3 #reduce would work on this even without any fancy kernel nats

Mario Carneiro (Jul 16 2021 at 20:25):

Here it is minimized some more:

def n := 30000
theorem simpler (h : True) : Nat.ble ((fun _ => n) h) n = true := rfl

(The number has been reduced so that it is long enough to be noticeable without bricking your machine)

Mario Carneiro (Jul 16 2021 at 20:26):

My guess is that at this point, lean's defeq heuristics say to unfold Nat.ble instead of the beta redex, after which point it loses the ability to take advantage of the kernel computation

Daniel Selsam (Jul 16 2021 at 20:48):

(deleted)

Mario Carneiro (Jul 16 2021 at 20:54):

The check to not reduce open terms is correct here by the way; even if it's a prop, it can be a false assumption, in which case the compiled code could go off the rails

Mario Carneiro (Jul 16 2021 at 20:55):

well, even closed terms can have that issue in the presence of axioms

Daniel Selsam (Jul 17 2021 at 01:04):

@Mario Carneiro This commit fixes the performance issue: https://github.com/dselsam/lean4/commit/6ced516e6acb8cf038b2e9a1d2c87a1a6497bef8 I may be missing something obvious but I don't see why it would be unsound -- reduce_nat only does built-in operations on terms that actually reduce to nat literals.

Mario Carneiro (Jul 17 2021 at 01:12):

I think you are right

Mario Carneiro (Jul 17 2021 at 01:14):

although that check in reduce_nat is probably a perfomance optimization since it's a lot easier to check the has_fvar bit on an expression than see whether it is nat.succ, nat.add etc

Mac (Jul 17 2021 at 01:20):

Daniel Selsam said:

reduce_nat only does built-in operations on terms that actually reduce to nat literals.

But couldn't that still be a problem? For example:

axiom h : 300 < 256
def n : UInt8 := 300,h
#eval n -- 44

n is still a nat literal -- it just happens to be the wrong nat literal. I imagine this could cause unsoundness in a more sophisticated example.

Mario Carneiro (Jul 17 2021 at 01:37):

n doesn't reduce to 44 though

Mario Carneiro (Jul 17 2021 at 01:38):

It's also not a nat literal

Mario Carneiro (Jul 17 2021 at 01:40):

n.1 has type Nat, and it whnf-reduces to a nat literal, namely 300, and it is provably equal to 44 but I don't think you can make it reduce to 44

Mac (Jul 17 2021 at 01:54):

Well, this crashes with max recursion:

axiom h : 300 < 256
def n : UInt8 := 300,h
def n' := n + 1
#reduce n'
/-
maximum recursion depth has been reached
(use `set_option maxRecDepth <num>` to increase limit)
-/

Is that expected?

Mac (Jul 17 2021 at 01:58):

Oh, apparently that just always happens:

def n : UInt8 := 0
def n' := n + 0
#reduce n' -- same error

Is this expected?

Daniel Selsam (Jul 17 2021 at 02:08):

It does not seem surprising -- calling reduce on a proof isn't usually a good idea. Note that there is a TODO in the code to allow #reduce to take additional arguments e.g. to avoid reducing proofs https://github.com/leanprover/lean4/blob/master/src/Lean/Elab/BuiltinCommand.lean#L222-L224

Mac (Jul 17 2021 at 02:11):

Ah so it is trying to reduce the isLt proof of UInt8 and that is causing the crash. Makes sense.


Last updated: Dec 20 2023 at 11:08 UTC