Skip to content

Coroutines

An echo server and its client in one program, written with ppy.aio. ppy.aio is asyncio under CPython. Where the compiler lowers the coroutines, it is a native runtime: one loop per process over epoll, timers, and non-blocking sockets.

Run it

python  echo.ppy
ppy run echo.ppy
ppy emit ir echo.ppy

What it prints

python echo.ppy

42333003
# compiled coroutine here: False

ppy run echo.ppy

42333003
# compiled coroutine here: True

ppy emit ir echo.ppy

466 lines
ppyir 1
module @echo
dialect async 1
dialect core 1
attrs {ppy.libraries = ["ppy_aio"]}

private global @ppy.str.0 : buffer<u8> = "127.0.0.1"
private global @ppy.str.1 : buffer<u8> = "127.0.0.1"

func @echo_wait_and_double(%n: i64) -> future<i64> attrs {effects = ["may_raise", "sync", "time"], ppy.abi = "ppy", ppy.async = true, ppy.async.lowered = true, ppy.qualname = "echo.wait_and_double", ppy.releases_gil = true, ppy.symbol = "ppy_echo_wait_and_double"} loc("examples/37_aio/echo.ppy":5:0) {
^entry:
    %frame = async.frame_new {slots = 7} : ptr<i64>
    %0 = core.const 3 : i64
    %1 = core.ptr_offset %frame, %0 : ptr<i64>
    core.store %n, %1
    %future = async.spawn %frame {callee = @echo_wait_and_double_resume} : future<i64>
    core.ret %future
}

func @echo_echo_once(%listening: i64) -> future<i64> attrs {effects = ["alloc", "network", "read_memory", "sync", "write_memory"], ppy.abi = "ppy", ppy.async = true, ppy.async.lowered = true, ppy.qualname = "echo.echo_once", ppy.releases_gil = true, ppy.symbol = "ppy_echo_echo_once"} loc("examples/37_aio/echo.ppy":13:0) {
^entry:
    %frame = async.frame_new {slots = 17} : ptr<i64>
    %0 = core.const 3 : i64
    %1 = core.ptr_offset %frame, %0 : ptr<i64>
    core.store %listening, %1
    %future = async.spawn %frame {callee = @echo_echo_once_resume} : future<i64>
    core.ret %future
}

func @echo_send_hello(%port: i64) -> future<i64> attrs {effects = ["alloc", "network", "read_memory", "sync", "write_memory"], ppy.abi = "ppy", ppy.async = true, ppy.async.lowered = true, ppy.qualname = "echo.send_hello", ppy.releases_gil = true, ppy.symbol = "ppy_echo_send_hello"} loc("examples/37_aio/echo.ppy":22:0) {
^entry:
    %frame = async.frame_new {slots = 20} : ptr<i64>
    %0 = core.const 3 : i64
    %1 = core.ptr_offset %frame, %0 : ptr<i64>
    core.store %port, %1
    %future = async.spawn %frame {callee = @echo_send_hello_resume} : future<i64>
    core.ret %future
}

func @echo_main() -> future<i64> attrs {effects = ["alloc", "may_raise", "network", "read_memory", "sync", "time", "write_memory"], ppy.abi = "ppy", ppy.async = true, ppy.async.lowered = true, ppy.qualname = "echo.main", ppy.releases_gil = true, ppy.symbol = "ppy_echo_main"} loc("examples/37_aio/echo.ppy":35:0) {
^entry:
    %frame = async.frame_new {slots = 9} : ptr<i64>
    %future = async.spawn %frame {callee = @echo_main_resume} : future<i64>
    core.ret %future
}

