nim-works / cps

Continuation-Passing Style for Nim 🔗

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Leak, another one

zevv opened this issue · comments

For @Clyybber

Here's a little puzzle. The program below outputs this:

done
destroyed noleak one

Now go uncomment the commented line, what do you expect it would print?

import cps, deques  


type
  C = Continuation
  
  ThingObj = object
    name: string
    
  Thing = ref ThingObj

proc `=destroy`*(t: var ThingObj) =
  echo "destroyed ", t.name

proc foo_leak(name: string): Thing {.cps:C.}=
  Thing(name: name)

proc foo_noleak(name: string): Thing =
  Thing(name: name)
                  
proc foo1() {.cps:C.} =
  let c1 = foo_noleak("noleak one") 
  # let c2 = foo_leak("leak one")       # <-- try uncommenting out this line
  echo "done"

                    
var queue: Deque[Continuation] 

block:
  queue.addLast whelp foo1() 

while queue.len > 0:
  var c = queue.popFirst
  discard c.trampoline

Heavily minimized:

type
  ThingObj = object
    name: string
    
  Thing = ref ThingObj

  C = ref object of RootObj
    fn: proc(c: C): C {.nimcall.}
    mom: C
    result: Thing
    c2: Thing
    child: C

proc `=destroy`(t: var ThingObj) =
  echo "destroyed ", t.name

proc trampoline(c: C): C =
  var c: C = c
  while not c.isNil and not c.fn.isNil:
    c = c.fn(c)
  result = c

proc foo_leak(continuation: C): C =
  echo "hey"
  continuation.result = Thing(name: "leak one")
  continuation.fn = nil
  result = continuation.mom
  #continuation.mom = nil # Break cycle; fix leak

proc call(continuation: C): Thing {.used.} =
  result =
    if continuation.fn == nil: continuation.result
    else: (trampoline continuation).call

proc finish(continuation: C): C {.nimcall.} =
  echo ["done"]
  continuation.fn = nil
  result = continuation

proc postChild(continuation: C): C {.nimcall.} =
  continuation.c2 = continuation.child.call
  continuation.fn = finish
  return continuation

proc foo1(continuation: C): C =
  continuation.fn = postChild
  continuation.child = C(fn: foo_leak, mom: continuation)
  return continuation.child

proc main =
  var c = C(fn: foo1)
  discard c.trampoline
  echo cast[int](c)
  echo cast[int](c.child.mom) # Cyclic reference to c

main()

c.child.mom pointing to c creates a cycle.

Ok, but given that I did not make the mistake of spawning the continuations in the global scope, why does this even leak when using orc?

It's an orc bug nim-lang/Nim#18421.
The cycle itself comes from this line in the minimized example continuation.child = C(fn: foo_leak, mom: continuation) which corresponds to cps environment_402654022(continuation).child_402654055 = cps environment_402653363(tail(Continuation(continuation), C(whelp_402653376("leak one")))) in the cps generated code.

Upstream bug was fixed, leaving this open though, since I believe we could get rid of the cycle, which is better than relying on the cycle collector.

How do you propose to get rid of the cycle?

In the minimized snippet zeroing out mom works (the commented out statement), but not sure if that would be correct.

Well, if we don't store the parent in the child continuation, how can we unwind the stack and return the the parent after the child has completed?

Similarly, if we don't store the child in the parent, we cannot, for example, fetch the result of the child when the parent resumes.