goplus / llgo

A Go compiler based on LLVM in order to better integrate Go with the C ecosystem including Python
Apache License 2.0
343 stars 26 forks source link

Crash with closure #774

Closed cpunion closed 1 week ago

cpunion commented 2 months ago
package main

func Async[T any](fn func(resolve func(T))) func(func(T)) {
    return func(resolve func(T)) {
        fn(resolve)
    }
}

func ReadFile(fileName string) func(func(error)) {
    return Async(func(resolve func(error)) {
        resolve(nil)
    })
}

func main() {
    a := func(resolve func(error)) {
        println("resolve 1:", resolve)
        ReadFile("2.txt")(func(err error) {
            // println("resolve 2:", resolve) // DEBUG 1
            if err != nil {
                resolve(err)
                return
            }
            println("resolve 3:", resolve)
            ReadFile("3.txt")(func(err error) {
                println("resolve 4:", resolve)
                resolve(err)
            })
        })
    }

    a(func(err error) {
        println("done", err)
    })
}

When enable DEBUG 1, it run success and print same address. But when disable DEBUG 1, printed addresses changed and it crashed.

cpunion commented 2 months ago

A simpler case without generic function also crashed:

package main

func main() {
    func(resolve func(error)) {
        func(err error) {
            if err != nil {
                resolve(err)
                return
            }
            resolve(nil)
        }(nil)
    }(func(err error) {
    })
}

Generates wrong LLVM-IR code:

define void @"main.main$1$1"(ptr %0, %"github.com/goplus/llgo/internal/runtime.iface" %1) {
_llgo_0:
  %2 = call ptr @"github.com/goplus/llgo/internal/runtime.IfaceType"(%"github.com/goplus/llgo/internal/runtime.iface" %1)
  %3 = extractvalue %"github.com/goplus/llgo/internal/runtime.iface" %1, 1
  %4 = alloca %"github.com/goplus/llgo/internal/runtime.eface", align 8
  %5 = getelementptr inbounds %"github.com/goplus/llgo/internal/runtime.eface", ptr %4, i32 0, i32 0
  store ptr %2, ptr %5, align 8
  %6 = getelementptr inbounds %"github.com/goplus/llgo/internal/runtime.eface", ptr %4, i32 0, i32 1
  store ptr %3, ptr %6, align 8
  %7 = load %"github.com/goplus/llgo/internal/runtime.eface", ptr %4, align 8
  %8 = call ptr @"github.com/goplus/llgo/internal/runtime.IfaceType"(%"github.com/goplus/llgo/internal/runtime.iface" zeroinitializer)
  %9 = alloca %"github.com/goplus/llgo/internal/runtime.eface", align 8
  %10 = getelementptr inbounds %"github.com/goplus/llgo/internal/runtime.eface", ptr %9, i32 0, i32 0
  store ptr %8, ptr %10, align 8
  %11 = getelementptr inbounds %"github.com/goplus/llgo/internal/runtime.eface", ptr %9, i32 0, i32 1
  store ptr null, ptr %11, align 8
  %12 = load %"github.com/goplus/llgo/internal/runtime.eface", ptr %9, align 8
  %13 = call i1 @"github.com/goplus/llgo/internal/runtime.EfaceEqual"(%"github.com/goplus/llgo/internal/runtime.eface" %7, %"github.com/goplus/llgo/internal/runtime.eface" %12)
  %14 = xor i1 %13, true
  br i1 %14, label %_llgo_1, label %_llgo_2

_llgo_1:                                          ; preds = %_llgo_0
  %15 = load { ptr }, ptr %0, align 8
  %16 = extractvalue { ptr } %15, 0
  %17 = load { ptr, ptr }, ptr %16, align 8
  %18 = extractvalue { ptr, ptr } %17, 1
  %19 = extractvalue { ptr, ptr } %17, 0
  call void %19(ptr %18, %"github.com/goplus/llgo/internal/runtime.iface" zeroinitializer)
  ret void

_llgo_2:                                          ; preds = %_llgo_0
  %20 = extractvalue { ptr } %15, 0
  %21 = load { ptr, ptr }, ptr %20, align 8
  %22 = extractvalue { ptr, ptr } %21, 1
  %23 = extractvalue { ptr, ptr } %21, 0
  call void %23(ptr %22, %"github.com/goplus/llgo/internal/runtime.iface" zeroinitializer)
  ret void
}

%15 is initialized in _llgo_1 (if then branch), but used in _llgo_2 (else branch).