func @echo_wait_and_double_resume(%frame: ptr<i64>) -> () attrs {ppy.abi = "resume", ppy.async.resume = "echo_wait_and_double", ppy.symbol = "ppy_echo_wait_and_double_resume"} loc("examples/37_aio/echo.ppy":5:0) {
^entry:
    %0 = core.const 2 : i64
    %1 = core.ptr_offset %frame, %0 : ptr<i64>
    %state = core.load %1 : i64
    %2 = core.const 1 : i64
    %3 = core.cmp.eq %state, %2 : bool
    core.cond_br %3, ^for.body4.resume2, ^dispatch3
^dispatch3:
    %4 = core.const 3 : i64
    %5 = core.ptr_offset %frame, %4 : ptr<i64>
    %6 = core.load %5 : i64
    %7 = core.const 4 : i64
    %8 = core.ptr_offset %frame, %7 : ptr<i64>
    core.store %6, %8 loc("examples/37_aio/echo.ppy":5:0)
    %9 = core.const 0 : i64 loc("examples/37_aio/echo.ppy":6:4)
    %10 = core.const 5 : i64
    %11 = core.ptr_offset %frame, %10 : ptr<i64>
    core.store %9, %11 loc("examples/37_aio/echo.ppy":6:4)
    %12 = core.const 0 : i64 loc("examples/37_aio/echo.ppy":7:4)
    %13 = core.const 4 : i64
    %14 = core.ptr_offset %frame, %13 : ptr<i64>
    %n_entry = core.load %14 : i64
    %15 = core.const 6 : i64
    %16 = core.ptr_offset %frame, %15 : ptr<i64>
    core.store %12, %16 loc("examples/37_aio/echo.ppy":7:4)
    core.br ^for.head3
^for.body4.resume2:
    %17 = core.const 5 : i64
    %18 = core.ptr_offset %frame, %17 : ptr<i64>
    %19 = core.load %18 : i64 loc("examples/37_aio/echo.ppy":9:8)
    %20 = core.const 4 : i64
    %21 = core.ptr_offset %frame, %20 : ptr<i64>
    %22 = core.load %21 : i64 loc("examples/37_aio/echo.ppy":9:8)
    %23 = core.add %19, %22 {overflow = "python"} : i64
    %24 = core.const 5 : i64
    %25 = core.ptr_offset %frame, %24 : ptr<i64>
    core.store %23, %25 loc("examples/37_aio/echo.ppy":9:8)
    %26 = core.const 6 : i64
    %27 = core.ptr_offset %frame, %26 : ptr<i64>
    %28 = core.load %27 : i64 loc("examples/37_aio/echo.ppy":9:8)
    %29 = core.const 1 : i64
    %30 = core.add %28, %29 {overflow = "python"} : i64 loc("examples/37_aio/echo.ppy":9:8)
    %31 = core.const 6 : i64
    %32 = core.ptr_offset %frame, %31 : ptr<i64>
    core.store %30, %32 loc("examples/37_aio/echo.ppy":9:8)
    core.br ^for.head3
^for.head3:
    %33 = core.const 6 : i64
    %34 = core.ptr_offset %frame, %33 : ptr<i64>
    %35 = core.load %34 : i64 loc("examples/37_aio/echo.ppy":7:4)
    %36 = core.const 2 : i64
    %37 = core.cmp.lt %35, %36 : bool loc("examples/37_aio/echo.ppy":7:4)
    core.cond_br %37, ^for.body4, ^for.end6
^for.end6:
    %38 = core.const 5 : i64
    %39 = core.ptr_offset %frame, %38 : ptr<i64>
    %40 = core.load %39 : i64 loc("examples/37_aio/echo.ppy":10:4)
    async.complete %frame, %40
    core.ret
^for.body4:
    %41 = core.const 0.005 : f64 loc("examples/37_aio/echo.ppy":8:8)
    %42 = async.sleep %41 : future<void>
    %43 = core.const 1 : i64
    %44 = core.const 2 : i64
    %45 = core.ptr_offset %frame, %44 : ptr<i64>
    core.store %43, %45
    async.suspend %frame, %42
    core.ret
}

