koka-lang / koka

Koka language compiler and interpreter
http://koka-lang.org
Other
3.3k stars 167 forks source link

Segfault when using first-class resumption #564

Closed hflsmax closed 1 month ago

hflsmax commented 3 months ago

Apologize for not able to narrow down the issue. I implemented a scheduler using raw ctl handler, and stores the resumption in a queue. The program type checks but it segfaults. Can someone help me diagnose whether it's a compiler bug or a program bug? Or point me to the direction of how to debug.

The scheduler uses a custom dequeue data structure, which is not included in the code below. An compilable file is here: scheduler.txt

Thanks!

module main

import std/os/env
import dequeue

effect tick
  fun tick(): ()

rec effect process
   ctl yield(): ()
   ctl fork(g: () -> <process, div, exn, st<global>> ()): ()

fun scheduler(f)
  val q: ref<global, dequeue<() -> <div, st<global>|e> ()>> = ref(emptyQueue())

  fun spawn(f)
    with handler
      raw ctl yield()
        q := (!q).pushBack(fn() rcontext.resume(()))
      raw ctl fork(g)
        q := (!q).pushBack(fn() rcontext.resume(()))
        spawn(g)
    f()

  fun driver(): <st<global>, pure> ()
    with handler
      final ctl throw-exn(err) ()
    val res = (!q).popFront()
    q := snd(res)
    fst(res)()
    driver()

  spawn(f)
  driver()

fun job(): <process, pure> ()
  yield()

fun jobs(n_jobs: int): <process, tick, pure> ()
  var i: int := 0;
  while {i < n_jobs}
    fork(job)
    tick()
    i := i + 1

fun run(n_jobs : int)
  var c := 0
  with handler
    fun tick() c := c + 1
  scheduler(fn () jobs(n_jobs))
  c

fun repeat(n_jobs : int)
  fun step(i: int, acc: int)
    if i == 0 then
      acc
    else 
      val r: int = run(n_jobs)
      step(i - 1, acc + r)
  step(1, 0)

pub fun main()
  val n = get-args().head("").parse-int().default(5)
  val r = repeat(n)
  println(r)
TimWhiting commented 3 months ago

Looks like there is an error in type inference / checking. Change this:

  fun driver(): <st<global>, pure> ()
    with handler
      final ctl throw-exn(err) ()
    val res = (!q).popFront()
    q := snd(res)
    fst(res)()
    driver()

to this:

  fun driver(): <st<global>, pure|e> () // The function can have more than the effects listed
    with handler
      final ctl throw-exn(err) ()
    val res = (!q).popFront()
    q := snd(res)
    mask<exn> // Since we are calling driver in a loop, and it introduces a new handler at each loop, we need to mask `exn`, additionally, we don't really want to catch exceptions raised in the job either, so mask it for that as well.
      fst(res)()
      driver()

Alternatively you can just introduce the exception handler later above the entire driver loop, and just mask the exn for the user function.

fun spawn(f)
  ...
  fun driver(): <st<global>, pure|e> ()
    val res = (!q).popFront()
    q := snd(res)
    mask<exn>{fst(res)()}
    driver()

  spawn(f)
  with handler
    final ctl throw-exn(err) ()
  driver()
anfelor commented 3 months ago

At the start of the scheduler function, you can find this line (with forall<e> added for clarity):

val q: forall<e> ref<global, dequeue<() -> <div, st<global>|e> ()>> = ref(emptyQueue())

I am surprised that Koka accepts this, since it seems to violate the value restriction and is explicitly disallowed by the Gen rule in Figure 3 of Type Directed Compilation of Row-Typed Algebraic Effects.

hflsmax commented 3 months ago

Looks like there is an error in type inference / checking. Change this: ............ to this:

  fun driver(): <st<global>, pure|e> () // The function can have more than the effects listed
.....

Thanks for the suggestion. It works. But why is it necessary to make driver effect polymorphic?

daanx commented 1 month ago

Apologies for the late reply but it is fixed now -- the bug was in two place, a missing substitution and a missing check for polymorphic values due to annotations. Thanks!