Zulip Chat Archive

Stream: lean4

Topic: Stack overflow in for loops?


Frédéric Dupuis (Dec 11 2022 at 20:07):

I have just failed to solve today's Advent of Code problem with a very strange issue. You can have a look at the full code here. The main loop for Part 2 looks like this:

  let modulo : UInt64 := monkeys.foldl (fun n mon => n * mon.test) 1
  let n := monkeys.size
  let mut monkeyActivity : Array Nat := Array.mkArray n 0
  for rnd in [1:numRounds+1] do
    if rnd % 100 == 0 then dbg_trace s!"Starting round {rnd}, {monkeys.foldl (fun n m => n + m.items.size) 0}"
    for m in [0:n] do
      for i in monkeys[m]!.items do
        monkeyActivity := monkeyActivity.modify m (· + 1)
        let i' := (monkeys[m]!.op i) % modulo
        if i' % monkeys[m]!.test == 0 then
          monkeys := monkeys.modify monkeys[m]!.ifTrue (fun _ => monkeys[monkeys[m]!.ifTrue]!.catchItem i')
        else
          monkeys := monkeys.modify monkeys[m]!.ifFalse (fun _ => monkeys[monkeys[m]!.ifFalse]!.catchItem i')
      monkeys := monkeys.modify m (fun _ => monkeys[m]!.flushItems)

So basically just a few nested for loops whose body should run in constant time (in numRounds). However, what happens when I run this for 10000 rounds is that it progressively gets slower and slower until I get a stack overflow at round ~9200. I have no idea how a stack gets involved here.

@Sebastian Ullrich, maybe you would have a quick answer for this? I see you wrote nearly the same code, except that in your solution the items are not inside the Monkey structure.

Sebastian Ullrich (Dec 12 2022 at 11:44):

Do you have a stack trace, say with gdb?

Gabriel Ebner (Dec 12 2022 at 18:57):

My bet would be on the dbg_trace:

#eval dbg_trace s!"{List.range 10000}"; 42 -- stack overflow

Frédéric Dupuis (Dec 12 2022 at 19:54):

Gabriel Ebner said:

My bet would be on the dbg_trace:

#eval dbg_trace s!"{List.range 10000}"; 42 -- stack overflow

Nope -- removing it doesn't change anything...

Frédéric Dupuis (Dec 12 2022 at 19:55):

Here's the stack trace from gdb (assuming I did this correctly):

#0  0x0000555558561788 in lean_inc(lean_object*) ()
#1  0x0000555558560f85 in lean_apply_1 ()
#2  0x0000555558560f90 in lean_apply_1 ()
#3  0x0000555558560f90 in lean_apply_1 ()
#4  0x0000555558560f90 in lean_apply_1 ()
#5  0x0000555558560f90 in lean_apply_1 ()
#6  0x0000555558560f90 in lean_apply_1 ()
#7  0x0000555558560f90 in lean_apply_1 ()
#8  0x0000555558560f90 in lean_apply_1 ()
#9  0x0000555558560f90 in lean_apply_1 ()
#10 0x0000555558560f90 in lean_apply_1 ()

and it just keeps going like this forever.

Frédéric Dupuis (Dec 12 2022 at 19:56):

(This is without the dbg_trace line btw.)

Gabriel Ebner (Dec 12 2022 at 20:34):

Hmm that is strange. Either way lean4#1944 for the ToString instance.

Frédéric Dupuis (Dec 12 2022 at 20:45):

Nice -- now I can claim that doing AoC is not 100% procrastination :-)

Gabriel Ebner (Dec 12 2022 at 21:01):

This is an interesting bug. :smile: Here's an MWE, you can probably spot what's going wrong:

structure Monkey where
  items : Array Nat
  op : Nat  Nat

set_option trace.compiler.ir.result true in
def Monkey.catchItem (item : Nat) (monkey : Monkey) : Monkey :=
  { monkey with items := monkey.items.push item }

def f (n : Nat) : Monkey := Id.run do
  let mut monkey : Monkey := { items := #[], op := (· + 42) }
  for i in [:n] do
    monkey := monkey.catchItem i
  return monkey

#eval (f 10000).op 0 -- stack overflow

Sebastian Ullrich (Dec 12 2022 at 22:01):

Good thing I started exactly the same way but decided to factor out the mutable field halfway through because I didn't want to deal with the nested update :grimacing: ...

James Gallicchio (Sep 18 2023 at 19:19):

Is there an issue tracking this? I ran into a similar stack overflow from for loop in my code base, on a relatively recent toolchain.

James Gallicchio (Sep 18 2023 at 20:10):

ah, nevermind -- the stack overflow was coming from filterMap not being tail recursive

James Gallicchio (Sep 18 2023 at 20:36):

plot twist, I lied -- even with Std's tailrec filterMap, I get a stackoverflow, and I have a very funky MWE :big_smile:

structure Solver (m : Type  Type v) where
  solve : [Monad m]  m Unit

def DimacsCommand (cmd : String) : Solver IO :=
  do
  let child  IO.Process.spawn {
    cmd := cmd
  }
  for _ in [0:10000] do
    IO.println "hi"
  

def mwe : IO Unit :=
  (DimacsCommand "cadical").solve

#eval mwe

This is on leanprover/lean4:nightly-2023-08-23, let me test it on stable

James Gallicchio (Sep 18 2023 at 20:39):

yep, still causes stackoverflow or bug on 4.0.0

Sebastian Ullrich (Sep 18 2023 at 20:41):

Why would you put the Monad argument in the field instead of the structure signature?

James Gallicchio (Sep 18 2023 at 20:42):

I don't remember :melt: but I remember there being a reason, because it used to be the other way. This is heavily, heavily minimized from LeanSAT.

James Gallicchio (Sep 18 2023 at 20:43):

(and the eval hangs infinitely instead of stack-overflowing if the monad argument is removed!)

James Gallicchio (Sep 18 2023 at 20:55):

although you can specialize m:

structure Solver where
  solve : Monad IO  IO Unit

def DimacsCommand (cmd : String) : Solver :=
  fun _m => do
  let _child  IO.Process.spawn {
    cmd := cmd
  }
  for _ in [0:10000] do
    IO.println "hi"
  

#eval (DimacsCommand "ls").solve inferInstance

(also put in a command that most people have...)

Sebastian Ullrich (Sep 18 2023 at 21:22):

Yes, the problem is not m but the unknown instance


Last updated: Dec 20 2023 at 11:08 UTC