func @echo_echo_once_resume(%frame: ptr<i64>) -> () attrs {ppy.abi = "resume", ppy.async.resume = "echo_echo_once", ppy.symbol = "ppy_echo_echo_once_resume"} loc("examples/37_aio/echo.ppy":13:0) {
^entry:
    %0 = core.const 2 : i64
    %1 = core.ptr_offset %frame, %0 : ptr<i64>
    %state = core.load %1 : i64
    %2 = core.const 1 : i64
    %3 = core.cmp.eq %state, %2 : bool
    core.cond_br %3, ^begin1.resume2, ^dispatch5
^dispatch5:
    %4 = core.const 2 : i64
    %5 = core.cmp.eq %state, %4 : bool
    core.cond_br %5, ^begin1.resume2.resume3, ^dispatch6
^dispatch6:
    %6 = core.const 3 : i64
    %7 = core.cmp.eq %state, %6 : bool
    core.cond_br %7, ^begin1.resume2.resume3.resume4, ^dispatch7
^dispatch7:
    %8 = core.const 3 : i64
    %9 = core.ptr_offset %frame, %8 : ptr<i64>
    %10 = core.load %9 : i64
    %11 = core.const 4 : i64
    %12 = core.ptr_offset %frame, %11 : ptr<i64>
    core.store %10, %12 loc("examples/37_aio/echo.ppy":13:0)
    %13 = core.const 4 : i64
    %14 = core.ptr_offset %frame, %13 : ptr<i64>
    %15 = core.load %14 : i64 loc("examples/37_aio/echo.ppy":14:4)
    %16 = core.const 4 : i64
    %17 = core.ptr_offset %frame, %16 : ptr<i64>
    %listening_entry = core.load %17 : i64 loc("examples/37_aio/echo.ppy":14:4)
    %18 = async.accept %15 : future<i64>
    %19 = core.const 1 : i64
    %20 = core.const 2 : i64
    %21 = core.ptr_offset %frame, %20 : ptr<i64>
    core.store %19, %21
    async.suspend %frame, %18
    core.ret
^begin1.resume2.resume3.resume4:
    %22 = async.result %frame : i64
    %23 = core.const 16 : i64
    %24 = core.ptr_offset %frame, %23 : ptr<i64>
    core.store %22, %24 loc("examples/37_aio/echo.ppy":17:4)
    %25 = core.const 5 : i64
    %26 = core.ptr_offset %frame, %25 : ptr<i64>
    %27 = core.load %26 : i64 loc("examples/37_aio/echo.ppy":18:4)
    async.close %27
    %28 = core.const 16 : i64
    %29 = core.ptr_offset %frame, %28 : ptr<i64>
    %30 = core.load %29 : i64 loc("examples/37_aio/echo.ppy":19:4)
    async.complete %frame, %30
    core.ret
^begin1.resume2.resume3:
    %31 = async.result %frame : i64
    %32 = core.const 15 : i64
    %33 = core.ptr_offset %frame, %32 : ptr<i64>
    core.store %31, %33 loc("examples/37_aio/echo.ppy":16:4)
    %34 = core.const 14 : i64
    %35 = core.ptr_offset %frame, %34 : ptr<i64>
    %36 = core.cast %35 : ptr<ptr<u8>>
    %37 = core.load %36 : ptr<u8> loc("examples/37_aio/echo.ppy":17:4)
    %38 = core.const 5 : i64
    %39 = core.ptr_offset %frame, %38 : ptr<i64>
    %40 = core.load %39 : i64 loc("examples/37_aio/echo.ppy":17:4)
    %41 = core.cast %37 : ptr<u8, generic, const>
    %42 = core.const 15 : i64
    %43 = core.ptr_offset %frame, %42 : ptr<i64>
    %44 = core.load %43 : i64 loc("examples/37_aio/echo.ppy":17:4)
    %45 = async.write %40, %41, %44 : future<i64>
    %46 = core.const 3 : i64
    %47 = core.const 2 : i64
    %48 = core.ptr_offset %frame, %47 : ptr<i64>
    core.store %46, %48
    async.suspend %frame, %45
    core.ret
^begin1.resume2:
    %49 = async.result %frame : i64
    %50 = core.const 5 : i64
    %51 = core.ptr_offset %frame, %50 : ptr<i64>
    core.store %49, %51 loc("examples/37_aio/echo.ppy":14:4)
    %52 = core.const 6 : i64
    %53 = core.ptr_offset %frame, %52 : ptr<i64>
    %54 = core.cast %53 : ptr<u8>
    %55 = core.const 14 : i64
    %56 = core.ptr_offset %frame, %55 : ptr<i64>
    %57 = core.cast %56 : ptr<ptr<u8>>
    core.store %54, %57 loc("examples/37_aio/echo.ppy":15:4)
    %58 = core.const 14 : i64
    %59 = core.ptr_offset %frame, %58 : ptr<i64>
    %60 = core.cast %59 : ptr<ptr<u8>>
    %61 = core.load %60 : ptr<u8> loc("examples/37_aio/echo.ppy":16:4)
    %62 = core.const 5 : i64
    %63 = core.ptr_offset %frame, %62 : ptr<i64>
    %64 = core.load %63 : i64 loc("examples/37_aio/echo.ppy":16:4)
    %65 = core.const 8 : i64
    %66 = async.read %64, %61, %65 : future<i64>
    %67 = core.const 2 : i64
    %68 = core.const 2 : i64
    %69 = core.ptr_offset %frame, %68 : ptr<i64>
    core.store %67, %69
    async.suspend %frame, %66
    core.ret
}

func @echo_send_hello_resume(%frame: ptr<i64>) -> () attrs {ppy.abi = "resume", ppy.async.resume = "echo_send_hello", ppy.symbol = "ppy_echo_send_hello_resume"} loc("examples/37_aio/echo.ppy":22:0) {
^entry:
    %0 = core.const 2 : i64
    %1 = core.ptr_offset %frame, %0 : ptr<i64>
    %state = core.load %1 : i64
    %2 = core.const 1 : i64
    %3 = core.cmp.eq %state, %2 : bool
    core.cond_br %3, ^begin1.resume8, ^dispatch11
^dispatch11:
    %4 = core.const 2 : i64
    %5 = core.cmp.eq %state, %4 : bool
    core.cond_br %5, ^begin1.ok2.ok4.ok6.resume9, ^dispatch12
^dispatch12:
    %6 = core.const 3 : i64
    %7 = core.cmp.eq %state, %6 : bool
    core.cond_br %7, ^begin1.ok2.ok4.ok6.resume9.resume10, ^dispatch13
^dispatch13:
    %8 = core.const 3 : i64
    %9 = core.ptr_offset %frame, %8 : ptr<i64>
    %10 = core.load %9 : i64
    %11 = core.const 4 : i64
    %12 = core.ptr_offset %frame, %11 : ptr<i64>
    core.store %10, %12 loc("examples/37_aio/echo.ppy":22:0)
    %13 = core.call_intrinsic {intrinsic = "ppy.string_data", symbol = "ppy.str.0"} : ptr<u8> loc("examples/37_aio/echo.ppy":23:4)
    %14 = core.cast %13 : ptr<u8, generic, const>
    %15 = core.const 9 : i64
    %16 = core.const 4 : i64
    %17 = core.ptr_offset %frame, %16 : ptr<i64>
    %18 = core.load %17 : i64 loc("examples/37_aio/echo.ppy":23:4)
    %19 = core.const 4 : i64
    %20 = core.ptr_offset %frame, %19 : ptr<i64>
    %port_entry = core.load %20 : i64 loc("examples/37_aio/echo.ppy":23:4)
    %21 = async.connect %14, %15, %18 : future<i64>
    %22 = core.const 1 : i64
    %23 = core.const 2 : i64
    %24 = core.ptr_offset %frame, %23 : ptr<i64>
    core.store %22, %24
    async.suspend %frame, %21
    core.ret
^begin1.ok2.ok4.ok6.resume9.resume10:
    %25 = async.result %frame : i64
    %26 = core.const 19 : i64
    %27 = core.ptr_offset %frame, %26 : ptr<i64>
    core.store %25, %27 loc("examples/37_aio/echo.ppy":30:4)
    %28 = core.const 5 : i64
    %29 = core.ptr_offset %frame, %28 : ptr<i64>
    %30 = core.load %29 : i64 loc("examples/37_aio/echo.ppy":31:4)
    async.close %30
    %31 = core.const 19 : i64
    %32 = core.ptr_offset %frame, %31 : ptr<i64>
    %33 = core.load %32 : i64 loc("examples/37_aio/echo.ppy":32:4)
    %34 = core.const 100 : i64
    %35 = core.mul %33, %34 {overflow = "python"} : i64
    %36 = core.const 18 : i64
    %37 = core.ptr_offset %frame, %36 : ptr<i64>
    %38 = core.cast %37 : ptr<ptr<u8>>
    %39 = core.load %38 : ptr<u8> loc("examples/37_aio/echo.ppy":32:4)
    %40 = core.const 2 : i64
    %41 = core.ptr_offset %39, %40 : ptr<u8>
    %42 = core.load %41 : u8
    %43 = core.cast %42 : i64
    %44 = core.add %35, %43 {overflow = "python"} : i64
    async.complete %frame, %44
    core.ret
^begin1.ok2.ok4.ok6.resume9:
    %45 = async.result %frame : i64
    %46 = core.const 10 : i64
    %47 = core.ptr_offset %frame, %46 : ptr<i64>
    %48 = core.cast %47 : ptr<u8>
    %49 = core.const 18 : i64
    %50 = core.ptr_offset %frame, %49 : ptr<i64>
    %51 = core.cast %50 : ptr<ptr<u8>>
    core.store %48, %51 loc("examples/37_aio/echo.ppy":29:4)
    %52 = core.const 18 : i64
    %53 = core.ptr_offset %frame, %52 : ptr<i64>
    %54 = core.cast %53 : ptr<ptr<u8>>
    %55 = core.load %54 : ptr<u8> loc("examples/37_aio/echo.ppy":30:4)
    %56 = core.const 5 : i64
    %57 = core.ptr_offset %frame, %56 : ptr<i64>
    %58 = core.load %57 : i64 loc("examples/37_aio/echo.ppy":30:4)
    %59 = core.const 8 : i64
    %60 = async.read %58, %55, %59 : future<i64>
    %61 = core.const 3 : i64
    %62 = core.const 2 : i64
    %63 = core.ptr_offset %frame, %62 : ptr<i64>
    core.store %61, %63
    async.suspend %frame, %60
    core.ret
^begin1.resume8:
    %64 = async.result %frame : i64
    %65 = core.const 5 : i64
    %66 = core.ptr_offset %frame, %65 : ptr<i64>
    core.store %64, %66 loc("examples/37_aio/echo.ppy":23:4)
    %67 = core.const 6 : i64
    %68 = core.ptr_offset %frame, %67 : ptr<i64>
    %69 = core.cast %68 : ptr<u8>
    %70 = core.const 9 : i64
    %71 = core.ptr_offset %frame, %70 : ptr<i64>
    %72 = core.cast %71 : ptr<ptr<u8>>
    core.store %69, %72 loc("examples/37_aio/echo.ppy":24:4)
    %73 = core.const 9 : i64
    %74 = core.ptr_offset %frame, %73 : ptr<i64>
    %75 = core.cast %74 : ptr<ptr<u8>>
    %76 = core.load %75 : ptr<u8> loc("examples/37_aio/echo.ppy":25:4)
    %77 = core.const 104 : u8
    core.store %77, %76 loc("examples/37_aio/echo.ppy":25:4)
    %78 = core.const 9 : i64
    %79 = core.ptr_offset %frame, %78 : ptr<i64>
    %80 = core.cast %79 : ptr<ptr<u8>>
    %81 = core.load %80 : ptr<u8> loc("examples/37_aio/echo.ppy":26:4)
    %82 = core.const 1 : i64
    %83 = core.ptr_offset %81, %82 : ptr<u8>
    %84 = core.const 105 : u8
    core.store %84, %83 loc("examples/37_aio/echo.ppy":26:4)
    %85 = core.const 9 : i64
    %86 = core.ptr_offset %frame, %85 : ptr<i64>
    %87 = core.cast %86 : ptr<ptr<u8>>
    %88 = core.load %87 : ptr<u8> loc("examples/37_aio/echo.ppy":27:4)
    %89 = core.const 2 : i64
    %90 = core.ptr_offset %88, %89 : ptr<u8>
    %91 = core.const 33 : u8
    core.store %91, %90 loc("examples/37_aio/echo.ppy":27:4)
    %92 = core.const 9 : i64
    %93 = core.ptr_offset %frame, %92 : ptr<i64>
    %94 = core.cast %93 : ptr<ptr<u8>>
    %95 = core.load %94 : ptr<u8> loc("examples/37_aio/echo.ppy":28:4)
    %96 = core.const 5 : i64
    %97 = core.ptr_offset %frame, %96 : ptr<i64>
    %98 = core.load %97 : i64 loc("examples/37_aio/echo.ppy":28:4)
    %99 = core.cast %95 : ptr<u8, generic, const>
    %100 = core.const 3 : i64
    %101 = async.write %98, %99, %100 : future<i64>
    %102 = core.const 2 : i64
    %103 = core.const 2 : i64
    %104 = core.ptr_offset %frame, %103 : ptr<i64>
    core.store %102, %104
    async.suspend %frame, %101
    core.ret
}

func @echo_main_resume(%frame: ptr<i64>) -> () attrs {ppy.abi = "resume", ppy.async.resume = "echo_main", ppy.symbol = "ppy_echo_main_resume"} loc("examples/37_aio/echo.ppy":35:0) {
^entry:
    %0 = core.const 2 : i64
    %1 = core.ptr_offset %frame, %0 : ptr<i64>
    %state = core.load %1 : i64
    %2 = core.const 1 : i64
    %3 = core.cmp.eq %state, %2 : bool
    core.cond_br %3, ^begin1.resume2, ^dispatch5
^dispatch5:
    %4 = core.const 2 : i64
    %5 = core.cmp.eq %state, %4 : bool
    core.cond_br %5, ^begin1.resume2.resume3, ^dispatch6
^dispatch6:
    %6 = core.const 3 : i64
    %7 = core.cmp.eq %state, %6 : bool
    core.cond_br %7, ^begin1.resume2.resume3.resume4, ^dispatch7
^dispatch7:
    %8 = core.const 21 : i64 loc("examples/37_aio/echo.ppy":36:4)
    %9 = core.call %8 {callee = @echo_wait_and_double} : future<i64>
    %10 = core.const 1 : i64
    %11 = core.const 2 : i64
    %12 = core.ptr_offset %frame, %11 : ptr<i64>
    core.store %10, %12
    async.suspend %frame, %9
    core.ret
^begin1.resume2.resume3.resume4:
    %13 = async.result %frame : i64
    %14 = core.const 8 : i64
    %15 = core.ptr_offset %frame, %14 : ptr<i64>
    core.store %13, %15 loc("examples/37_aio/echo.ppy":41:4)
    %16 = core.const 4 : i64
    %17 = core.ptr_offset %frame, %16 : ptr<i64>
    %18 = core.load %17 : i64 loc("examples/37_aio/echo.ppy":42:4)
    async.close %18
    %19 = core.const 3 : i64
    %20 = core.ptr_offset %frame, %19 : ptr<i64>
    %21 = core.load %20 : i64 loc("examples/37_aio/echo.ppy":43:4)
    %22 = core.const 1000000 : i64
    %23 = core.mul %21, %22 {overflow = "python"} : i64
    %24 = core.const 7 : i64
    %25 = core.ptr_offset %frame, %24 : ptr<i64>
    %26 = core.load %25 : i64 loc("examples/37_aio/echo.ppy":43:4)
    %27 = core.const 1000 : i64
    %28 = core.mul %26, %27 {overflow = "python"} : i64
    %29 = core.add %23, %28 {overflow = "python"} : i64
    %30 = core.const 8 : i64
    %31 = core.ptr_offset %frame, %30 : ptr<i64>
    %32 = core.load %31 : i64 loc("examples/37_aio/echo.ppy":43:4)
    %33 = core.add %29, %32 {overflow = "python"} : i64
    async.complete %frame, %33
    core.ret
^begin1.resume2.resume3:
    %34 = async.result %frame : i64
    %35 = core.const 7 : i64
    %36 = core.ptr_offset %frame, %35 : ptr<i64>
    core.store %34, %36 loc("examples/37_aio/echo.ppy":40:4)
    %37 = core.const 6 : i64
    %38 = core.ptr_offset %frame, %37 : ptr<i64>
    %39 = core.cast %38 : ptr<future<i64>>
    %40 = core.load %39 : future<i64> loc("examples/37_aio/echo.ppy":41:4)
    %41 = core.const 3 : i64
    %42 = core.const 2 : i64
    %43 = core.ptr_offset %frame, %42 : ptr<i64>
    core.store %41, %43
    async.suspend %frame, %40
    core.ret
^begin1.resume2:
    %44 = async.result %frame : i64
    %45 = core.const 3 : i64
    %46 = core.ptr_offset %frame, %45 : ptr<i64>
    core.store %44, %46 loc("examples/37_aio/echo.ppy":36:4)
    %47 = core.call_intrinsic {intrinsic = "ppy.string_data", symbol = "ppy.str.1"} : ptr<u8> loc("examples/37_aio/echo.ppy":37:4)
    %48 = core.cast %47 : ptr<u8, generic, const>
    %49 = core.const 9 : i64
    %50 = core.const 16 : i64
    %51 = core.const 0 : i64
    %52 = async.listen %48, %49, %51, %50 : i64
    %53 = core.const 4 : i64
    %54 = core.ptr_offset %frame, %53 : ptr<i64>
    core.store %52, %54 loc("examples/37_aio/echo.ppy":37:4)
    %55 = core.const 4 : i64
    %56 = core.ptr_offset %frame, %55 : ptr<i64>
    %57 = core.load %56 : i64 loc("examples/37_aio/echo.ppy":38:4)
    %58 = async.port %57 : i64
    %59 = core.const 5 : i64
    %60 = core.ptr_offset %frame, %59 : ptr<i64>
    core.store %58, %60 loc("examples/37_aio/echo.ppy":38:4)
    %61 = core.const 4 : i64
    %62 = core.ptr_offset %frame, %61 : ptr<i64>
    %63 = core.load %62 : i64 loc("examples/37_aio/echo.ppy":39:4)
    %64 = core.call %63 {callee = @echo_echo_once} : future<i64>
    async.start %64 loc("examples/37_aio/echo.ppy":39:4)
    %65 = core.const 6 : i64
    %66 = core.ptr_offset %frame, %65 : ptr<i64>
    %67 = core.cast %66 : ptr<future<i64>>
    core.store %64, %67 loc("examples/37_aio/echo.ppy":39:4)
    %68 = core.const 5 : i64
    %69 = core.ptr_offset %frame, %68 : ptr<i64>
    %70 = core.load %69 : i64 loc("examples/37_aio/echo.ppy":40:4)
    %71 = core.call %70 {callee = @echo_send_hello} : future<i64>
    %72 = core.const 2 : i64
    %73 = core.const 2 : i64
    %74 = core.ptr_offset %frame, %73 : ptr<i64>
    core.store %72, %74
    async.suspend %frame, %71
    core.ret
}

Sockets are integers, bytes are pointers

async def echo_once(listening: int) -> int:
    client = await aio.accept(listening)
    room = native.stack_alloc[ppy.u8](8)
    got = await aio.read(client, room, 8)
    sent = await aio.write(client, room, got)
    aio.close(client)
    return sent

aio.listen, port, accept, connect, read, write, close, and sleep are the vocabulary.

  • A socket is an int.
  • Bytes move through a native.ptr[ppy.u8].
  • Every socket operation answers a negative errno rather than raising, which is what a state machine with no exception to throw can do.

aio.spawn starts the server as a task, and aio.run drives the client to its value. The answer folds the bytes echoed, the byte read back, and the doubled sleep into one number.

How the coroutines are lowered

An async def becomes a state machine when its awaits are these operations and other coroutines of the module, over scalars and pointers, returning one scalar or nothing. lower-async splits each coroutine into a starter and a resume function, and a value read across an await lives in the frame.

Compared with asyncio and uvloop

Twenty thousand eight-byte round trips over one loopback connection (a client and an echo server in one process), in compare/: echo_bench.ppy, echo_asyncio.py, echo_uvloop.py. Milliseconds for the whole run, over five processes.

  • PPy is the coroutines as the example writes them, lowered to state machines over the native loop.
  • asyncio is the standard library's streams, as an echo server is usually written.
  • uvloop is the same program on the libuv-backed loop.
async def serve(listening: int, rounds: int) -> int:
    client = await aio.accept(listening)
    room = native.stack_alloc[ppy.u8](8)
    total = 0
    for _ in range(rounds):
        got = await aio.read(client, room, 8)
        total += await aio.write(client, room, got)
    aio.close(client)
    return total
async def serve(reader, writer):
    total = 0
    for _ in range(ROUNDS):
        data = await reader.readexactly(8)
        writer.write(data)
        await writer.drain()
        total += len(data)
    writer.close()
    await writer.wait_closed()
    return total
PPy ppy.aio asyncio streams uvloop
20000 round trips 102.52 ± 2.81 682.49 ± 26.46 153.19 ± 3.17

A round trip is four socket operations and four resumptions of a coroutine.

  • Under asyncio each of those is a Python frame, a Future, and a trip through the event loop's callback queue.
  • uvloop moves the loop and the transports into C and leaves the coroutines in Python.
  • PPy's state machines resume in native code and call read and write directly, so what remains per round trip is the syscalls.

The program pays for that in vocabulary. Sockets are integers, bytes go through pointers, and an error is a negative errno, where asyncio's streams give you bytes and exceptions.

Intel Core Ultra 9 386H; uvloop 0.22.1 on CPython 3.12.13, asyncio and PPy on CPython 3.14.5, from a checkout on a native filesystem.

Limitations

An await the compiler does not know keeps the coroutine in Python. So does a machine without the runtime (Linux and a C compiler build it once into the cache). Nothing is ever turned into a blocking call to look correct.

aio.compiled(main) says which happened here. The line that prints it starts with #, the mark for output that may differ by path.

Read on: Coroutines · The IR: the async dialect

echo.ppy is hand-written; there is no .py source and no conversion step.

37_aio/echo.ppy

import ppy
from ppy import aio, native


async def wait_and_double(n: int) -> int:
    total = 0
    for _ in range(2):
        await aio.sleep(0.005)
        total = total + n
    return total


async def echo_once(listening: int) -> int:
    client = await aio.accept(listening)
    room = native.stack_alloc[ppy.u8](8)
    got = await aio.read(client, room, 8)
    sent = await aio.write(client, room, got)
    aio.close(client)
    return sent


async def send_hello(port: int) -> int:
    fd = await aio.connect("127.0.0.1", port)
    message = native.stack_alloc[ppy.u8](3)
    native.store(message, 104)
    native.store(native.offset(message, 1), 105)
    native.store(native.offset(message, 2), 33)
    await aio.write(fd, message, 3)
    back = native.stack_alloc[ppy.u8](8)
    got = await aio.read(fd, back, 8)
    aio.close(fd)
    return got * 100 + native.load(native.offset(back, 2))


async def main() -> int:
    doubled = await wait_and_double(21)
    listening = aio.listen("127.0.0.1", 0)
    port = aio.port(listening)
    served = aio.spawn(echo_once(listening))
    got = await send_hello(port)
    sent = await served
    aio.close(listening)
    return doubled * 1000000 + got * 1000 + sent


print(aio.run(main()))
print(f"# compiled coroutine here: {aio.compiled(main)}")

Counterpart programs

The programs the comparison above measured, each written the way its tool expects. The PPy one is first.

echo_bench.ppy (PPy)
"""Twenty thousand eight-byte round trips over one loopback connection: the PPY side, the
coroutines lowered to the native loop where the runtime is present."""

import time

import ppy
from ppy import aio, native

ROUNDS = 20000


async def serve(listening: int, rounds: int) -> int:
    client = await aio.accept(listening)
    room = native.stack_alloc[ppy.u8](8)
    total = 0
    for _ in range(rounds):
        got = await aio.read(client, room, 8)
        total += await aio.write(client, room, got)
    aio.close(client)
    return total


async def ask(port: int, rounds: int) -> int:
    fd = await aio.connect("127.0.0.1", port)
    message = native.stack_alloc[ppy.u8](8)
    for i in range(8):
        native.store(native.offset(message, i), 97 + i)
    back = native.stack_alloc[ppy.u8](8)
    total = 0
    for _ in range(rounds):
        await aio.write(fd, message, 8)
        total += await aio.read(fd, back, 8)
    aio.close(fd)
    return total


async def main() -> int:
    listening = aio.listen("127.0.0.1", 0)
    port = aio.port(listening)
    served = aio.spawn(serve(listening, ROUNDS))
    got = await ask(port, ROUNDS)
    sent = await served
    aio.close(listening)
    return got + sent


started = time.perf_counter()
answer = aio.run(main())
took = (time.perf_counter() - started) * 1000.0
print(f"# {ROUNDS} round trips: {took:.2f} ms")
print(answer)
echo_asyncio.py (Python)
"""The same round trips with asyncio's streams, as the standard library writes an echo server."""

import asyncio
import time

ROUNDS = 20000


async def serve(reader, writer):
    total = 0
    for _ in range(ROUNDS):
        data = await reader.readexactly(8)
        writer.write(data)
        await writer.drain()
        total += len(data)
    writer.close()
    await writer.wait_closed()
    return total


async def main():
    served = asyncio.get_running_loop().create_future()

    async def on_client(reader, writer):
        served.set_result(await serve(reader, writer))

    server = await asyncio.start_server(on_client, "127.0.0.1", 0)
    port = server.sockets[0].getsockname()[1]
    reader, writer = await asyncio.open_connection("127.0.0.1", port)
    got = 0
    for _ in range(ROUNDS):
        writer.write(b"abcdefgh")
        await writer.drain()
        got += len(await reader.readexactly(8))
    writer.close()
    await writer.wait_closed()
    sent = await served
    server.close()
    await server.wait_closed()
    return got + sent


started = time.perf_counter()
answer = asyncio.run(main())
took = (time.perf_counter() - started) * 1000.0
print(f"# {ROUNDS} round trips: {took:.2f} ms")
print(answer)
echo_uvloop.py (Python)
"""The same round trips with asyncio's streams, on uvloop, the libuv-backed loop."""

import asyncio
import time

import uvloop

ROUNDS = 20000


async def serve(reader, writer):
    total = 0
    for _ in range(ROUNDS):
        data = await reader.readexactly(8)
        writer.write(data)
        await writer.drain()
        total += len(data)
    writer.close()
    await writer.wait_closed()
    return total


async def main():
    served = asyncio.get_running_loop().create_future()

    async def on_client(reader, writer):
        served.set_result(await serve(reader, writer))

    server = await asyncio.start_server(on_client, "127.0.0.1", 0)
    port = server.sockets[0].getsockname()[1]
    reader, writer = await asyncio.open_connection("127.0.0.1", port)
    got = 0
    for _ in range(ROUNDS):
        writer.write(b"abcdefgh")
        await writer.drain()
        got += len(await reader.readexactly(8))
    writer.close()
    await writer.wait_closed()
    sent = await served
    server.close()
    await server.wait_closed()
    return got + sent


started = time.perf_counter()
answer = uvloop.run(main())
took = (time.perf_counter() - started) * 1000.0
print(f"# {ROUNDS} round trips: {took:.2f} ms")
print(answer)

Source: examples/37_aio.