diff --git a/cl/_testdata/cpkg/in.go b/cl/_testdata/cpkg/in.go index e0bfc6a625..d2c20fd6e0 100644 --- a/cl/_testdata/cpkg/in.go +++ b/cl/_testdata/cpkg/in.go @@ -5,8 +5,12 @@ package C // CHECK-LABEL: define double @Double(double %0){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %1 = fmul double 2.000000e+00, %0 -// CHECK-NEXT: ret double %1 +// CHECK-NEXT: %1 = alloca %"{{.*}}LocalContext", align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %1, i8 0, i64 8, i1 false) +// CHECK-NEXT: %2 = call i64 @"{{.*}}EnterLocalContext"(ptr %1) +// CHECK-NEXT: %3 = fmul double 2.000000e+00, %0 +// CHECK-NEXT: call void @"{{.*}}LeaveLocalContext"(ptr %1, i64 %2) +// CHECK-NEXT: ret double %3 // CHECK-NEXT: } func Double(x float64) float64 { return 2 * x @@ -14,8 +18,12 @@ func Double(x float64) float64 { // CHECK-LABEL: define i64 @add(i64 %0, i64 %1){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %2 = call i64 @"{{.*}}.add"(i64 %0, i64 %1) -// CHECK-NEXT: ret i64 %2 +// CHECK-NEXT: %2 = alloca %"{{.*}}LocalContext", align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %2, i8 0, i64 8, i1 false) +// CHECK-NEXT: %3 = call i64 @"{{.*}}EnterLocalContext"(ptr %2) +// CHECK-NEXT: %4 = call i64 @"{{.*}}.add"(i64 %0, i64 %1) +// CHECK-NEXT: call void @"{{.*}}LeaveLocalContext"(ptr %2, i64 %3) +// CHECK-NEXT: ret i64 %4 // CHECK-NEXT: } func Xadd(a, b int) int { return add(a, b) diff --git a/cl/_testdata/vargs/in.go b/cl/_testdata/vargs/in.go index 27eef21e99..98ea773fd7 100644 --- a/cl/_testdata/vargs/in.go +++ b/cl/_testdata/vargs/in.go @@ -3,7 +3,6 @@ package main import "github.com/goplus/lib/c" -// CHECK: @0 = private unnamed_addr constant [3 x i8] c"int", align 1 // CHECK: @1 = private unnamed_addr constant [4 x i8] c"%d\0A\00", align 1 func main() { @@ -88,7 +87,8 @@ func test(a ...any) { // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %12, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %17 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %12, ptr @_llgo_int, ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %17) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/abimethod/in.go b/cl/_testgo/abimethod/in.go index c72d8dce01..b06ce6fa1f 100644 --- a/cl/_testgo/abimethod/in.go +++ b/cl/_testgo/abimethod/in.go @@ -736,7 +736,8 @@ type I2 interface { // CHECK-NEXT: br i1 %29, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %23, %"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %30 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %23, ptr @_llgo_int, ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %30) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/cgodefer/cgodefer.go b/cl/_testgo/cgodefer/cgodefer.go index 7415613677..179ea33da9 100644 --- a/cl/_testgo/cgodefer/cgodefer.go +++ b/cl/_testgo/cgodefer/cgodefer.go @@ -90,17 +90,20 @@ import "C" // CHECK-NEXT: store ptr %32, ptr %18, align 8 // CHECK-NEXT: %33 = extractvalue { ptr, i64, { ptr, ptr } } %31, 2 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.FreeDeferNode"(ptr %30) -// CHECK-NEXT: %34 = extractvalue { ptr, ptr } %33, 1 -// CHECK-NEXT: %35 = extractvalue { ptr, ptr } %33, 0 -// CHECK-NEXT: call void %35(ptr %34) +// CHECK-NEXT: %34 = extractvalue { ptr, ptr } %33, 0 +// CHECK-NEXT: %35 = call ptr @"{{.*}}/runtime/internal/runtime.StartRecoverFrame"(ptr %34) +// CHECK-NEXT: %36 = extractvalue { ptr, ptr } %33, 1 +// CHECK-NEXT: %37 = extractvalue { ptr, ptr } %33, 0 +// CHECK-NEXT: call void %37(ptr %36) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.EndRecoverFrame"(ptr %35) // CHECK-NEXT: br label %_llgo_8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_7, %_llgo_2 -// CHECK-NEXT: %36 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %10, align 8 -// CHECK-NEXT: %37 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %36, 2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %37) -// CHECK-NEXT: %38 = load ptr, ptr %17, align 8 -// CHECK-NEXT: indirectbr ptr %38, [label %_llgo_3, label %_llgo_6] +// CHECK-NEXT: %38 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %10, align 8 +// CHECK-NEXT: %39 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %38, 2 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %39) +// CHECK-NEXT: %40 = load ptr, ptr %17, align 8 +// CHECK-NEXT: indirectbr ptr %40, [label %_llgo_3, label %_llgo_6] // CHECK-NEXT: } func main() { // CHECK-LABEL: define { ptr, ptr } @"{{.*}}/cl/_testgo/cgodefer.main$1"(ptr %0){{.*}} { diff --git a/cl/_testgo/closureall/in.go b/cl/_testgo/closureall/in.go index a9dedf95bb..0a0d89d585 100644 --- a/cl/_testgo/closureall/in.go +++ b/cl/_testgo/closureall/in.go @@ -7,8 +7,6 @@ import "github.com/goplus/lib/c" // CHECK: @0 = private unnamed_addr constant [46 x i8] c"{{.*}}/cl/_testgo/closureall.S", align 1 // CHECK: @1 = private unnamed_addr constant [3 x i8] c"Inc", align 1 -// CHECK: @7 = private unnamed_addr constant [3 x i8] c"Add", align 1 -// CHECK: @9 = private unnamed_addr constant [23 x i8] c"interface{Add(int) int}", align 1 //go:linkname cSqrt C.sqrt func cSqrt(x c.Double) c.Double @@ -173,7 +171,8 @@ func makeWithFree(base int) Fn { // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %21, %"{{.*}}/runtime/internal/runtime.String" { ptr @9, i64 23 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @7, i64 3 }) +// CHECK-NEXT: %32 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %21, ptr @"_llgo_iface$VdBKYV8-gcMjZtZfcf-u2oKoj9Lu3VXwuG8TGCW2S4A", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %32) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/genericembediface/in.go b/cl/_testgo/genericembediface/in.go index c83bc68087..6c4294bc1b 100644 --- a/cl/_testgo/genericembediface/in.go +++ b/cl/_testgo/genericembediface/in.go @@ -7,10 +7,9 @@ import ( // CHECK: {{^}}@2 = private unnamed_addr constant [20 x i8] c"ServerReflectionInfo", align 1{{$}} // CHECK: {{^}}@5 = private unnamed_addr constant [7 x i8] c"Context", align 1{{$}} -// CHECK: {{^}}@11 = private unnamed_addr constant [68 x i8] c"{{.*}}/cl/_testgo/genericembediface.ReflectionServer", align 1{{$}} -// CHECK: {{^}}@19 = private unnamed_addr constant [4 x i8] c"pass", align 1{{$}} -// CHECK: {{^}}@20 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/genericembediface.server", align 1{{$}} -// CHECK: {{^}}@21 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/genericembediface.stream", align 1{{$}} +// CHECK: {{^}}@18 = private unnamed_addr constant [4 x i8] c"pass", align 1{{$}} +// CHECK: {{^}}@19 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/genericembediface.server", align 1{{$}} +// CHECK: {{^}}@20 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/genericembediface.stream", align 1{{$}} type Request struct{} type Response struct{} @@ -48,7 +47,8 @@ type ReflectionServer interface { // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %21 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @11, i64 68 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 20 }) +// CHECK-NEXT: %22 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %2, ptr @"_llgo_{{.*}}/cl/_testgo/genericembediface.ReflectionServer", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %22) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -93,7 +93,7 @@ func (stream) Context() string { // CHECK-NEXT: %4 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" undef, ptr %3, 0 // CHECK-NEXT: %5 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" %4, ptr %2, 1 // CHECK-NEXT: %6 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/genericembediface.handler"(%"{{.*}}/runtime/internal/runtime.eface" %1, %"{{.*}}/runtime/internal/runtime.iface" %5) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @19, i64 4 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @18, i64 4 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-NEXT: } @@ -111,7 +111,7 @@ func main() { // CHECK-LABEL: define %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/genericembediface.(*server).ServerReflectionInfo"(ptr %0, %"{{.*}}/runtime/internal/runtime.iface" %1){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %2 = icmp eq ptr %0, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @20, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 20 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @19, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 20 }) // CHECK-NEXT: %3 = icmp eq ptr %0, null // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.AssertNilDeref"(i1 %3) // CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/genericembediface.server.ServerReflectionInfo"(%"{{.*}}/cl/_testgo/genericembediface.server" zeroinitializer, %"{{.*}}/runtime/internal/runtime.iface" %1) @@ -126,7 +126,7 @@ func main() { // CHECK-LABEL: define %"{{.*}}/runtime/internal/runtime.String" @"{{.*}}/cl/_testgo/genericembediface.(*stream).Context"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %1 = icmp eq ptr %0, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @21, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 7 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @20, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 7 }) // CHECK-NEXT: %2 = icmp eq ptr %0, null // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.AssertNilDeref"(i1 %2) // CHECK-NEXT: %3 = call %"{{.*}}/runtime/internal/runtime.String" @"{{.*}}/cl/_testgo/genericembediface.stream.Context"(%"{{.*}}/cl/_testgo/genericembediface.stream" zeroinitializer) diff --git a/cl/_testgo/goroutine/in.go b/cl/_testgo/goroutine/in.go index 84d3a6c39c..ba64a06864 100644 --- a/cl/_testgo/goroutine/in.go +++ b/cl/_testgo/goroutine/in.go @@ -40,20 +40,28 @@ func main() { // CHECK-LABEL: define ptr @"{{.*}}goroutine._llgo_routine$1"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %1 = load { %"{{.*}}String" }, ptr %0, align 8 -// CHECK-NEXT: %2 = extractvalue { %"{{.*}}String" } %1, 0 +// CHECK-NEXT: %1 = alloca %"{{.*}}LocalContext", align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %1, i8 0, i64 8, i1 false) +// CHECK-NEXT: %2 = call i64 @"{{.*}}EnterLocalContext"(ptr %1) +// CHECK-NEXT: %3 = load { %"{{.*}}String" }, ptr %0, align 8 +// CHECK-NEXT: %4 = extractvalue { %"{{.*}}String" } %3, 0 // CHECK-NEXT: call void @"{{.*}}FreeRoot"(ptr %0) -// CHECK-NEXT: call void @"{{.*}}PrintString"(%"{{.*}}String" %2) +// CHECK-NEXT: call void @"{{.*}}PrintString"(%"{{.*}}String" %4) // CHECK-NEXT: call void @"{{.*}}PrintByte"(i8 10) +// CHECK-NEXT: call void @"{{.*}}LeaveLocalContext"(ptr %1, i64 %2) // CHECK-NEXT: ret ptr null // CHECK-LABEL: define ptr @"{{.*}}goroutine._llgo_routine$2"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %1 = load { { ptr, ptr }, %"{{.*}}String" }, ptr %0, align 8 -// CHECK-NEXT: %2 = extractvalue { { ptr, ptr }, %"{{.*}}String" } %1, 0 -// CHECK-NEXT: %3 = extractvalue { { ptr, ptr }, %"{{.*}}String" } %1, 1 +// CHECK-NEXT: %1 = alloca %"{{.*}}LocalContext", align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %1, i8 0, i64 8, i1 false) +// CHECK-NEXT: %2 = call i64 @"{{.*}}EnterLocalContext"(ptr %1) +// CHECK-NEXT: %3 = load { { ptr, ptr }, %"{{.*}}String" }, ptr %0, align 8 +// CHECK-NEXT: %4 = extractvalue { { ptr, ptr }, %"{{.*}}String" } %3, 0 +// CHECK-NEXT: %5 = extractvalue { { ptr, ptr }, %"{{.*}}String" } %3, 1 // CHECK-NEXT: call void @"{{.*}}FreeRoot"(ptr %0) -// CHECK-NEXT: %4 = extractvalue { ptr, ptr } %2, 1 -// CHECK-NEXT: %5 = extractvalue { ptr, ptr } %2, 0 -// CHECK-NEXT: call void %5(ptr %4, %"{{.*}}String" %3) +// CHECK-NEXT: %6 = extractvalue { ptr, ptr } %4, 1 +// CHECK-NEXT: %7 = extractvalue { ptr, ptr } %4, 0 +// CHECK-NEXT: call void %7(ptr %6, %"{{.*}}String" %5) +// CHECK-NEXT: call void @"{{.*}}LeaveLocalContext"(ptr %1, i64 %2) // CHECK-NEXT: ret ptr null diff --git a/cl/_testgo/ifaceprom/in.go b/cl/_testgo/ifaceprom/in.go index b84e4a3a35..96f73586fd 100644 --- a/cl/_testgo/ifaceprom/in.go +++ b/cl/_testgo/ifaceprom/in.go @@ -8,8 +8,7 @@ package main // CHECK: @0 = private unnamed_addr constant [3 x i8] c"two", align 1 // CHECK: @1 = private unnamed_addr constant [48 x i8] c"{{.*}}/cl/_testgo/ifaceprom.impl", align 1 // CHECK: @2 = private unnamed_addr constant [3 x i8] c"one", align 1 -// CHECK: @13 = private unnamed_addr constant [45 x i8] c"{{.*}}/cl/_testgo/ifaceprom.I", align 1 -// CHECK: @14 = private unnamed_addr constant [4 x i8] c"pass", align 1 +// CHECK: @13 = private unnamed_addr constant [4 x i8] c"pass", align 1 type I interface { one() int @@ -256,7 +255,7 @@ func main() { // CHECK-EMPTY: // CHECK-NEXT: _llgo_7: ; preds = %_llgo_19 // CHECK-NEXT: %44 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 8) -// CHECK-NEXT: store i64 %100, ptr %44, align 8 +// CHECK-NEXT: store i64 %101, ptr %44, align 8 // CHECK-NEXT: %45 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_int, ptr undef }, ptr %44, 1 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %45) // CHECK-NEXT: unreachable @@ -316,7 +315,7 @@ func main() { // CHECK-EMPTY: // CHECK-NEXT: _llgo_13: ; preds = %_llgo_21 // CHECK-NEXT: %80 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" %107, ptr %80, align 8 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" %109, ptr %80, align 8 // CHECK-NEXT: %81 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %80, 1 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %81) // CHECK-NEXT: unreachable @@ -330,13 +329,13 @@ func main() { // CHECK-EMPTY: // CHECK-NEXT: _llgo_15: ; preds = %_llgo_23 // CHECK-NEXT: %86 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" %115, ptr %86, align 8 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" %118, ptr %86, align 8 // CHECK-NEXT: %87 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %86, 1 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %87) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_16: ; preds = %_llgo_23 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @14, i64 4 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 4 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-EMPTY: @@ -352,54 +351,58 @@ func main() { // CHECK-NEXT: br i1 %94, label %_llgo_5, label %_llgo_6 // CHECK-EMPTY: // CHECK-NEXT: _llgo_18: ; preds = %_llgo_4 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %36, %"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 45 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }) +// CHECK-NEXT: %95 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %36, ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %95) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_19: ; preds = %_llgo_6 -// CHECK-NEXT: %95 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: %96 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %95, i32 0, i32 0 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %41, ptr %96, align 8 -// CHECK-NEXT: %97 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.one$bound", ptr undef }, ptr %95, 1 -// CHECK-NEXT: %98 = extractvalue { ptr, ptr } %97, 1 -// CHECK-NEXT: %99 = extractvalue { ptr, ptr } %97, 0 -// CHECK-NEXT: %100 = call i64 %99(ptr %98) -// CHECK-NEXT: %101 = icmp ne i64 %100, 1 -// CHECK-NEXT: br i1 %101, label %_llgo_7, label %_llgo_8 +// CHECK-NEXT: %96 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: %97 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %96, i32 0, i32 0 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %41, ptr %97, align 8 +// CHECK-NEXT: %98 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.one$bound", ptr undef }, ptr %96, 1 +// CHECK-NEXT: %99 = extractvalue { ptr, ptr } %98, 1 +// CHECK-NEXT: %100 = extractvalue { ptr, ptr } %98, 0 +// CHECK-NEXT: %101 = call i64 %100(ptr %99) +// CHECK-NEXT: %102 = icmp ne i64 %101, 1 +// CHECK-NEXT: br i1 %102, label %_llgo_7, label %_llgo_8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_20: ; preds = %_llgo_6 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %42, %"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 45 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }) +// CHECK-NEXT: %103 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %42, ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %103) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_21: ; preds = %_llgo_12 -// CHECK-NEXT: %102 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: %103 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %102, i32 0, i32 0 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %77, ptr %103, align 8 -// CHECK-NEXT: %104 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.two$bound", ptr undef }, ptr %102, 1 -// CHECK-NEXT: %105 = extractvalue { ptr, ptr } %104, 1 -// CHECK-NEXT: %106 = extractvalue { ptr, ptr } %104, 0 -// CHECK-NEXT: %107 = call %"{{.*}}/runtime/internal/runtime.String" %106(ptr %105) -// CHECK-NEXT: %108 = call i1 @"{{.*}}/runtime/internal/runtime.StringEqual"(%"{{.*}}/runtime/internal/runtime.String" %107, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }) -// CHECK-NEXT: %109 = xor i1 %108, true -// CHECK-NEXT: br i1 %109, label %_llgo_13, label %_llgo_14 +// CHECK-NEXT: %104 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: %105 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %104, i32 0, i32 0 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %77, ptr %105, align 8 +// CHECK-NEXT: %106 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.two$bound", ptr undef }, ptr %104, 1 +// CHECK-NEXT: %107 = extractvalue { ptr, ptr } %106, 1 +// CHECK-NEXT: %108 = extractvalue { ptr, ptr } %106, 0 +// CHECK-NEXT: %109 = call %"{{.*}}/runtime/internal/runtime.String" %108(ptr %107) +// CHECK-NEXT: %110 = call i1 @"{{.*}}/runtime/internal/runtime.StringEqual"(%"{{.*}}/runtime/internal/runtime.String" %109, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }) +// CHECK-NEXT: %111 = xor i1 %110, true +// CHECK-NEXT: br i1 %111, label %_llgo_13, label %_llgo_14 // CHECK-EMPTY: // CHECK-NEXT: _llgo_22: ; preds = %_llgo_12 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %78, %"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 45 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }) +// CHECK-NEXT: %112 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %78, ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %112) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_23: ; preds = %_llgo_14 -// CHECK-NEXT: %110 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: %111 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %110, i32 0, i32 0 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %83, ptr %111, align 8 -// CHECK-NEXT: %112 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.two$bound", ptr undef }, ptr %110, 1 -// CHECK-NEXT: %113 = extractvalue { ptr, ptr } %112, 1 -// CHECK-NEXT: %114 = extractvalue { ptr, ptr } %112, 0 -// CHECK-NEXT: %115 = call %"{{.*}}/runtime/internal/runtime.String" %114(ptr %113) -// CHECK-NEXT: %116 = call i1 @"{{.*}}/runtime/internal/runtime.StringEqual"(%"{{.*}}/runtime/internal/runtime.String" %115, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }) -// CHECK-NEXT: %117 = xor i1 %116, true -// CHECK-NEXT: br i1 %117, label %_llgo_15, label %_llgo_16 +// CHECK-NEXT: %113 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: %114 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %113, i32 0, i32 0 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %83, ptr %114, align 8 +// CHECK-NEXT: %115 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.two$bound", ptr undef }, ptr %113, 1 +// CHECK-NEXT: %116 = extractvalue { ptr, ptr } %115, 1 +// CHECK-NEXT: %117 = extractvalue { ptr, ptr } %115, 0 +// CHECK-NEXT: %118 = call %"{{.*}}/runtime/internal/runtime.String" %117(ptr %116) +// CHECK-NEXT: %119 = call i1 @"{{.*}}/runtime/internal/runtime.StringEqual"(%"{{.*}}/runtime/internal/runtime.String" %118, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }) +// CHECK-NEXT: %120 = xor i1 %119, true +// CHECK-NEXT: br i1 %120, label %_llgo_15, label %_llgo_16 // CHECK-EMPTY: // CHECK-NEXT: _llgo_24: ; preds = %_llgo_14 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %84, %"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 45 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }) +// CHECK-NEXT: %121 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %84, ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %121) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/invoke/in.go b/cl/_testgo/invoke/in.go index 6c7f8377f7..3d75637de1 100644 --- a/cl/_testgo/invoke/in.go +++ b/cl/_testgo/invoke/in.go @@ -17,9 +17,6 @@ package main // CHECK: {{^}}@13 = private unnamed_addr constant [43 x i8] c"{{.*}}/cl/_testgo/invoke.T6", align 1{{$}} // CHECK: {{^}}@14 = private unnamed_addr constant [5 x i8] c"hello", align 1{{$}} // CHECK: {{^}}@36 = private unnamed_addr constant [5 x i8] c"world", align 1{{$}} -// CHECK: {{^}}@38 = private unnamed_addr constant [42 x i8] c"{{.*}}/cl/_testgo/invoke.I", align 1{{$}} -// CHECK: {{^}}@40 = private unnamed_addr constant [3 x i8] c"any", align 1{{$}} -// CHECK: {{^}}@41 = private unnamed_addr constant [23 x i8] c"interface{Invoke() int}", align 1{{$}} type T struct { s string @@ -445,28 +442,31 @@ type M interface { // CHECK-NEXT: br i1 %85, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %78, %"{{.*}}/runtime/internal/runtime.String" { ptr @38, i64 42 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 6 }) +// CHECK-NEXT: %86 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %78, ptr @"_llgo_{{.*}}/cl/_testgo/invoke.I", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %86) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_1 -// CHECK-NEXT: %86 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %77, 0 -// CHECK-NEXT: %87 = call i1 @"{{.*}}/runtime/internal/runtime.Implements"(ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr %86) -// CHECK-NEXT: br i1 %87, label %_llgo_5, label %_llgo_6 +// CHECK-NEXT: %87 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %77, 0 +// CHECK-NEXT: %88 = call i1 @"{{.*}}/runtime/internal/runtime.Implements"(ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr %87) +// CHECK-NEXT: br i1 %88, label %_llgo_5, label %_llgo_6 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %84, %"{{.*}}/runtime/internal/runtime.String" { ptr @40, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %89 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %84, ptr @_llgo_any, ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %89) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_3 -// CHECK-NEXT: %88 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %77, 1 -// CHECK-NEXT: %89 = call ptr @"{{.*}}/runtime/internal/runtime.NewItab"(ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr %86) -// CHECK-NEXT: %90 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" undef, ptr %89, 0 -// CHECK-NEXT: %91 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" %90, ptr %88, 1 -// CHECK-NEXT: call void @"{{.*}}/cl/_testgo/invoke.invoke"(%"{{.*}}/runtime/internal/runtime.iface" %91) +// CHECK-NEXT: %90 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %77, 1 +// CHECK-NEXT: %91 = call ptr @"{{.*}}/runtime/internal/runtime.NewItab"(ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr %87) +// CHECK-NEXT: %92 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" undef, ptr %91, 0 +// CHECK-NEXT: %93 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" %92, ptr %90, 1 +// CHECK-NEXT: call void @"{{.*}}/cl/_testgo/invoke.invoke"(%"{{.*}}/runtime/internal/runtime.iface" %93) // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_6: ; preds = %_llgo_3 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %86, %"{{.*}}/runtime/internal/runtime.String" { ptr @41, i64 23 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 6 }) +// CHECK-NEXT: %94 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %87, ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %94) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/localitycodegen/in.go b/cl/_testgo/localitycodegen/in.go new file mode 100644 index 0000000000..b200a72584 --- /dev/null +++ b/cl/_testgo/localitycodegen/in.go @@ -0,0 +1,77 @@ +// LITTEST +package main + +// CHECK-DAG: @"{{.*}}localitycodegen.scalar" = thread_local global i64 0 +// CHECK-DAG: @"{{.*}}localitycodegen.__llgo_local_cache" = thread_local global i64 0 +// CHECK-DAG: @"{{.*}}localitycodegen.__llgo_tls_init$guard" = thread_local global i8 0 +// CHECK-DAG: @"{{.*}}localitycodegen.__llgo_tls_init$failure_cache" = thread_local global i64 0 +// CHECK-NOT: RegisterLocalRoot +// CHECK-NOT: localitycodegen.pointer" = thread_local +// CHECK-NOT: localitycodegen.initialized" = thread_local + +// CHECK-LABEL: define ptr @"{{.*}}localitycodegen.__llgo_local_block"() +// CHECK: load i64, ptr @"{{.*}}localitycodegen.__llgo_local_cache" +// CHECK: icmp ne i64 +// CHECK: ret ptr +// CHECK: call ptr @"{{.*}}runtime.LocalPackage"(ptr @"{{.*}}localitycodegen.__llgo_local_cache", i64 16, i64 8) +// CHECK: ret ptr + +// CHECK-LABEL: define void @"{{.*}}localitycodegen.__llgo_tls_init"() +// CHECK: call void @"{{.*}}localitycodegen.__llgo_local_init_0"() + +// CHECK-LABEL: define void @"{{.*}}localitycodegen.__llgo_tls_init$ensure"() +// CHECK: load i8, ptr +// CHECK: call void @"{{.*}}runtime.EnsureLocalInitializer"(ptr @"{{.*}}localitycodegen.__llgo_tls_init$guard", ptr @"{{.*}}localitycodegen.__llgo_tls_init$failure_cache" + +// CHECK-LABEL: define ptr @{{"?ExportedLocality"?}}() +// CHECK: call i64 @"{{.*}}EnterLocalContext" +// CHECK: call ptr @"{{.*}}localitycodegen.__llgo_local_block"() +// CHECK: call void @"{{.*}}LeaveLocalContext" +// CHECK: ret ptr + +// CHECK-LABEL: define void @"{{.*}}localitycodegen.init"() +// CHECK: store i8 2, ptr +// CHECK: call ptr @"{{.*}}localitycodegen.newPointer"() +// CHECK: call void @"{{.*}}localitycodegen.__llgo_tls_init$ensure"() +// CHECK: call ptr @"{{.*}}localitycodegen.__llgo_local_block"() + +// CHECK-LABEL: define { i64, ptr, ptr } @"{{.*}}localitycodegen.values"() +// CHECK: call void @"{{.*}}localitycodegen.__llgo_tls_init$ensure"() +// CHECK: load i64, ptr @"{{.*}}localitycodegen.scalar" +// CHECK: call ptr @"{{.*}}localitycodegen.__llgo_local_block"() +// CHECK: load ptr, ptr +// CHECK: load ptr, ptr + +// CHECK-LABEL: define ptr @"{{.*}}localitycodegen._llgo_routine$1"(ptr %0) +// CHECK: alloca %"{{.*}}LocalContext", align 8 +// CHECK: call i64 @"{{.*}}EnterLocalContext" +// CHECK: call void @"{{.*}}LeaveLocalContext" + +var backing int + +func newPointer() *int { + return &backing +} + +//llgo:tls +var scalar int + +//llgo:gls +var pointer *int + +//llgo:tls +var initialized = newPointer() + +func values() (int, *int, *int) { + return scalar, pointer, initialized +} + +//export ExportedLocality +func ExportedLocality() *int { + return pointer +} + +func main() { + _, _, _ = values() + go values() +} diff --git a/cl/_testgo/reader/in.go b/cl/_testgo/reader/in.go index 5aa0a21155..b80ee91a76 100644 --- a/cl/_testgo/reader/in.go +++ b/cl/_testgo/reader/in.go @@ -11,15 +11,14 @@ import ( // CHECK: @29 = private unnamed_addr constant [11 x i8] c"short write", align 1 // CHECK: @30 = private unnamed_addr constant [11 x i8] c"hello world", align 1 // CHECK: @53 = private unnamed_addr constant [50 x i8] c"{{.*}}/cl/_testgo/reader.nopCloser", align 1 -// CHECK: @54 = private unnamed_addr constant [49 x i8] c"{{.*}}/cl/_testgo/reader.WriterTo", align 1 -// CHECK: @55 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/reader.nopCloserWriterTo", align 1 -// CHECK: @56 = private unnamed_addr constant [37 x i8] c"stringsReader.ReadAt: negative offset", align 1 -// CHECK: @57 = private unnamed_addr constant [34 x i8] c"stringsReader.Seek: invalid whence", align 1 -// CHECK: @58 = private unnamed_addr constant [37 x i8] c"stringsReader.Seek: negative position", align 1 -// CHECK: @59 = private unnamed_addr constant [48 x i8] c"stringsReader.UnreadByte: at beginning of string", align 1 -// CHECK: @60 = private unnamed_addr constant [49 x i8] c"strings.Reader.UnreadRune: at beginning of string", align 1 -// CHECK: @61 = private unnamed_addr constant [62 x i8] c"strings.Reader.UnreadRune: previous operation was not ReadRune", align 1 -// CHECK: @62 = private unnamed_addr constant [48 x i8] c"stringsReader.WriteTo: invalid WriteString count", align 1 +// CHECK: @54 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/reader.nopCloserWriterTo", align 1 +// CHECK: @55 = private unnamed_addr constant [37 x i8] c"stringsReader.ReadAt: negative offset", align 1 +// CHECK: @56 = private unnamed_addr constant [34 x i8] c"stringsReader.Seek: invalid whence", align 1 +// CHECK: @57 = private unnamed_addr constant [37 x i8] c"stringsReader.Seek: negative position", align 1 +// CHECK: @58 = private unnamed_addr constant [48 x i8] c"stringsReader.UnreadByte: at beginning of string", align 1 +// CHECK: @59 = private unnamed_addr constant [49 x i8] c"strings.Reader.UnreadRune: at beginning of string", align 1 +// CHECK: @60 = private unnamed_addr constant [62 x i8] c"strings.Reader.UnreadRune: previous operation was not ReadRune", align 1 +// CHECK: @61 = private unnamed_addr constant [48 x i8] c"stringsReader.WriteTo: invalid WriteString count", align 1 type Reader interface { Read(p []byte) (n int, err error) @@ -689,14 +688,15 @@ func main() { // CHECK-NEXT: ret { i64, %"{{.*}}/runtime/internal/runtime.iface" } %23 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %5, %"{{.*}}/runtime/internal/runtime.String" { ptr @54, i64 49 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 7 }) +// CHECK-NEXT: %24 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %5, ptr @"_llgo_{{.*}}/cl/_testgo/reader.WriterTo", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %24) // CHECK-NEXT: unreachable // CHECK-NEXT: } // CHECK-LABEL: define %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.(*nopCloserWriterTo).Close"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %1 = icmp eq ptr %0, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @55, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @17, i64 5 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @54, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @17, i64 5 }) // CHECK-NEXT: %2 = load %"{{.*}}/cl/_testgo/reader.nopCloserWriterTo", ptr %0, align 8 // CHECK-NEXT: %3 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.nopCloserWriterTo.Close"(%"{{.*}}/cl/_testgo/reader.nopCloserWriterTo" %2) // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %3 @@ -725,7 +725,7 @@ func main() { // CHECK-LABEL: define { i64, %"{{.*}}/runtime/internal/runtime.iface" } @"{{.*}}/cl/_testgo/reader.(*nopCloserWriterTo).WriteTo"(ptr %0, %"{{.*}}/runtime/internal/runtime.iface" %1){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %2 = icmp eq ptr %0, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @55, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 7 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @54, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 7 }) // CHECK-NEXT: %3 = load %"{{.*}}/cl/_testgo/reader.nopCloserWriterTo", ptr %0, align 8 // CHECK-NEXT: %4 = call { i64, %"{{.*}}/runtime/internal/runtime.iface" } @"{{.*}}/cl/_testgo/reader.nopCloserWriterTo.WriteTo"(%"{{.*}}/cl/_testgo/reader.nopCloserWriterTo" %3, %"{{.*}}/runtime/internal/runtime.iface" %1) // CHECK-NEXT: %5 = extractvalue { i64, %"{{.*}}/runtime/internal/runtime.iface" } %4, 0 @@ -801,7 +801,7 @@ func main() { // CHECK-NEXT: br i1 %3, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 -// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @56, i64 37 }) +// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @55, i64 37 }) // CHECK-NEXT: %5 = insertvalue { i64, %"{{.*}}/runtime/internal/runtime.iface" } { i64 0, %"{{.*}}/runtime/internal/runtime.iface" undef }, %"{{.*}}/runtime/internal/runtime.iface" %4, 1 // CHECK-NEXT: ret { i64, %"{{.*}}/runtime/internal/runtime.iface" } %5 // CHECK-EMPTY: @@ -987,12 +987,12 @@ func main() { // CHECK-NEXT: br i1 %15, label %_llgo_5, label %_llgo_7 // CHECK-EMPTY: // CHECK-NEXT: _llgo_7: ; preds = %_llgo_6 -// CHECK-NEXT: %16 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @57, i64 34 }) +// CHECK-NEXT: %16 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @56, i64 34 }) // CHECK-NEXT: %17 = insertvalue { i64, %"{{.*}}/runtime/internal/runtime.iface" } { i64 0, %"{{.*}}/runtime/internal/runtime.iface" undef }, %"{{.*}}/runtime/internal/runtime.iface" %16, 1 // CHECK-NEXT: ret { i64, %"{{.*}}/runtime/internal/runtime.iface" } %17 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_1 -// CHECK-NEXT: %18 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @58, i64 37 }) +// CHECK-NEXT: %18 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @57, i64 37 }) // CHECK-NEXT: %19 = insertvalue { i64, %"{{.*}}/runtime/internal/runtime.iface" } { i64 0, %"{{.*}}/runtime/internal/runtime.iface" undef }, %"{{.*}}/runtime/internal/runtime.iface" %18, 1 // CHECK-NEXT: ret { i64, %"{{.*}}/runtime/internal/runtime.iface" } %19 // CHECK-EMPTY: @@ -1020,7 +1020,7 @@ func main() { // CHECK-NEXT: br i1 %3, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 -// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @59, i64 48 }) +// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @58, i64 48 }) // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 @@ -1042,7 +1042,7 @@ func main() { // CHECK-NEXT: br i1 %3, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 -// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @60, i64 49 }) +// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @59, i64 49 }) // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 @@ -1052,7 +1052,7 @@ func main() { // CHECK-NEXT: br i1 %7, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_2 -// CHECK-NEXT: %8 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @61, i64 62 }) +// CHECK-NEXT: %8 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @60, i64 62 }) // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_2 @@ -1096,7 +1096,7 @@ func main() { // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_2 // CHECK-NEXT: %20 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @62, i64 48 }, ptr %20, align 8 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @61, i64 48 }, ptr %20, align 8 // CHECK-NEXT: %21 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %20, 1 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %21) // CHECK-NEXT: unreachable diff --git a/cl/_testgo/recoverthenpanic/in.go b/cl/_testgo/recoverthenpanic/in.go index bee54a59bb..1f5b5e198a 100644 --- a/cl/_testgo/recoverthenpanic/in.go +++ b/cl/_testgo/recoverthenpanic/in.go @@ -7,7 +7,7 @@ package main // CHECK-LABEL: define void @"{{.*}}/cl/_testgo/recoverthenpanic.End"(){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %0 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.Recover"() +// CHECK-NEXT: %0 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.Recover"(ptr @"{{.*}}/cl/_testgo/recoverthenpanic.End") // CHECK-NEXT: %1 = call i1 @"{{.*}}/runtime/internal/runtime.EfaceEqual"(%"{{.*}}/runtime/internal/runtime.eface" %0, %"{{.*}}/runtime/internal/runtime.eface" zeroinitializer) // CHECK-NEXT: %2 = xor i1 %1, true // CHECK-NEXT: %3 = call ptr @"{{.*}}/runtime/internal/runtime.GetThreadDefer"() @@ -161,26 +161,28 @@ func main() { // CHECK-NEXT: _llgo_2: ; preds = %_llgo_5 // CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.main", %_llgo_3), ptr %8, align 8 // CHECK-NEXT: %13 = load i64, ptr %7, align 8 +// CHECK-NEXT: %14 = call ptr @"{{.*}}/runtime/internal/runtime.StartRecoverFrame"(ptr @"{{.*}}/cl/_testgo/recoverthenpanic.End") // CHECK-NEXT: call void @"{{.*}}/cl/_testgo/recoverthenpanic.End"() -// CHECK-NEXT: %14 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %2, align 8 -// CHECK-NEXT: %15 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %14, 2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %15) -// CHECK-NEXT: %16 = load ptr, ptr %9, align 8 -// CHECK-NEXT: indirectbr ptr %16, [label %_llgo_3] +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.EndRecoverFrame"(ptr %14) +// CHECK-NEXT: %15 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %2, align 8 +// CHECK-NEXT: %16 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %15, 2 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %16) +// CHECK-NEXT: %17 = load ptr, ptr %9, align 8 +// CHECK-NEXT: indirectbr ptr %17, [label %_llgo_3] // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_5, %_llgo_2 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Rethrow"(ptr %0) // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_0 -// CHECK-NEXT: %17 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 13 }, ptr %17, align 8 -// CHECK-NEXT: %18 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %17, 1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %18) +// CHECK-NEXT: %18 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 13 }, ptr %18, align 8 +// CHECK-NEXT: %19 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %18, 1 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %19) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_0 // CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.main", %_llgo_3), ptr %9, align 8 -// CHECK-NEXT: %19 = load ptr, ptr %8, align 8 -// CHECK-NEXT: indirectbr ptr %19, [label %_llgo_3, label %_llgo_2] +// CHECK-NEXT: %20 = load ptr, ptr %8, align 8 +// CHECK-NEXT: indirectbr ptr %20, [label %_llgo_3, label %_llgo_2] // CHECK-NEXT: } diff --git a/cl/_testgo/reflect/in.go b/cl/_testgo/reflect/in.go index fa7b800830..f34ead84dc 100644 --- a/cl/_testgo/reflect/in.go +++ b/cl/_testgo/reflect/in.go @@ -7,7 +7,6 @@ import ( ) // CHECK: @0 = private unnamed_addr constant [11 x i8] c"call.method", align 1 -// CHECK: @2 = private unnamed_addr constant [3 x i8] c"int", align 1 // CHECK: @6 = private unnamed_addr constant [7 x i8] c"closure", align 1 // CHECK: @7 = private unnamed_addr constant [5 x i8] c"error", align 1 // CHECK: @9 = private unnamed_addr constant [12 x i8] c"call.closure", align 1 @@ -724,7 +723,8 @@ func callMethod() { // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %22, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %37 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %22, ptr @_llgo_int, ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %37) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/selects/in.go b/cl/_testgo/selects/in.go index 5f69de4d6a..0849d5be49 100644 --- a/cl/_testgo/selects/in.go +++ b/cl/_testgo/selects/in.go @@ -234,11 +234,15 @@ func main() { // CHECK-LABEL: define ptr @"{{.*}}/cl/_testgo/selects._llgo_routine$1"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %1 = load { { ptr, ptr } }, ptr %0, align 8 -// CHECK-NEXT: %2 = extractvalue { { ptr, ptr } } %1, 0 +// CHECK-NEXT: %1 = alloca %"{{.*}}/runtime/internal/runtime.LocalContext", align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %1, i8 0, i64 8, i1 false) +// CHECK-NEXT: %2 = call i64 @"{{.*}}/runtime/internal/runtime.EnterLocalContext"(ptr %1) +// CHECK-NEXT: %3 = load { { ptr, ptr } }, ptr %0, align 8 +// CHECK-NEXT: %4 = extractvalue { { ptr, ptr } } %3, 0 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.FreeRoot"(ptr %0) -// CHECK-NEXT: %3 = extractvalue { ptr, ptr } %2, 1 -// CHECK-NEXT: %4 = extractvalue { ptr, ptr } %2, 0 -// CHECK-NEXT: call void %4(ptr %3) +// CHECK-NEXT: %5 = extractvalue { ptr, ptr } %4, 1 +// CHECK-NEXT: %6 = extractvalue { ptr, ptr } %4, 0 +// CHECK-NEXT: call void %6(ptr %5) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.LeaveLocalContext"(ptr %1, i64 %2) // CHECK-NEXT: ret ptr null // CHECK-NEXT: } diff --git a/cl/_testgo/tpinst/main.go b/cl/_testgo/tpinst/main.go index 7e87b9680e..388900ecd4 100644 --- a/cl/_testgo/tpinst/main.go +++ b/cl/_testgo/tpinst/main.go @@ -4,8 +4,6 @@ package main // CHECK-NOT: @6 = private unnamed_addr constant [5 x i8] c"value", align 1 // CHECK: @6 = private unnamed_addr constant [46 x i8] c"{{.*}}/cl/_testgo/tpinst.value", align 1 // CHECK: {{^}}@8 = private unnamed_addr constant [5 x i8] c"error", align 1{{$}} -// CHECK: {{^}}@15 = private unnamed_addr constant [22 x i8] c"interface{value() int}", align 1{{$}} -// CHECK: {{^}}@16 = private unnamed_addr constant [5 x i8] c"value", align 1{{$}} type M[T interface{}] struct { v T @@ -101,7 +99,8 @@ type I[T interface{}] interface { // CHECK-NEXT: br i1 %51, label %_llgo_5, label %_llgo_6 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_4 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %34, %"{{.*}}/runtime/internal/runtime.String" { ptr @15, i64 22 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @16, i64 5 }) +// CHECK-NEXT: %52 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %34, ptr @"{{.*}}/cl/_testgo/tpinst.iface$2sV9fFeqOv1SzesvwIdhTqCFzDT8ZX5buKUSAoHNSww", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %52) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testrt/any/in.go b/cl/_testrt/any/in.go index c498abc180..c09ae66398 100644 --- a/cl/_testrt/any/in.go +++ b/cl/_testrt/any/in.go @@ -5,10 +5,8 @@ import ( "github.com/goplus/lib/c" ) -// CHECK: @1 = private unnamed_addr constant [29 x i8] c"*github.com/goplus/lib/c.Char", align 1 -// CHECK: @2 = private unnamed_addr constant [3 x i8] c"int", align 1 -// CHECK: @3 = private unnamed_addr constant [7 x i8] c"%s %d\0A\00", align 1 -// CHECK: @4 = private unnamed_addr constant [6 x i8] c"Hello\00", align 1 +// CHECK: @2 = private unnamed_addr constant [7 x i8] c"%s %d\0A\00", align 1 +// CHECK: @3 = private unnamed_addr constant [6 x i8] c"Hello\00", align 1 // CHECK-LABEL: define ptr @"{{.*}}/cl/_testrt/any.hi"(%"{{.*}}/runtime/internal/runtime.eface" %0){{.*}} { // CHECK-NEXT: _llgo_0: @@ -21,7 +19,8 @@ import ( // CHECK-NEXT: ret ptr %3 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @1, i64 29 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %1, ptr @"*_llgo_int8", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %4) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -42,7 +41,8 @@ func hi(a any) *c.Char { // CHECK-NEXT: ret i64 %5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %6 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %1, ptr @_llgo_int, ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %6) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -69,12 +69,12 @@ func main() { // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/any.main"(){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %0 = call ptr @"{{.*}}/cl/_testrt/any.hi"(%"{{.*}}/runtime/internal/runtime.eface" { ptr @"*_llgo_int8", ptr @4 }) +// CHECK-NEXT: %0 = call ptr @"{{.*}}/cl/_testrt/any.hi"(%"{{.*}}/runtime/internal/runtime.eface" { ptr @"*_llgo_int8", ptr @3 }) // CHECK-NEXT: %1 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 8) // CHECK-NEXT: store i64 100, ptr %1, align 8 // CHECK-NEXT: %2 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_int, ptr undef }, ptr %1, 1 // CHECK-NEXT: %3 = call i64 @"{{.*}}/cl/_testrt/any.incVal"(%"{{.*}}/runtime/internal/runtime.eface" %2) -// CHECK-NEXT: %4 = call i32 (ptr, ...) @printf(ptr @3, ptr %0, i64 %3) +// CHECK-NEXT: %4 = call i32 (ptr, ...) @printf(ptr @2, ptr %0, i64 %3) // CHECK-NEXT: ret void // CHECK-NEXT: } diff --git a/cl/_testrt/any/out.ll b/cl/_testrt/any/out.ll new file mode 100644 index 0000000000..3c251678be --- /dev/null +++ b/cl/_testrt/any/out.ll @@ -0,0 +1,136 @@ +; ModuleID = 'github.com/goplus/llgo/cl/_testrt/any' +source_filename = "github.com/goplus/llgo/cl/_testrt/any" +target datalayout = "e-m:o-i64:64-i128:128-n32:64-S128-Fn32" +target triple = "arm64-apple-macosx" + +%"github.com/goplus/llgo/runtime/abi.PtrType" = type { %"github.com/goplus/llgo/runtime/abi.Type", ptr } +%"github.com/goplus/llgo/runtime/abi.Type" = type { i64, i64, i32, i8, i8, i8, i8, { ptr, ptr }, ptr, %"github.com/goplus/llgo/runtime/internal/runtime.String", ptr } +%"github.com/goplus/llgo/runtime/internal/runtime.String" = type { ptr, i64 } +%"github.com/goplus/llgo/runtime/abi.InterfaceType" = type { %"github.com/goplus/llgo/runtime/abi.Type", %"github.com/goplus/llgo/runtime/internal/runtime.String", %"github.com/goplus/llgo/runtime/internal/runtime.Slice" } +%"github.com/goplus/llgo/runtime/internal/runtime.Slice" = type { ptr, i64, i64 } +%"github.com/goplus/llgo/runtime/internal/runtime.eface" = type { ptr, ptr } + +@"github.com/goplus/llgo/cl/_testrt/any.init$guard" = global i1 false, align 1 +@"*_llgo_int8" = weak_odr constant %"github.com/goplus/llgo/runtime/abi.PtrType" { %"github.com/goplus/llgo/runtime/abi.Type" { i64 8, i64 8, i32 -1399554408, i8 10, i8 8, i8 8, i8 54, { ptr, ptr } { ptr @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequalptr", ptr null }, ptr null, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @0, i64 4 }, ptr null }, ptr @_llgo_int8 }, align 8 +@0 = private unnamed_addr constant [4 x i8] c"int8", align 1 +@_llgo_int8 = weak_odr constant %"github.com/goplus/llgo/runtime/abi.Type" { i64 1, i64 0, i32 1444672578, i8 12, i8 1, i8 1, i8 3, { ptr, ptr } { ptr @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequal8", ptr null }, ptr null, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @0, i64 4 }, ptr @"*_llgo_int8" }, align 8 +@_llgo_any = weak_odr constant %"github.com/goplus/llgo/runtime/abi.InterfaceType" { %"github.com/goplus/llgo/runtime/abi.Type" { i64 16, i64 16, i32 1376530322, i8 0, i8 8, i8 8, i8 20, { ptr, ptr } { ptr @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.nilinterequal", ptr null }, ptr null, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @1, i64 12 }, ptr @"*_llgo_any" }, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @2, i64 37 }, %"github.com/goplus/llgo/runtime/internal/runtime.Slice" zeroinitializer }, align 8 +@1 = private unnamed_addr constant [12 x i8] c"interface {}", align 1 +@"*_llgo_any" = weak_odr constant %"github.com/goplus/llgo/runtime/abi.PtrType" { %"github.com/goplus/llgo/runtime/abi.Type" { i64 8, i64 8, i32 1741196194, i8 10, i8 8, i8 8, i8 54, { ptr, ptr } { ptr @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequalptr", ptr null }, ptr null, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @1, i64 12 }, ptr null }, ptr @_llgo_any }, align 8 +@2 = private unnamed_addr constant [37 x i8] c"github.com/goplus/llgo/cl/_testrt/any", align 1 +@_llgo_int = weak_odr constant %"github.com/goplus/llgo/runtime/abi.Type" { i64 8, i64 0, i32 -25294021, i8 12, i8 8, i8 8, i8 2, { ptr, ptr } { ptr @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequal64", ptr null }, ptr null, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @3, i64 3 }, ptr @"*_llgo_int" }, align 8 +@3 = private unnamed_addr constant [3 x i8] c"int", align 1 +@"*_llgo_int" = weak_odr constant %"github.com/goplus/llgo/runtime/abi.PtrType" { %"github.com/goplus/llgo/runtime/abi.Type" { i64 8, i64 8, i32 -939606833, i8 10, i8 8, i8 8, i8 54, { ptr, ptr } { ptr @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequalptr", ptr null }, ptr null, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @3, i64 3 }, ptr null }, ptr @_llgo_int }, align 8 +@4 = private unnamed_addr constant [7 x i8] c"%s %d\0A\00", align 1 +@5 = private unnamed_addr constant [6 x i8] c"Hello\00", align 1 + +; Function Attrs: null_pointer_is_valid +define ptr @"github.com/goplus/llgo/cl/_testrt/any.hi"(%"github.com/goplus/llgo/runtime/internal/runtime.eface" %0) #0 { +_llgo_0: + %1 = extractvalue %"github.com/goplus/llgo/runtime/internal/runtime.eface" %0, 0 + %2 = icmp eq ptr %1, @"*_llgo_int8" + br i1 %2, label %_llgo_1, label %_llgo_2 + +_llgo_1: ; preds = %_llgo_0 + %3 = extractvalue %"github.com/goplus/llgo/runtime/internal/runtime.eface" %0, 1 + ret ptr %3 + +_llgo_2: ; preds = %_llgo_0 + %4 = call %"github.com/goplus/llgo/runtime/internal/runtime.eface" @"github.com/goplus/llgo/runtime/internal/runtime.TypeAssertError"(ptr %1, ptr @"*_llgo_int8", ptr @_llgo_any) + call void @"github.com/goplus/llgo/runtime/internal/runtime.Panic"(%"github.com/goplus/llgo/runtime/internal/runtime.eface" %4) + unreachable +} + +; Function Attrs: null_pointer_is_valid +define i64 @"github.com/goplus/llgo/cl/_testrt/any.incVal"(%"github.com/goplus/llgo/runtime/internal/runtime.eface" %0) #0 { +_llgo_0: + %1 = extractvalue %"github.com/goplus/llgo/runtime/internal/runtime.eface" %0, 0 + %2 = icmp eq ptr %1, @_llgo_int + br i1 %2, label %_llgo_1, label %_llgo_2 + +_llgo_1: ; preds = %_llgo_0 + %3 = extractvalue %"github.com/goplus/llgo/runtime/internal/runtime.eface" %0, 1 + %4 = load i64, ptr %3, align 8 + %5 = add i64 %4, 1 + ret i64 %5 + +_llgo_2: ; preds = %_llgo_0 + %6 = call %"github.com/goplus/llgo/runtime/internal/runtime.eface" @"github.com/goplus/llgo/runtime/internal/runtime.TypeAssertError"(ptr %1, ptr @_llgo_int, ptr @_llgo_any) + call void @"github.com/goplus/llgo/runtime/internal/runtime.Panic"(%"github.com/goplus/llgo/runtime/internal/runtime.eface" %6) + unreachable +} + +; Function Attrs: null_pointer_is_valid +define void @"github.com/goplus/llgo/cl/_testrt/any.init"() #0 { +_llgo_0: + %0 = load i1, ptr @"github.com/goplus/llgo/cl/_testrt/any.init$guard", align 1 + br i1 %0, label %_llgo_2, label %_llgo_1 + +_llgo_1: ; preds = %_llgo_0 + store i1 true, ptr @"github.com/goplus/llgo/cl/_testrt/any.init$guard", align 1 + br label %_llgo_2 + +_llgo_2: ; preds = %_llgo_1, %_llgo_0 + ret void +} + +; Function Attrs: null_pointer_is_valid +define void @"github.com/goplus/llgo/cl/_testrt/any.main"() #0 { +_llgo_0: + %0 = call ptr @"github.com/goplus/llgo/cl/_testrt/any.hi"(%"github.com/goplus/llgo/runtime/internal/runtime.eface" { ptr @"*_llgo_int8", ptr @5 }) + %1 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 8) + store i64 100, ptr %1, align 8 + %2 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.eface" { ptr @_llgo_int, ptr undef }, ptr %1, 1 + %3 = call i64 @"github.com/goplus/llgo/cl/_testrt/any.incVal"(%"github.com/goplus/llgo/runtime/internal/runtime.eface" %2) + %4 = call i32 (ptr, ...) @printf(ptr @4, ptr %0, i64 %3) + ret void +} + +; Function Attrs: null_pointer_is_valid +declare i1 @"github.com/goplus/llgo/runtime/internal/runtime.memequalptr"(ptr, ptr) #0 + +define linkonce i1 @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequalptr"(ptr %0, ptr %1, ptr %2) { +_llgo_0: + %3 = tail call i1 @"github.com/goplus/llgo/runtime/internal/runtime.memequalptr"(ptr %1, ptr %2) + ret i1 %3 +} + +; Function Attrs: null_pointer_is_valid +declare i1 @"github.com/goplus/llgo/runtime/internal/runtime.memequal8"(ptr, ptr) #0 + +define linkonce i1 @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequal8"(ptr %0, ptr %1, ptr %2) { +_llgo_0: + %3 = tail call i1 @"github.com/goplus/llgo/runtime/internal/runtime.memequal8"(ptr %1, ptr %2) + ret i1 %3 +} + +; Function Attrs: null_pointer_is_valid +declare %"github.com/goplus/llgo/runtime/internal/runtime.eface" @"github.com/goplus/llgo/runtime/internal/runtime.TypeAssertError"(ptr, ptr, ptr) #0 + +; Function Attrs: null_pointer_is_valid +declare i1 @"github.com/goplus/llgo/runtime/internal/runtime.nilinterequal"(ptr, ptr) #0 + +define linkonce i1 @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.nilinterequal"(ptr %0, ptr %1, ptr %2) { +_llgo_0: + %3 = tail call i1 @"github.com/goplus/llgo/runtime/internal/runtime.nilinterequal"(ptr %1, ptr %2) + ret i1 %3 +} + +; Function Attrs: null_pointer_is_valid +declare void @"github.com/goplus/llgo/runtime/internal/runtime.Panic"(%"github.com/goplus/llgo/runtime/internal/runtime.eface") #0 + +; Function Attrs: null_pointer_is_valid +declare i1 @"github.com/goplus/llgo/runtime/internal/runtime.memequal64"(ptr, ptr) #0 + +define linkonce i1 @"__llgo_stub.github.com/goplus/llgo/runtime/internal/runtime.memequal64"(ptr %0, ptr %1, ptr %2) { +_llgo_0: + %3 = tail call i1 @"github.com/goplus/llgo/runtime/internal/runtime.memequal64"(ptr %1, ptr %2) + ret i1 %3 +} + +; Function Attrs: null_pointer_is_valid +declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64) #0 + +declare i32 @printf(ptr, ...) + +attributes #0 = { null_pointer_is_valid "frame-pointer"="non-leaf" } diff --git a/cl/_testrt/funcdecl/in.go b/cl/_testrt/funcdecl/in.go index 45f6f0cc67..d3d7632a27 100644 --- a/cl/_testrt/funcdecl/in.go +++ b/cl/_testrt/funcdecl/in.go @@ -5,9 +5,8 @@ import ( "unsafe" ) -// CHECK: @4 = private unnamed_addr constant [39 x i8] c"struct{$f func(); $data unsafe.Pointer}", align 1 -// CHECK: @5 = private unnamed_addr constant [4 x i8] c"demo", align 1 -// CHECK: @6 = private unnamed_addr constant [5 x i8] c"hello", align 1 +// CHECK: @4 = private unnamed_addr constant [4 x i8] c"demo", align 1 +// CHECK: @5 = private unnamed_addr constant [5 x i8] c"hello", align 1 // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/funcdecl.check"({ ptr, ptr } %0){{.*}} { // CHECK-NEXT: _llgo_0: @@ -29,36 +28,38 @@ import ( // CHECK-NEXT: br i1 %10, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %5, %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 39 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %11 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %5, ptr @"_llgo_closure$b7Su1hWaFih-M0M9hMk6nO_RD1K_GQu5WjIXQp6Q2e8", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %11) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_1 -// CHECK-NEXT: %11 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %4, 1 -// CHECK-NEXT: %12 = load { ptr, ptr }, ptr %11, align 8 +// CHECK-NEXT: %12 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %4, 1 +// CHECK-NEXT: %13 = load { ptr, ptr }, ptr %12, align 8 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintEface"(%"{{.*}}/runtime/internal/runtime.eface" %2) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintEface"(%"{{.*}}/runtime/internal/runtime.eface" %4) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: %13 = extractvalue { ptr, ptr } %0, 0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr %13) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: %14 = extractvalue { ptr, ptr } %8, 0 +// CHECK-NEXT: %14 = extractvalue { ptr, ptr } %0, 0 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr %14) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: %15 = extractvalue { ptr, ptr } %12, 0 +// CHECK-NEXT: %15 = extractvalue { ptr, ptr } %8, 0 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr %15) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) +// CHECK-NEXT: %16 = extractvalue { ptr, ptr } %13, 0 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr %16) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr @"{{.*}}/cl/_testrt/funcdecl.demo") // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: %16 = call ptr @"{{.*}}/cl/_testrt/funcdecl.closurePtr"(%"{{.*}}/runtime/internal/runtime.eface" %2) -// CHECK-NEXT: %17 = call ptr @"{{.*}}/cl/_testrt/funcdecl.closurePtr"(%"{{.*}}/runtime/internal/runtime.eface" %4) -// CHECK-NEXT: %18 = icmp eq ptr %16, %17 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %18) +// CHECK-NEXT: %17 = call ptr @"{{.*}}/cl/_testrt/funcdecl.closurePtr"(%"{{.*}}/runtime/internal/runtime.eface" %2) +// CHECK-NEXT: %18 = call ptr @"{{.*}}/cl/_testrt/funcdecl.closurePtr"(%"{{.*}}/runtime/internal/runtime.eface" %4) +// CHECK-NEXT: %19 = icmp eq ptr %17, %18 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %19) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %9, %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 39 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %20 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %9, ptr @"_llgo_closure$b7Su1hWaFih-M0M9hMk6nO_RD1K_GQu5WjIXQp6Q2e8", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %20) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -96,7 +97,7 @@ type rtype struct { // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/funcdecl.demo"(){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 4 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 4 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-NEXT: } @@ -120,7 +121,7 @@ func demo() { // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/funcdecl.main"(){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @6, i64 5 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 5 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: call void @"{{.*}}/cl/_testrt/funcdecl.check"({ ptr, ptr } { ptr @"__llgo_stub.{{.*}}/cl/_testrt/funcdecl.demo", ptr null }) // CHECK-NEXT: ret void diff --git a/cl/_testrt/makemap/in.go b/cl/_testrt/makemap/in.go index 69fce2a770..736591977f 100644 --- a/cl/_testrt/makemap/in.go +++ b/cl/_testrt/makemap/in.go @@ -8,9 +8,6 @@ package main // CHECK: @22 = private unnamed_addr constant [2 x i8] c"go", align 1 // CHECK: @23 = private unnamed_addr constant [7 x i8] c"bad key", align 1 // CHECK: @24 = private unnamed_addr constant [7 x i8] c"bad len", align 1 -// CHECK: @32 = private unnamed_addr constant [44 x i8] c"{{.*}}/cl/_testrt/makemap.N1", align 1 -// CHECK: @39 = private unnamed_addr constant [43 x i8] c"{{.*}}/cl/_testrt/makemap.K", align 1 -// CHECK: @42 = private unnamed_addr constant [44 x i8] c"{{.*}}/cl/_testrt/makemap.K2", align 1 func main() { make1() @@ -377,7 +374,8 @@ type N1 [1]int // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %39, %"{{.*}}/runtime/internal/runtime.String" { ptr @32, i64 44 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %54 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %39, ptr @"_llgo_{{.*}}/cl/_testrt/makemap.N1", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %54) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -514,7 +512,8 @@ type K2 [1]*N // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %39, %"{{.*}}/runtime/internal/runtime.String" { ptr @39, i64 43 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %56 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %39, ptr @"_llgo_{{.*}}/cl/_testrt/makemap.K", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %56) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -646,7 +645,8 @@ func make3() { // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %43, %"{{.*}}/runtime/internal/runtime.String" { ptr @42, i64 44 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %61 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %43, ptr @"_llgo_{{.*}}/cl/_testrt/makemap.K2", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %61) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testrt/slice2array/in.go b/cl/_testrt/slice2array/in.go index 78af725c11..5c2a7b1438 100644 --- a/cl/_testrt/slice2array/in.go +++ b/cl/_testrt/slice2array/in.go @@ -1,6 +1,26 @@ // LITTEST package main +func main() { + array := [4]byte{1, 2, 3, 4} + ptr := (*[4]byte)(array[:]) + println(array == *ptr) + println(*(*[2]byte)(array[:]) == [2]byte{1, 2}) +} + +// CHECK-LABEL: define void @"{{.*}}/cl/_testrt/slice2array.init"(){{.*}} { +// CHECK-NEXT: _llgo_0: +// CHECK-NEXT: %0 = load i1, ptr @"{{.*}}/cl/_testrt/slice2array.init$guard", align 1 +// CHECK-NEXT: br i1 %0, label %_llgo_2, label %_llgo_1 +// CHECK-EMPTY: +// CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 +// CHECK-NEXT: store i1 true, ptr @"{{.*}}/cl/_testrt/slice2array.init$guard", align 1 +// CHECK-NEXT: br label %_llgo_2 +// CHECK-EMPTY: +// CHECK-NEXT: _llgo_2: ; preds = %_llgo_1, %_llgo_0 +// CHECK-NEXT: ret void +// CHECK-NEXT: } + // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/slice2array.main"(){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %0 = call ptr @"{{.*}}/runtime/internal/runtime.AllocZ"(i64 4) @@ -80,9 +100,3 @@ package main // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-NEXT: } -func main() { - array := [4]byte{1, 2, 3, 4} - ptr := (*[4]byte)(array[:]) - println(array == *ptr) - println(*(*[2]byte)(array[:]) == [2]byte{1, 2}) -} diff --git a/cl/_testrt/tpabi/in.go b/cl/_testrt/tpabi/in.go index ca04d5219b..cf003eb199 100644 --- a/cl/_testrt/tpabi/in.go +++ b/cl/_testrt/tpabi/in.go @@ -5,8 +5,8 @@ import "github.com/goplus/lib/c" // CHECK: @0 = private unnamed_addr constant [1 x i8] c"a", align 1 // CHECK: @5 = private unnamed_addr constant [4 x i8] c"Info", align 1 -// CHECK: @10 = private unnamed_addr constant [54 x i8] c"{{.*}}/cl/_testrt/tpabi.T[string, int]", align 1 -// CHECK: @11 = private unnamed_addr constant [5 x i8] c"hello", align 1 +// CHECK: @10 = private unnamed_addr constant [5 x i8] c"hello", align 1 +// CHECK: @12 = private unnamed_addr constant [54 x i8] c"{{.*}}/cl/_testrt/tpabi.T[string, int]", align 1 // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/tpabi.init"(){{.*}} { // CHECK-NEXT: _llgo_0: @@ -70,7 +70,7 @@ func (t *K[N]) Advance(n int) *K[N] { // CHECK-NEXT: %11 = call ptr @"{{.*}}/runtime/internal/runtime.AllocZ"(i64 24) // CHECK-NEXT: %12 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpabi.T[string,int]", ptr %11, i32 0, i32 0 // CHECK-NEXT: %13 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpabi.T[string,int]", ptr %11, i32 0, i32 1 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @11, i64 5 }, ptr %12, align 8 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @10, i64 5 }, ptr %12, align 8 // CHECK-NEXT: store i64 100, ptr %13, align 8 // CHECK-NEXT: %14 = call ptr @"{{.*}}/runtime/internal/runtime.NewItab"(ptr @"_llgo_iface$BP0p_lUsEd-IbbtJVukGmgrdQkqzcoYzSiwgUvgFvUs", ptr @"*_llgo_{{.*}}/cl/_testrt/tpabi.T[string,int]") // CHECK-NEXT: %15 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" undef, ptr %14, 0 @@ -102,7 +102,8 @@ func (t *K[N]) Advance(n int) *K[N] { // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %6, %"{{.*}}/runtime/internal/runtime.String" { ptr @10, i64 54 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %32 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %6, ptr @"_llgo_{{.*}}/cl/_testrt/tpabi.T[string,int]", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %32) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -149,7 +150,7 @@ func main() { // CHECK-LABEL: define linkonce void @"{{.*}}/cl/_testrt/tpabi.(*T[string,int]).Info"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %1 = icmp eq ptr %0, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @10, i64 54 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 4 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @12, i64 54 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 4 }) // CHECK-NEXT: %2 = load %"{{.*}}/cl/_testrt/tpabi.T[string,int]", ptr %0, align 8 // CHECK-NEXT: call void @"{{.*}}/cl/_testrt/tpabi.T[string,int].Info"(%"{{.*}}/cl/_testrt/tpabi.T[string,int]" %2) // CHECK-NEXT: ret void diff --git a/cl/_testrt/typed/in.go b/cl/_testrt/typed/in.go index 1444e36c60..49a2468ab6 100644 --- a/cl/_testrt/typed/in.go +++ b/cl/_testrt/typed/in.go @@ -2,7 +2,6 @@ package main // CHECK: @0 = private unnamed_addr constant [5 x i8] c"hello", align 1 -// CHECK: @3 = private unnamed_addr constant [41 x i8] c"{{.*}}/cl/_testrt/typed.T", align 1 // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/typed.init"(){{.*}} { // CHECK-NEXT: _llgo_0: @@ -39,67 +38,68 @@ type A [2]int // CHECK-NEXT: br i1 %7, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @3, i64 41 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %8 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %2, ptr @"_llgo_{{.*}}/cl/_testrt/typed.T", ptr null) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %8) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_1 -// CHECK-NEXT: %8 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %1, 1 -// CHECK-NEXT: %9 = load %"{{.*}}/runtime/internal/runtime.String", ptr %8, align 8 -// CHECK-NEXT: %10 = insertvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } undef, %"{{.*}}/runtime/internal/runtime.String" %9, 0 -// CHECK-NEXT: %11 = insertvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %10, i1 true, 1 +// CHECK-NEXT: %9 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %1, 1 +// CHECK-NEXT: %10 = load %"{{.*}}/runtime/internal/runtime.String", ptr %9, align 8 +// CHECK-NEXT: %11 = insertvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } undef, %"{{.*}}/runtime/internal/runtime.String" %10, 0 +// CHECK-NEXT: %12 = insertvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %11, i1 true, 1 // CHECK-NEXT: br label %_llgo_5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_1 // CHECK-NEXT: br label %_llgo_5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_4, %_llgo_3 -// CHECK-NEXT: %12 = phi { %"{{.*}}/runtime/internal/runtime.String", i1 } [ %11, %_llgo_3 ], [ zeroinitializer, %_llgo_4 ] -// CHECK-NEXT: %13 = extractvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %12, 0 -// CHECK-NEXT: %14 = extractvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %12, 1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" %13) +// CHECK-NEXT: %13 = phi { %"{{.*}}/runtime/internal/runtime.String", i1 } [ %12, %_llgo_3 ], [ zeroinitializer, %_llgo_4 ] +// CHECK-NEXT: %14 = extractvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %13, 0 +// CHECK-NEXT: %15 = extractvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %13, 1 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" %14) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %14) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %15) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: %15 = alloca [2 x i64], align 8 -// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %15, i8 0, i64 16, i1 false) -// CHECK-NEXT: %16 = getelementptr inbounds i64, ptr %15, i64 0 -// CHECK-NEXT: %17 = getelementptr inbounds i64, ptr %15, i64 1 -// CHECK-NEXT: store i64 1, ptr %16, align 8 -// CHECK-NEXT: store i64 2, ptr %17, align 8 -// CHECK-NEXT: %18 = load [2 x i64], ptr %15, align 8 -// CHECK-NEXT: %19 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store [2 x i64] %18, ptr %19, align 8 -// CHECK-NEXT: %20 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @"_llgo_{{.*}}/cl/_testrt/typed.A", ptr undef }, ptr %19, 1 -// CHECK-NEXT: %21 = alloca [2 x i64], align 8 -// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %21, i8 0, i64 16, i1 false) -// CHECK-NEXT: %22 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %20, 0 -// CHECK-NEXT: %23 = icmp eq ptr %22, @"_llgo_{{.*}}/cl/_testrt/typed.A" -// CHECK-NEXT: br i1 %23, label %_llgo_6, label %_llgo_7 +// CHECK-NEXT: %16 = alloca [2 x i64], align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %16, i8 0, i64 16, i1 false) +// CHECK-NEXT: %17 = getelementptr inbounds i64, ptr %16, i64 0 +// CHECK-NEXT: %18 = getelementptr inbounds i64, ptr %16, i64 1 +// CHECK-NEXT: store i64 1, ptr %17, align 8 +// CHECK-NEXT: store i64 2, ptr %18, align 8 +// CHECK-NEXT: %19 = load [2 x i64], ptr %16, align 8 +// CHECK-NEXT: %20 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: store [2 x i64] %19, ptr %20, align 8 +// CHECK-NEXT: %21 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @"_llgo_{{.*}}/cl/_testrt/typed.A", ptr undef }, ptr %20, 1 +// CHECK-NEXT: %22 = alloca [2 x i64], align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %22, i8 0, i64 16, i1 false) +// CHECK-NEXT: %23 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %21, 0 +// CHECK-NEXT: %24 = icmp eq ptr %23, @"_llgo_{{.*}}/cl/_testrt/typed.A" +// CHECK-NEXT: br i1 %24, label %_llgo_6, label %_llgo_7 // CHECK-EMPTY: // CHECK-NEXT: _llgo_6: ; preds = %_llgo_5 -// CHECK-NEXT: %24 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %20, 1 -// CHECK-NEXT: %25 = load [2 x i64], ptr %24, align 8 -// CHECK-NEXT: %26 = insertvalue { [2 x i64], i1 } undef, [2 x i64] %25, 0 -// CHECK-NEXT: %27 = insertvalue { [2 x i64], i1 } %26, i1 true, 1 +// CHECK-NEXT: %25 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %21, 1 +// CHECK-NEXT: %26 = load [2 x i64], ptr %25, align 8 +// CHECK-NEXT: %27 = insertvalue { [2 x i64], i1 } undef, [2 x i64] %26, 0 +// CHECK-NEXT: %28 = insertvalue { [2 x i64], i1 } %27, i1 true, 1 // CHECK-NEXT: br label %_llgo_8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_7: ; preds = %_llgo_5 // CHECK-NEXT: br label %_llgo_8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_7, %_llgo_6 -// CHECK-NEXT: %28 = phi { [2 x i64], i1 } [ %27, %_llgo_6 ], [ zeroinitializer, %_llgo_7 ] -// CHECK-NEXT: %29 = extractvalue { [2 x i64], i1 } %28, 0 -// CHECK-NEXT: store [2 x i64] %29, ptr %21, align 8 -// CHECK-NEXT: %30 = extractvalue { [2 x i64], i1 } %28, 1 -// CHECK-NEXT: %31 = getelementptr inbounds i64, ptr %21, i64 0 -// CHECK-NEXT: %32 = load i64, ptr %31, align 8 -// CHECK-NEXT: %33 = getelementptr inbounds i64, ptr %21, i64 1 -// CHECK-NEXT: %34 = load i64, ptr %33, align 8 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 %32) +// CHECK-NEXT: %29 = phi { [2 x i64], i1 } [ %28, %_llgo_6 ], [ zeroinitializer, %_llgo_7 ] +// CHECK-NEXT: %30 = extractvalue { [2 x i64], i1 } %29, 0 +// CHECK-NEXT: store [2 x i64] %30, ptr %22, align 8 +// CHECK-NEXT: %31 = extractvalue { [2 x i64], i1 } %29, 1 +// CHECK-NEXT: %32 = getelementptr inbounds i64, ptr %22, i64 0 +// CHECK-NEXT: %33 = load i64, ptr %32, align 8 +// CHECK-NEXT: %34 = getelementptr inbounds i64, ptr %22, i64 1 +// CHECK-NEXT: %35 = load i64, ptr %34, align 8 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 %33) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 %34) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 %35) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %30) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %31) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-NEXT: } diff --git a/cl/cgo_test.go b/cl/cgo_test.go index 8ba0838340..289b68a809 100644 --- a/cl/cgo_test.go +++ b/cl/cgo_test.go @@ -207,6 +207,47 @@ func findStaticCall(t *testing.T, fn *gossa.Function, name string) *gossa.Call { return nil } +func TestRecoverCallClassificationHelpers(t *testing.T) { + if functionUsesRecover(nil) { + t.Fatal("nil function should not report recover use") + } + + ssaPkg, _, _ := buildGoSSAPkg(t, ` +package foo + +func usesRecover() { + recover() +} + +func plain() {} +`) + usesRecover := ssaPkg.Members["usesRecover"].(*gossa.Function) + plain := ssaPkg.Members["plain"].(*gossa.Function) + ctx := &context{} + + if !functionUsesRecover(usesRecover) { + t.Fatal("usesRecover should report direct recover use") + } + if functionUsesRecover(plain) { + t.Fatal("plain should not report recover use") + } + if !ctx.callMayRecover(usesRecover) { + t.Fatal("function using recover should be recover-capable") + } + if ctx.callMayRecover(plain) { + t.Fatal("plain static function should not be recover-capable") + } + if !ctx.callMayRecover(&gossa.MakeClosure{}) { + t.Fatal("unknown closure target should conservatively be recover-capable") + } + if !ctx.callMayRecover(&gossa.Call{}) { + t.Fatal("function value returned by a call should conservatively be recover-capable") + } + if !ctx.callMayRecover(nil) { + t.Fatal("unknown call value should conservatively be recover-capable") + } +} + func TestCgoCgocall_InitArgsFromParams(t *testing.T) { ssaPkg, _, _ := buildGoSSAPkg(t, ` package foo diff --git a/cl/compile.go b/cl/compile.go index 6cb94df500..0c5c04de3e 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -180,6 +180,7 @@ type context struct { debugAllocVars map[*ssa.Alloc]*types.Var runtimeCallerFuncs map[*ssa.Function]bool pcLineSeq uint64 + recoverSlots map[*ssa.Alloc]none patches Patches blkInfos []blocks.Info @@ -210,6 +211,7 @@ type context struct { staticGlobalInits map[*ssa.Global]llssa.Expr staticInitStores map[*ssa.Store]none staticInitInstrs map[ssa.Instruction]none + locality localityLowering } func (p *context) rewriteValue(name string) (string, bool) { @@ -391,7 +393,10 @@ func (p *context) compileGlobal(pkg llssa.Package, gbl *ssa.Global) { return } dbgInstrln("==> NewVar", name, typ) - g := pkg.NewVar(name, typ, llssa.Background(vtype)) + g, skip := p.localityGlobalStorage(pkg, gbl, name, typ, llssa.Background(vtype)) + if skip { + return + } if p.tryEmbedGlobalInit(pkg, gbl, g, name) { return } @@ -559,12 +564,15 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun noInlineDirective := hasNoInlineDirective(f) runtimeStackNoInline := needsRuntimeStackNoInline(pkgTypes, f) pcLineNoInline := p.needsPCLineNoInline(f) - if disableInline || noInlineDirective || runtimeStackNoInline || pcLineNoInline { + if disableInline || noInlineDirective || runtimeStackNoInline || pcLineNoInline || functionUsesRecover(f) { fn.Inline(llssa.NoInline) } if noInlineDirective || runtimeStackNoInline || pcLineNoInline { fn.DisableTailCalls() } + if functionUsesRecover(f) { + fn.Expr = fn.Expr.MarkMayRecover() + } p.funcs[f] = fn isCgo := isCgoExternSymbol(f) if nblk := len(f.Blocks); nblk > 0 { @@ -601,12 +609,22 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun dbgSymsEnabled := enableDbgSyms && (f == nil || f.Origin() == nil) p.inits = append(p.inits, func() { oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark := p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark + oldLocalityFunction := p.locality.function + oldRecoverSlots := p.recoverSlots p.fn = fn p.goFn = f p.callerFrameMark = llssa.Nil + p.locality.function = localityFunction{} p.state = state // restore pkgState when compiling funcBody + if f.Recover != nil { + p.recoverSlots = make(map[*ssa.Alloc]none) + } else { + p.recoverSlots = nil + } defer func() { p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark = oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark + p.locality.function = oldLocalityFunction + p.recoverSlots = oldRecoverSlots }() p.phis = nil if dbgSymsEnabled { @@ -624,6 +642,7 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun bodyPos := p.getFuncBodyPos(f) b.DebugFunction(fn, debugFunctionScope(f), pos, bodyPos) } + p.prepareExportedLocalContext(f) p.bvals = make(map[ssa.Value]llssa.Expr) p.methodNilDerefChecks = collectMethodNilDerefChecks(f) off := make([]int, len(f.Blocks)) @@ -832,6 +851,9 @@ func (p *context) debugParams(b llssa.Builder, f *ssa.Function) { } func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, doModInit bool) llssa.BasicBlock { + oldLocalBlock := p.locality.function.block + p.locality.function.block = block + defer func() { p.locality.function.block = oldLocalBlock }() var last int var pyModInit bool var prog = p.prog @@ -840,6 +862,9 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do var instrs = block.Instrs[n:] var ret = fn.Block(block.Index) b.SetBlock(ret) + if block.Index == 0 { + p.enterExportedLocalContext(b) + } if block.Index == 0 && p.shouldTrackCallerFrames() { p.pushCallerLocationFrame(b, block.Parent()) } @@ -852,6 +877,7 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do } if doModInit { + p.initializeLocalGuards(b) if p.state != pkgInPatch { p.applyEmbedInits(b) } @@ -1118,6 +1144,29 @@ func (p *context) syntheticMakeSliceCap(v *ssa.Slice) (llssa.Expr, bool) { return p.prog.IntVal(uint64(arr.Len()), p.prog.Int()), true } +func (p *context) markRecoverSlot(v *ssa.Alloc) { + if p.recoverSlots == nil || v.Heap { + return + } + p.recoverSlots[v] = none{} +} + +func (p *context) isRecoverSlotAddr(v ssa.Value) bool { + if p.recoverSlots == nil { + return false + } + switch v := v.(type) { + case *ssa.Alloc: + _, ok := p.recoverSlots[v] + return ok + case *ssa.FieldAddr: + return p.isRecoverSlotAddr(v.X) + case *ssa.IndexAddr: + return p.isRecoverSlotAddr(v.X) + } + return false +} + func isAllocVargs(ctx *context, v *ssa.Alloc) bool { refs, ok := nonDebugReferrers(v) if !ok || len(refs) == 0 { @@ -1303,6 +1352,9 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue } } ret = b.UnOp(v.Op, x) + if v.Op == token.MUL && p.isRecoverSlotAddr(v.X) { + ret = ret.SetVolatile(true) + } } case *ssa.ChangeType: t := v.Type() @@ -1338,6 +1390,10 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue elem := p.type_(t.Elem(), llssa.InGo) ret = b.Alloc(elem, v.Heap) p.debugAlloc(b, v, ret) + p.markRecoverSlot(v) + if p.isRecoverSlotAddr(v) { + b.Store(ret, p.prog.Zero(elem)).SetVolatile(true) + } case *ssa.IndexAddr: vx := v.X if _, ok := p.isVArgs(vx); ok { // varargs: this is a varargs index @@ -1676,7 +1732,10 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { } ptr := p.compileValue(b, va) val := p.compileValue(b, v.Val) - b.Store(ptr, val) + store := b.Store(ptr, val) + if p.isRecoverSlotAddr(va) { + store.SetVolatile(true) + } case *ssa.Jump: jmpb := p.jumpTo(v) b.Jump(jmpb) @@ -1695,6 +1754,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { if p.shouldTrackCallerFrames() { p.popCallerLocationFrame(b) } + p.leaveExportedLocalContext(b) b.Return(results...) case *ssa.If: fn := p.fn @@ -1754,6 +1814,25 @@ func (p *context) getLocalVariable(b llssa.Builder, fn *ssa.Function, v *types.V return div } +func functionUsesRecover(fn *ssa.Function) bool { + if fn == nil { + return false + } + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + call, ok := instr.(ssa.CallInstruction) + if !ok { + continue + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if ok && builtin.Name() == "recover" { + return true + } + } + } + return false +} + func (p *context) compileFunction(v *ssa.Function) (goFn llssa.Function, pyFn llssa.PyObjRef, kind int) { // TODO(xsw) v.Pkg == nil: means auto generated function? if v.Pkg == p.goPkg || v.Pkg == nil { @@ -1793,7 +1872,7 @@ func (p *context) compileValue(b llssa.Builder, v ssa.Value) llssa.Expr { if isCgoVar(varName) { p.cgoSymbols = append(p.cgoSymbols, val.Name()) } - if enableDbgSyms { + if enableDbgSyms && p.localityAllowsGlobalDebug(v) { pos := p.fset.Position(v.Pos()) b.DIGlobal(val, v.Name(), pos) } @@ -2029,6 +2108,15 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri pkg.Pkg = pkgTypes patch.Alt.Pkg = pkgTypes } + if err = ParsePkgSyntax(prog, pkgProg.Fset, pkgTypes, files); err != nil { + return nil, nil, err + } + if err = prog.ValidateLocalities(llssa.PathOf(pkgTypes)); err != nil { + return nil, nil, err + } + if err = validateLocalInitializers(prog, pkgTypes); err != nil { + return nil, nil, err + } if pkgPath == llssa.PkgRuntime { prog.SetRuntime(pkgTypes) } @@ -2130,6 +2218,17 @@ func processPkg(ctx *context, ret llssa.Package, pkg *ssa.Package) { sort.Slice(members, func(i, j int) bool { return members[i].name < members[j].name }) + localGlobals := make([]*ssa.Global, 0) + for _, m := range members { + global, ok := m.val.(*ssa.Global) + if !ok || isCgoFuncPtrVar(global.Name()) { + continue + } + localGlobals = append(localGlobals, global) + } + // Address accessors and replay guards must exist before any function body + // can reference a local package variable, regardless of member sort order. + ctx.prepareLocalVariables(ret, localGlobals) for _, m := range members { member := m.val diff --git a/cl/import.go b/cl/import.go index eb7a0e2815..5b18496eac 100644 --- a/cl/import.go +++ b/cl/import.go @@ -28,7 +28,9 @@ import ( "golang.org/x/tools/go/ssa" + "github.com/goplus/llgo/internal/directive" "github.com/goplus/llgo/internal/env" + "github.com/goplus/llgo/internal/locality" llssa "github.com/goplus/llgo/ssa" ) @@ -218,30 +220,6 @@ func (p *context) initFiles(pkgPath string, files []*ast.File, cPkg bool) { } } -// PreCollectLinknames scans syntax files before SSA compilation and populates -// prog.Linkname for package-level //go:linkname / //llgo:link declarations. -// It intentionally ignores //export because there is no package export context -// during the pre-collection phase. -func PreCollectLinknames(prog llssa.Program, pkgPath string, files []*ast.File) { - ctx := &context{prog: prog} - for _, file := range files { - for _, decl := range file.Decls { - switch decl := decl.(type) { - case *ast.FuncDecl: - fullName, inPkgName := astFuncName(pkgPath, decl) - ctx.processLinknameByDoc(decl.Doc, fullName, inPkgName, false, false) - case *ast.GenDecl: - if decl.Tok == token.VAR && len(decl.Specs) == 1 { - if names := decl.Specs[0].(*ast.ValueSpec).Names; len(names) == 1 { - inPkgName := names[0].Name - ctx.processLinknameByDoc(decl.Doc, pkgPath+"."+inPkgName, inPkgName, true, false) - } - } - } - } - } -} - // Collect skip names and skip other annotations, such as go: and llgo: // llgo:skip symbol1 symbol2 ... // llgo:skipall @@ -298,6 +276,21 @@ func (p *context) collectSkip(line string, prefix int) { } } +func collectLinknameByDoc(prog llssa.Program, doc *ast.CommentGroup, fullName, inPkgName string) { + directives := directive.ParseGroup(doc) + for n := len(directives) - 1; n >= 0; n-- { + directive := directives[n] + if directive.Name != "go:linkname" && directive.Name != "llgo:link" { + continue + } + fields := strings.Fields(directive.Args) + if len(fields) >= 2 && fields[0] == inPkgName { + prog.SetLinkname(fullName, strings.Join(fields[1:], " ")) + return + } + } +} + func (p *context) processLinknameByDoc(doc *ast.CommentGroup, fullName, inPkgName string, isVar, allowExport bool) bool { if doc != nil { for n := len(doc.List) - 1; n >= 0; n-- { @@ -722,6 +715,9 @@ func (p *context) varOf(b llssa.Builder, v *ssa.Global) llssa.Expr { } panic("unreachable") } + if local, ok := p.localVariableAddress(b, v, name); ok { + return local + } ret := pkg.VarOf(name) if ret == nil { ret = pkg.NewVar(name, p.patchType(v.Type()), llssa.Background(vtype)) @@ -754,24 +750,59 @@ func (p *context) initPyModule() { } } -// ParsePkgSyntax parses AST of a package to check package-level compiler directives. -func ParsePkgSyntax(prog llssa.Program, pkg *types.Package, files []*ast.File) { +// ParsePkgSyntax collects declaration directives in one syntax pass before SSA +// creation. Directives that need an LLVM package (such as //export) are applied +// later by initFiles. +func ParsePkgSyntax(prog llssa.Program, fset *token.FileSet, pkg *types.Package, files []*ast.File) error { + if pkg == nil { + return nil + } + if prog.PackageSyntaxParsed(pkg) { + return nil + } ctx := &context{prog: prog} pkgPath := llssa.PathOf(pkg) for _, file := range files { for _, decl := range file.Decls { switch decl := decl.(type) { case *ast.FuncDecl: - fullName, _ := astFuncName(pkgPath, decl) + if err := locality.ValidateDoc(fset, decl.Doc); err != nil { + return err + } + if err := locality.ValidateFuncBody(fset, decl.Body); err != nil { + return err + } + fullName, inPkgName := astFuncName(pkgPath, decl) + collectLinknameByDoc(prog, decl.Doc, fullName, inPkgName) ctx.processNoInterfaceByDoc(decl.Doc, fullName) case *ast.GenDecl: - switch decl.Tok { - case token.TYPE: + if decl.Tok == token.VAR { + if len(decl.Specs) == 1 { + if names := decl.Specs[0].(*ast.ValueSpec).Names; len(names) == 1 { + inPkgName := names[0].Name + collectLinknameByDoc(prog, decl.Doc, pkgPath+"."+inPkgName, inPkgName) + } + } + vars, err := locality.ScanPackageVar(fset, decl) + if err != nil { + return err + } + for _, variable := range vars { + prog.SetLocalityInfo(llssa.FullName(pkg, variable.Name), variable.Info) + } + continue + } + if err := locality.ValidateNonPackageVar(fset, decl); err != nil { + return err + } + if decl.Tok == token.TYPE { handleTypeDecl(prog, pkg, decl) } } } } + prog.MarkPackageSyntaxParsed(pkg) + return nil } func handleTypeDecl(prog llssa.Program, pkg *types.Package, decl *ast.GenDecl) { diff --git a/cl/import_coverage_test.go b/cl/import_coverage_test.go index 345693b29c..4afa5b0110 100644 --- a/cl/import_coverage_test.go +++ b/cl/import_coverage_test.go @@ -62,7 +62,9 @@ func (A) StackedHidden() {} } prog := llssa.NewProgram(nil) pkg := types.NewPackage("example.com/p", "p") - ParsePkgSyntax(prog, pkg, []*ast.File{file}) + if err := ParsePkgSyntax(prog, fset, pkg, []*ast.File{file}); err != nil { + t.Fatal(err) + } ctx := &context{prog: prog} ctx.processNoInterfaceByDoc(nil, "example.com/p.NilDoc") @@ -70,6 +72,59 @@ func (A) StackedHidden() {} {Text: "// not a directive"}, {Text: "//go:nointerface"}, }}, "example.com/p.NonDirectiveStops") + + if !prog.PackageSyntaxParsed(pkg) { + t.Fatal("package syntax was not marked as parsed") + } + badFile, err := parser.ParseFile(fset, "bad.go", "package p\n//llgo:tls\nfunc Bad() {}\n", parser.ParseComments) + if err != nil { + t.Fatal(err) + } + if err := ParsePkgSyntax(prog, fset, pkg, []*ast.File{badFile}); err != nil { + t.Fatalf("already parsed package was scanned again: %v", err) + } +} + +func TestParsePkgSyntaxReportsLocalityErrors(t *testing.T) { + prog := llssa.NewProgram(nil) + if err := ParsePkgSyntax(prog, nil, nil, nil); err != nil { + t.Fatal(err) + } + tests := []struct { + name string + src string + want string + }{ + { + name: "function body", + src: "package p\nfunc f() {\n//llgo:gls\nvar value int\n_ = value\n}\n", + want: "package-level var", + }, + { + name: "package var", + src: "package p\n//llgo:tls extra\nvar Value int\n", + want: "does not accept arguments", + }, + { + name: "non-var declaration", + src: "package p\n//llgo:gls\nconst Value = 1\n", + want: "package-level var", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "p.go", test.src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + pkg := types.NewPackage("example.com/"+strings.ReplaceAll(test.name, " ", "-"), "p") + err = ParsePkgSyntax(llssa.NewProgram(nil), fset, pkg, []*ast.File{file}) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("ParsePkgSyntax error = %v, want %q", err, test.want) + } + }) + } } func TestPkgSymInfoAddSymAndInitLinknamesCoverage(t *testing.T) { @@ -166,13 +221,14 @@ func TestAstAndTypesFuncNameCoverage(t *testing.T) { } } -func TestPreCollectLinknames(t *testing.T) { +func TestParsePkgSyntaxCollectsLinknames(t *testing.T) { cases := []struct { name string directive string want string }{ {name: "go-linkname", directive: "//go:linkname Sigsetjmp C.sigsetjmp", want: "C.sigsetjmp"}, + {name: "go-linkname-tabs", directive: "//go:linkname\tSigsetjmp\tC.sigsetjmp", want: "C.sigsetjmp"}, {name: "llgo-linkname", directive: "//llgo:link Sigsetjmp C.sigsetjmp", want: "C.sigsetjmp"}, {name: "llgo-linkname-spaced", directive: "// llgo:link Sigsetjmp C.sigsetjmp", want: "C.sigsetjmp"}, } @@ -185,12 +241,33 @@ func TestPreCollectLinknames(t *testing.T) { t.Fatalf("ParseFile failed: %v", err) } prog := llssa.NewProgram(nil) - PreCollectLinknames(prog, llssa.PkgRuntime, []*ast.File{file}) + pkg := types.NewPackage(llssa.PkgRuntime, "runtime") + if err := ParsePkgSyntax(prog, fset, pkg, []*ast.File{file}); err != nil { + t.Fatal(err) + } if got, ok := prog.Linkname(llssa.PkgRuntime + ".Sigsetjmp"); !ok || got != tt.want { t.Fatalf("pre-collected linkname = (%q,%v), want (%q,%v)", got, ok, tt.want, true) } }) } + prog := llssa.NewProgram(nil) + collectLinknameByDoc(prog, &ast.CommentGroup{List: []*ast.Comment{{Text: "//go:linkname Other C.other"}}}, llssa.PkgRuntime+".Sigsetjmp", "Sigsetjmp") + if _, ok := prog.Linkname(llssa.PkgRuntime + ".Sigsetjmp"); ok { + t.Fatal("mismatched linkname was collected") + } +} + +func TestCollectLinknameByDocIgnoresOtherDirectives(t *testing.T) { + prog := llssa.NewProgram(nil) + doc := &ast.CommentGroup{List: []*ast.Comment{ + {Text: "//go:noinline"}, + {Text: "//llgo:tls"}, + }} + const fullName = "example.com/p.Value" + collectLinknameByDoc(prog, doc, fullName, "Value") + if _, ok := prog.Linkname(fullName); ok { + t.Fatal("non-link directives installed a linkname") + } } func TestBoolToUint8InvalidArgs(t *testing.T) { diff --git a/cl/instr.go b/cl/instr.go index ddbcb58806..a613dd38b4 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -1792,12 +1792,36 @@ func (p *context) deferStackOwner(fn *ssa.Function) llssa.Function { return owner } -func (p *context) emitDo(b llssa.Builder, act llssa.DoAction, ds *explicitDeferStack, fn llssa.Expr, buildCall func(llssa.Builder, llssa.Expr, ...llssa.Expr) llssa.Expr, args ...llssa.Expr) llssa.Expr { +func (p *context) emitDo(b llssa.Builder, act llssa.DoAction, ds *explicitDeferStack, mayRecover bool, fn llssa.Expr, buildCall func(llssa.Builder, llssa.Expr, ...llssa.Expr) llssa.Expr, args ...llssa.Expr) llssa.Expr { if ds != nil { - b.DeferTo(ds.owner, ds.stack, fn, buildCall, args...) + b.DeferToRecover(ds.owner, ds.stack, mayRecover, fn, buildCall, args...) return llssa.Nil } - return b.Do(act, fn, buildCall, args...) + switch act { + case llssa.Call, llssa.Go: + return b.Do(act, fn, buildCall, args...) + default: + b.DeferRecover(act, mayRecover, fn, buildCall, args...) + return llssa.Nil + } +} + +func (p *context) callMayRecover(v ssa.Value) bool { + switch v := v.(type) { + case *ssa.Builtin: + return false + case *ssa.Function: + return functionUsesRecover(v) + case *ssa.MakeClosure: + if fn, ok := v.Fn.(*ssa.Function); ok { + return functionUsesRecover(fn) + } + return true + case *ssa.Call: + // The deferred callee is the call result, not the factory function. + return true + } + return true } func (p *context) staticArrayLenBuiltinArg(b llssa.Builder, arg ssa.Value) (llssa.Expr, bool) { @@ -1952,6 +1976,7 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm p.recordCallerLocationForCall(b, call) p.emitPCLineLabel(b, call.Pos()) cv := call.Value + mayRecover := p.callMayRecover(cv) if mthd := call.Method; mthd != nil { reflectCheck := p.reflectTypeMethodCheck(call, mthd) o := p.compileValue(b, cv) @@ -1961,7 +1986,7 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm hasVArg = fnHasVArg } args := p.compileValues(b, call.Args, hasVArg) - ret = p.emitDo(b, act, ds, fn, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, ds, true, fn, llssa.Builder.Call, args...) if reflectCheck.Kind&llssa.ReflectTypeMethodByName != 0 && reflectCheck.Name == "" { b.MarkReflectTypeMethodByNameExpr(ret, 1) } @@ -1995,7 +2020,7 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm } } args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Builtin(fn), llssa.Builder.Call, args...) + ret = p.emitDo(b, act, ds, false, llssa.Builtin(fn), llssa.Builder.Call, args...) case *ssa.Function: aFn, pyFn, ftype := p.compileFunction(cv) // TODO(xsw): check ca != llssa.Call @@ -2004,13 +2029,13 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm p.inCFunc = true args := p.compileValues(b, args, kind) p.inCFunc = false - ret = p.emitDo(b, act, ds, aFn.Expr, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, ds, mayRecover, aFn.Expr, llssa.Builder.Call, args...) case goFunc: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, aFn.Expr, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, ds, mayRecover, aFn.Expr, llssa.Builder.Call, args...) case pyFunc: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, pyFn.Expr, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, ds, mayRecover, pyFn.Expr, llssa.Builder.Call, args...) case llgoPyList: args := p.compileValues(b, args, fnHasVArg) ret = b.PyList(args...) @@ -2084,33 +2109,33 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm b.Unreachable() case llgoAtomicLoad: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, ds, false, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomicLoad(b, args) }, args...) case llgoAtomicStore: args := p.compileValues(b, args, kind) - p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + p.emitDo(b, act, ds, false, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomicStore(b, args) }, args...) case llgoAtomicCmpXchg: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, ds, false, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomicCmpXchg(b, args) }, args...) case llgoAtomicCmpXchgOK: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, ds, false, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomicCmpXchgOK(b, args) }, args...) case llgoAtomicAddReturnNew: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, ds, false, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return b.BinOp(token.ADD, p.atomic(b, llssa.OpAdd, args), args[1]) }, args...) default: if ftype >= llgoAtomicOpBase && ftype <= llgoAtomicOpLast { args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, ds, false, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomic(b, llssa.AtomicOp(ftype-llgoAtomicOpBase), args) }, args...) } else { @@ -2120,7 +2145,7 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm default: fn := p.compileValue(b, cv) args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, fn, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, ds, mayRecover, fn, llssa.Builder.Call, args...) } return } diff --git a/cl/locality.go b/cl/locality.go new file mode 100644 index 0000000000..d9c561ae39 --- /dev/null +++ b/cl/locality.go @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cl + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "strings" + + "github.com/goplus/llgo/internal/locality" + localitylayout "github.com/goplus/llgo/internal/locality/layout" + llssa "github.com/goplus/llgo/ssa" +) + +// PrepareLocalVariables extracts replayable initializer helpers before Go SSA +// construction, then records the storage strategy selected by the independent +// package-layout planner. +func PrepareLocalVariables(prog llssa.Program, fset *token.FileSet, pkg *types.Package, info *types.Info, files []*ast.File) error { + if pkg == nil || info == nil { + return nil + } + path := llssa.PathOf(pkg) + prepared, err := locality.Prepare(fset, path, pkg, info, files, packageLocalities(prog, path)) + if err != nil { + return err + } + for name, local := range prepared { + prog.SetLocalityInfo(llssa.FullName(pkg, name), local) + } + for fullName := range prog.PackageLocalities(path) { + name := strings.TrimPrefix(fullName, path+".") + object, _ := pkg.Scope().Lookup(name).(*types.Var) + if object == nil { + return fmt.Errorf("locality layout: package %s has no variable %s", path, name) + } + _, _, _, err := prog.ResolveLocality(fullName) + if err != nil { + return err + } + prog.SetLocalStorage(fullName, localitylayout.StorageForType(object.Type())) + } + _, err = planLocalPackage(prog, pkg) + return err +} + +func validateLocalInitializers(prog llssa.Program, pkg *types.Package) error { + return locality.ValidatePrepared(llssa.PathOf(pkg), packageLocalities(prog, llssa.PathOf(pkg))) +} + +func packageLocalities(prog llssa.Program, pkgPath string) map[string]locality.Info { + prefix := pkgPath + "." + ret := make(map[string]locality.Info) + for name, info := range prog.PackageLocalities(pkgPath) { + ret[strings.TrimPrefix(name, prefix)] = info.Info + } + return ret +} + +func planLocalPackage(prog llssa.Program, pkg *types.Package) (localitylayout.Package, error) { + if pkg == nil { + return localitylayout.Package{}, nil + } + path := llssa.PathOf(pkg) + prefix := path + "." + decls := prog.PackageLocalities(path) + input := make([]localitylayout.Declaration, 0, len(decls)) + for fullName := range decls { + _, info, _, err := prog.ResolveLocality(fullName) + if err != nil { + return localitylayout.Package{}, err + } + name := strings.TrimPrefix(fullName, prefix) + object, _ := pkg.Scope().Lookup(name).(*types.Var) + if object == nil { + return localitylayout.Package{}, fmt.Errorf("locality layout: package %s has no variable %s", path, name) + } + input = append(input, localitylayout.Declaration{Name: fullName, Type: object.Type(), Info: info.Info}) + } + return localitylayout.Plan(path, input) +} diff --git a/cl/locality_lower.go b/cl/locality_lower.go new file mode 100644 index 0000000000..7fbf599e64 --- /dev/null +++ b/cl/locality_lower.go @@ -0,0 +1,417 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cl + +import ( + "fmt" + "go/token" + "go/types" + "strings" + + "github.com/goplus/llgo/internal/locality" + localitylayout "github.com/goplus/llgo/internal/locality/layout" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// localInitReady is part of the compiler/runtime ABI. Keep it in sync with +// runtime/internal/runtime.localInitReady. +const localInitReady = 2 + +type localVariable struct { + planned localitylayout.Variable + owner *localPackage +} + +type localPackage struct { + plan localitylayout.Package + typ llssa.Type + blockFunc llssa.Function + direct map[string]llssa.Global + init map[locality.Kind]*localInitializer +} + +type localInitializer struct { + guard llssa.Global + failureCache llssa.Global + dispatch llssa.Function + ensure llssa.Function +} + +type localBaseCacheKey struct { + block *ssa.BasicBlock + owner *localPackage +} + +type localEnsureCacheKey struct { + block *ssa.BasicBlock + owner *localPackage + kind locality.Kind +} + +// localityLowering owns all compiler state for TLS/GLS lowering. Only this +// value is embedded in the general compiler context. +type localityLowering struct { + packages map[string]*localPackage + variables map[*ssa.Global]*localVariable + function localityFunction +} + +type localityFunction struct { + block *ssa.BasicBlock + packageBases map[localBaseCacheKey]llssa.Expr + packageEnsures map[localEnsureCacheKey]bool + entry *localEntryContext +} + +type localEntryContext struct { + context llssa.Expr + previous llssa.Expr + entered bool +} + +func (p *context) prepareExportedLocalContext(f *ssa.Function) { + if !p.prog.NeedsLocalContext() || f == nil || f.Pkg == nil { + return + } + fullName := funcName(f.Pkg.Pkg, f, false) + if _, exported := p.pkg.ExportFuncs()[fullName]; !exported { + return + } + p.locality.function.entry = &localEntryContext{} +} + +func (p *context) enterExportedLocalContext(b llssa.Builder) { + entry := p.locality.function.entry + if entry == nil || entry.entered { + return + } + context, previous := b.EnterLocalContext() + entry.context = context + entry.previous = previous + entry.entered = true +} + +func (p *context) leaveExportedLocalContext(b llssa.Builder) { + entry := p.locality.function.entry + if entry != nil && entry.entered { + b.LeaveLocalContext(entry.context, entry.previous) + } +} + +func (p *context) prepareLocalVariables(pkg llssa.Package, globals []*ssa.Global) { + _, err := p.localPackageFor(p.goTyps, pkg, true) + if err != nil { + panic(err) + } + if p.locality.variables == nil { + p.locality.variables = make(map[*ssa.Global]*localVariable) + } + for _, global := range globals { + variable, ok, err := p.localVariableFor(pkg, global, true) + if err != nil { + panic(err) + } + if !ok { + continue + } + p.locality.variables[global] = variable + } +} + +// localVariableFor validates one Go SSA global and finds its declaring package +// plan. Both definitions and references use this path so forbidden linkname +// aliases and layout validation cannot diverge between lowering cases. +func (p *context) localVariableFor(pkg llssa.Package, global *ssa.Global, defineCurrent bool) (*localVariable, bool, error) { + fullName := llssa.FullName(global.Pkg.Pkg, global.Name()) + canonical, info, ok, err := p.prog.ResolveLocality(fullName) + if err != nil { + return nil, false, err + } + if !ok || info.Locality == locality.None { + return nil, false, nil + } + typesPkg := p.localTypesPackage(canonical) + if typesPkg == nil { + return nil, false, fmt.Errorf("missing types package for local variable %s", canonical) + } + owner, err := p.localPackageFor(typesPkg, pkg, defineCurrent && typesPkg == p.goTyps) + if err != nil { + return nil, false, err + } + planned, ok := owner.plan.Lookup(canonical) + if !ok { + return nil, false, fmt.Errorf("missing locality layout for %s", canonical) + } + return &localVariable{planned: planned, owner: owner}, true, nil +} + +func (p *context) localityGlobalStorage(pkg llssa.Package, global *ssa.Global, name string, typ types.Type, bg llssa.Background) (llssa.Global, bool) { + info, ok := p.resolveLocality(llssa.FullName(global.Pkg.Pkg, global.Name())) + if !ok || info.Locality == locality.None { + return pkg.NewVar(name, typ, bg), false + } + variable := p.locality.variables[global] + if variable == nil { + panic(fmt.Sprintf("missing locality layout for %s", name)) + } + if variable.planned.Storage == localitylayout.StoragePackage { + return nil, true + } + return variable.owner.direct[variable.planned.Name], false +} + +func (p *context) localityAllowsGlobalDebug(global *ssa.Global) bool { + variable := p.locality.variables[global] + return variable == nil || variable.planned.Storage == localitylayout.StorageNativeTLS +} + +func (p *context) localTypesPackage(fullName string) *types.Package { + matches := func(pkg *types.Package) bool { + if pkg == nil { + return false + } + prefix := llssa.PathOf(pkg) + "." + if !strings.HasPrefix(fullName, prefix) { + return false + } + name := strings.TrimPrefix(fullName, prefix) + _, ok := pkg.Scope().Lookup(name).(*types.Var) + return ok + } + if matches(p.goTyps) { + return p.goTyps + } + if p.goProg != nil { + for _, pkg := range p.goProg.AllPackages() { + if pkg != nil && matches(pkg.Pkg) { + return pkg.Pkg + } + } + } + for pkg := range p.loaded { + if matches(pkg) { + return pkg + } + } + return nil +} + +func (p *context) localPackageFor(typesPkg *types.Package, pkg llssa.Package, define bool) (*localPackage, error) { + if typesPkg == nil { + return nil, nil + } + path := llssa.PathOf(typesPkg) + if owner := p.locality.packages[path]; owner != nil { + return owner, nil + } + plan, err := planLocalPackage(p.prog, typesPkg) + if err != nil { + return nil, err + } + if len(plan.Variables) == 0 { + return nil, nil + } + if p.locality.packages == nil { + p.locality.packages = make(map[string]*localPackage) + } + owner := &localPackage{ + plan: plan, + direct: make(map[string]llssa.Global), + init: make(map[locality.Kind]*localInitializer), + } + p.locality.packages[path] = owner + p.buildLocalPackage(pkg, owner, define) + return owner, nil +} + +func (p *context) buildLocalPackage(pkg llssa.Package, owner *localPackage, define bool) { + for _, variable := range owner.plan.Variables { + if variable.Storage != localitylayout.StorageNativeTLS { + continue + } + typ := types.NewPointer(p.patchType(variable.Type)) + global := pkg.NewThreadLocalVar(variable.Name, typ, llssa.InGo) + owner.direct[variable.Name] = global + } + if len(owner.plan.Block) != 0 { + fields := make([]*types.Var, len(owner.plan.Block)) + for index, variable := range owner.plan.Block { + fields[index] = types.NewField(token.NoPos, nil, fmt.Sprintf("v%d", index), p.patchType(variable.Type), false) + } + structType := types.NewStruct(fields, nil) + owner.typ = p.prog.Type(structType, llssa.InGo) + cache := pkg.NewThreadLocalVar(localitylayout.BlockCacheName(owner.plan.Path), types.NewPointer(types.Typ[types.Uintptr]), llssa.InGo) + if define { + cache.InitNil() + } + result := types.NewPointer(structType) + owner.blockFunc = pkg.NewFunc(localitylayout.BlockName(owner.plan.Path), noArgResultSignature(result), llssa.InGo) + owner.blockFunc.Inline(llssa.AlwaysInline) + if define && !owner.blockFunc.HasBody() { + owner.blockFunc.BuildLocalPackageAccessor( + cache.Expr, + p.prog.IntVal(p.prog.SizeOf(owner.typ), p.prog.Uintptr()), + p.prog.IntVal(p.prog.AlignOf(owner.typ), p.prog.Uintptr()), + ) + } + } + for _, kind := range []locality.Kind{locality.Thread, locality.Goroutine} { + initializers := owner.plan.Initializers(kind) + if len(initializers) == 0 { + continue + } + owner.init[kind] = p.buildLocalInitializer(pkg, owner, kind, initializers, define) + } +} + +func (p *context) buildLocalInitializer(pkg llssa.Package, owner *localPackage, kind locality.Kind, initializers []localitylayout.Initializer, define bool) *localInitializer { + ret := &localInitializer{} + ret.guard = pkg.NewThreadLocalVar(localitylayout.GuardName(owner.plan.Path, kind), types.NewPointer(types.Typ[types.Uint8]), llssa.InGo) + ret.failureCache = pkg.NewThreadLocalVar(localitylayout.FailureCacheName(owner.plan.Path, kind), types.NewPointer(types.Typ[types.Uintptr]), llssa.InGo) + ret.dispatch = pkg.NewFunc(localitylayout.InitName(owner.plan.Path, kind), llssa.NoArgsNoRet, llssa.InGo) + ret.ensure = pkg.NewFunc(localitylayout.EnsureName(owner.plan.Path, kind), llssa.NoArgsNoRet, llssa.InGo) + ret.ensure.Inline(llssa.AlwaysInline) + if !define { + return ret + } + ret.guard.InitNil() + ret.failureCache.InitNil() + if !ret.dispatch.HasBody() { + b := ret.dispatch.MakeBody(1) + for _, initializer := range initializers { + helper := pkg.NewFunc(initializer.Name, llssa.NoArgsNoRet, llssa.InGo) + b.Call(helper.Expr) + } + b.Return() + b.EndBuild() + } + if !ret.ensure.HasBody() { + b := ret.ensure.MakeBody(3) + ready := b.BinOp(token.EQL, b.Load(ret.guard.Expr), p.prog.IntVal(localInitReady, p.prog.Byte())) + b.If(ready, ret.ensure.Block(2), ret.ensure.Block(1)) + b.SetBlock(ret.ensure.Block(1)) + closure := b.MakeClosure(ret.dispatch.Expr, nil) + b.Call( + pkg.RuntimeFunc("EnsureLocalInitializer"), + ret.guard.Expr, + ret.failureCache.Expr, + closure, + ) + b.Jump(ret.ensure.Block(2)) + b.SetBlock(ret.ensure.Block(2)) + b.Return() + b.EndBuild() + } + return ret +} + +func noArgResultSignature(result types.Type) *types.Signature { + results := types.NewTuple(types.NewVar(token.NoPos, nil, "", result)) + return types.NewSignatureType(nil, nil, nil, nil, results, false) +} + +func (p *context) localVariableAddr(b llssa.Builder, v *ssa.Global, info llssa.VariableLocality, name string) llssa.Expr { + variable := p.locality.variables[v] + if variable == nil { + var ok bool + var err error + variable, ok, err = p.localVariableFor(p.pkg, v, false) + if err != nil { + panic(err) + } + if !ok { + panic(fmt.Sprintf("missing locality metadata for %s", name)) + } + p.locality.variables[v] = variable + } + p.ensureLocalInitializer(b, variable.owner, info.Locality) + if variable.planned.Storage == localitylayout.StorageNativeTLS { + direct := variable.owner.direct[variable.planned.Name] + if direct == nil { + panic(fmt.Sprintf("missing native TLS storage for %s", name)) + } + return direct.Expr + } + base := p.localPackageBase(b, variable.owner) + return b.FieldAddr(base, variable.planned.Field) +} + +func (p *context) localVariableAddress(b llssa.Builder, variable *ssa.Global, name string) (llssa.Expr, bool) { + info, ok := p.resolveLocality(llssa.FullName(variable.Pkg.Pkg, variable.Name())) + if !ok || info.Locality == locality.None { + return llssa.Expr{}, false + } + return p.localVariableAddr(b, variable, info, name), true +} + +func (p *context) resolveLocality(name string) (llssa.VariableLocality, bool) { + _, info, ok, err := p.prog.ResolveLocality(name) + if err != nil { + panic(err) + } + return info, ok +} + +func (p *context) localPackageBase(b llssa.Builder, owner *localPackage) llssa.Expr { + state := &p.locality.function + for block := state.block; block != nil; block = block.Idom() { + if base, ok := state.packageBases[localBaseCacheKey{block: block, owner: owner}]; ok { + return base + } + } + base := b.Call(owner.blockFunc.Expr) + if state.block != nil { + if state.packageBases == nil { + state.packageBases = make(map[localBaseCacheKey]llssa.Expr) + } + state.packageBases[localBaseCacheKey{block: state.block, owner: owner}] = base + } + return base +} + +func (p *context) ensureLocalInitializer(b llssa.Builder, owner *localPackage, kind locality.Kind) { + initializer := owner.init[kind] + if initializer == nil { + return + } + state := &p.locality.function + for block := state.block; block != nil; block = block.Idom() { + if state.packageEnsures[localEnsureCacheKey{block: block, owner: owner, kind: kind}] { + return + } + } + b.Call(initializer.ensure.Expr) + if state.block != nil { + if state.packageEnsures == nil { + state.packageEnsures = make(map[localEnsureCacheKey]bool) + } + state.packageEnsures[localEnsureCacheKey{block: state.block, owner: owner, kind: kind}] = true + } +} + +func (p *context) initializeLocalGuards(b llssa.Builder) { + owner := p.locality.packages[llssa.PathOf(p.goTyps)] + if owner == nil { + return + } + for _, kind := range []locality.Kind{locality.Thread, locality.Goroutine} { + if initializer := owner.init[kind]; initializer != nil { + b.Store(initializer.guard.Expr, p.prog.IntVal(localInitReady, p.prog.Byte())) + } + } +} diff --git a/cl/locality_lower_test.go b/cl/locality_lower_test.go new file mode 100644 index 0000000000..8bae3152f3 --- /dev/null +++ b/cl/locality_lower_test.go @@ -0,0 +1,219 @@ +package cl + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/locality" + localitylayout "github.com/goplus/llgo/internal/locality/layout" + llssa "github.com/goplus/llgo/ssa" + "github.com/goplus/llgo/ssa/ssatest" + "golang.org/x/tools/go/ssa" +) + +func localitySSAGlobal(t *testing.T, path string) (*types.Package, *ssa.Global) { + t.Helper() + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", "package locality\nvar Value int\n", 0) + if err != nil { + t.Fatal(err) + } + info := newLocalityTypeInfo() + pkg, err := (&types.Config{}).Check(path, fset, []*ast.File{file}, info) + if err != nil { + t.Fatal(err) + } + goProg := ssa.NewProgram(fset, ssa.SanityCheckFunctions) + ssaPkg := goProg.CreatePackage(pkg, []*ast.File{file}, info, true) + global, ok := ssaPkg.Members["Value"].(*ssa.Global) + if !ok { + t.Fatalf("Value SSA member = %T", ssaPkg.Members["Value"]) + } + return pkg, global +} + +func TestLocalityLoweringResolution(t *testing.T) { + typesPkg, global := localitySSAGlobal(t, "example.com/lowering") + prog := ssatest.NewProgram(t, nil) + llvmPkg := prog.NewPackage(typesPkg.Name(), typesPkg.Path()) + ctx := &context{prog: prog, pkg: llvmPkg, goTyps: typesPkg} + + if variable, ok, err := ctx.localVariableFor(llvmPkg, global, true); err != nil || ok || variable != nil { + t.Fatalf("ordinary localVariableFor = %+v, %v, %v", variable, ok, err) + } + name := llssa.FullName(typesPkg, global.Name()) + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + prog.SetLocalStorage(name, llssa.LocalStorageNativeTLS) + variable, ok, err := ctx.localVariableFor(llvmPkg, global, true) + if err != nil || !ok || variable.planned.Storage != localitylayout.StorageNativeTLS { + t.Fatalf("local localVariableFor = %+v, %v, %v", variable, ok, err) + } + ctx.locality.variables = map[*ssa.Global]*localVariable{global: variable} + if !ctx.localityAllowsGlobalDebug(global) { + t.Fatal("native TLS was hidden from global debug info") + } + variable.planned.Storage = localitylayout.StoragePackage + if ctx.localityAllowsGlobalDebug(global) { + t.Fatal("package storage was exposed as a fixed debug global") + } + if !ctx.localityAllowsGlobalDebug(new(ssa.Global)) { + t.Fatal("ordinary global was hidden from debug info") + } + if owner, err := ctx.localPackageFor(typesPkg, llvmPkg, true); err != nil || owner != variable.owner { + t.Fatalf("cached localPackageFor = %p, %v; want %p", owner, err, variable.owner) + } + + loaded := types.NewPackage("example.com/loaded", "loaded") + loaded.Scope().Insert(types.NewVar(token.NoPos, loaded, "Value", types.Typ[types.Int])) + ctx.loaded = map[*types.Package]*pkgInfo{loaded: {kind: PkgDeclOnly}} + if got := ctx.localTypesPackage("example.com/loaded.Value"); got != loaded { + t.Fatalf("loaded localTypesPackage = %v, want %v", got, loaded) + } + if got := (&context{goProg: global.Pkg.Prog}).localTypesPackage(name); got != typesPkg { + t.Fatalf("SSA-program localTypesPackage = %v, want %v", got, typesPkg) + } + if got := (&context{}).localTypesPackage("example.com/missing.Value"); got != nil { + t.Fatalf("missing localTypesPackage = %v", got) + } + if owner, err := ctx.localPackageFor(nil, llvmPkg, false); err != nil || owner != nil { + t.Fatalf("nil localPackageFor = %v, %v", owner, err) + } + empty := types.NewPackage("example.com/empty", "empty") + if owner, err := ctx.localPackageFor(empty, llvmPkg, false); err != nil || owner != nil { + t.Fatalf("empty localPackageFor = %v, %v", owner, err) + } +} + +func TestLocalityLoweringDeclarationOnlyState(t *testing.T) { + typesPkg, global := localitySSAGlobal(t, "example.com/declaration") + name := llssa.FullName(typesPkg, global.Name()) + prog := ssatest.NewProgram(t, nil) + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + prog.SetLocalStorage(name, llssa.LocalStorageNativeTLS) + llvmPkg := prog.NewPackage(typesPkg.Name(), typesPkg.Path()) + ctx := &context{ + prog: prog, + pkg: llvmPkg, + goTyps: typesPkg, + locality: localityLowering{ + variables: make(map[*ssa.Global]*localVariable), + }, + } + + addr := ctx.localVariableAddr(nil, global, llssa.VariableLocality{Info: llssa.LocalityInfo{Locality: llssa.ThreadLocal}}, name) + if addr != ctx.locality.variables[global].owner.direct[name].Expr { + t.Fatal("localVariableAddr did not return declaration-only TLS storage") + } + + owner := &localPackage{plan: localitylayout.Package{Path: typesPkg.Path()}} + initializer := ctx.buildLocalInitializer(llvmPkg, owner, locality.Thread, []localitylayout.Initializer{{Name: typesPkg.Path() + ".initLocal", Order: 1}}, false) + if initializer.dispatch.HasBody() || initializer.ensure.HasBody() { + t.Fatal("declaration-only initializer unexpectedly defined a body") + } +} + +func TestLocalityLoweringDiagnostics(t *testing.T) { + typesPkg, global := localitySSAGlobal(t, "example.com/diagnostic") + name := llssa.FullName(typesPkg, global.Name()) + newProgram := func() llssa.Program { + prog := ssatest.NewProgram(t, nil) + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + return prog + } + + t.Run("missing types package", func(t *testing.T) { + ctx := &context{prog: newProgram()} + if _, _, err := ctx.localVariableFor(nil, global, false); err == nil || !strings.Contains(err.Error(), "missing types package") { + t.Fatalf("localVariableFor error = %v", err) + } + }) + + t.Run("invalid plan", func(t *testing.T) { + prog := newProgram() + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal, HasInitializer: true}) + ctx := &context{prog: prog, goTyps: typesPkg} + if _, err := ctx.localPackageFor(typesPkg, nil, false); err == nil || !strings.Contains(err.Error(), "inconsistent initializer metadata") { + t.Fatalf("localPackageFor error = %v", err) + } + if _, _, err := ctx.localVariableFor(nil, global, false); err == nil || !strings.Contains(err.Error(), "inconsistent initializer metadata") { + t.Fatalf("localVariableFor error = %v", err) + } + }) + + t.Run("stale plan", func(t *testing.T) { + prog := newProgram() + ctx := &context{prog: prog, goTyps: typesPkg} + ctx.locality.packages = map[string]*localPackage{ + typesPkg.Path(): {plan: localitylayout.Package{Path: typesPkg.Path()}}, + } + if _, _, err := ctx.localVariableFor(nil, global, false); err == nil || !strings.Contains(err.Error(), "missing locality layout") { + t.Fatalf("localVariableFor error = %v", err) + } + }) + + t.Run("local linkname", func(t *testing.T) { + prog := newProgram() + other := typesPkg.Path() + ".Other" + prog.SetLinkname(name, other) + prog.SetLinkname(other, name) + ctx := &context{prog: prog, goTyps: typesPkg} + if _, _, err := ctx.localVariableFor(nil, global, false); err == nil || !strings.Contains(err.Error(), "cannot use go:linkname") { + t.Fatalf("localVariableFor error = %v", err) + } + assertLocalityPanic(t, "resolveLocality", func() { ctx.resolveLocality(name) }) + assertLocalityPanic(t, "prepareLocalVariables", func() { ctx.prepareLocalVariables(nil, nil) }) + assertLocalityPanic(t, "localVariableAddr", func() { + ctx.localVariableAddr(nil, global, llssa.VariableLocality{Info: llssa.LocalityInfo{Locality: llssa.ThreadLocal}}, name) + }) + }) + + t.Run("imported metadata error", func(t *testing.T) { + current := types.NewPackage("example.com/current", "current") + prog := newProgram() + other := typesPkg.Path() + ".Other" + prog.SetLinkname(name, other) + prog.SetLinkname(other, name) + ctx := &context{prog: prog, goTyps: current} + assertLocalityPanic(t, "prepare imported local", func() { ctx.prepareLocalVariables(nil, []*ssa.Global{global}) }) + }) + + t.Run("missing prepared layout", func(t *testing.T) { + ctx := &context{prog: newProgram()} + assertLocalityPanic(t, "localityGlobalStorage", func() { + ctx.localityGlobalStorage(nil, global, name, global.Type(), llssa.InGo) + }) + }) + + t.Run("missing address metadata", func(t *testing.T) { + ctx := &context{prog: ssatest.NewProgram(t, nil)} + assertLocalityPanic(t, "localVariableAddr", func() { + ctx.localVariableAddr(nil, global, llssa.VariableLocality{Info: llssa.LocalityInfo{Locality: llssa.ThreadLocal}}, name) + }) + }) + + t.Run("missing native storage", func(t *testing.T) { + ctx := &context{locality: localityLowering{variables: map[*ssa.Global]*localVariable{ + global: { + planned: localitylayout.Variable{Declaration: localitylayout.Declaration{Name: name}, Storage: localitylayout.StorageNativeTLS}, + owner: &localPackage{direct: map[string]llssa.Global{}, init: map[locality.Kind]*localInitializer{}}, + }, + }}} + assertLocalityPanic(t, "localVariableAddr", func() { + ctx.localVariableAddr(nil, global, llssa.VariableLocality{Info: llssa.LocalityInfo{Locality: llssa.ThreadLocal}}, name) + }) + }) +} + +func assertLocalityPanic(t *testing.T, name string, fn func()) { + t.Helper() + defer func() { + if recover() == nil { + t.Fatalf("%s did not panic", name) + } + }() + fn() +} diff --git a/cl/locality_test.go b/cl/locality_test.go new file mode 100644 index 0000000000..877313838c --- /dev/null +++ b/cl/locality_test.go @@ -0,0 +1,708 @@ +package cl + +import ( + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "runtime" + "strings" + "testing" + + llssa "github.com/goplus/llgo/ssa" + "github.com/goplus/llgo/ssa/ssatest" + "golang.org/x/tools/go/ssa" +) + +func compileLocalitySource(t *testing.T, src string) (llssa.Program, string) { + t.Helper() + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + info := newLocalityTypeInfo() + imp := importer.Default() + pkg, err := (&types.Config{Importer: imp}).Check("example.com/locality", fset, files, info) + if err != nil { + t.Fatal(err) + } + prog := ssatest.NewProgramEx(t, nil, imp) + prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) + prog.SetRuntime(localityRuntimePackage()) + if err := ParsePkgSyntax(prog, fset, pkg, files); err != nil { + t.Fatal(err) + } + if err := PrepareLocalVariables(prog, fset, pkg, info, files); err != nil { + t.Fatal(err) + } + goProg := ssa.NewProgram(fset, ssa.SanityCheckFunctions) + ssaPkg := goProg.CreatePackage(pkg, files, info, true) + ssaPkg.Build() + compiled, err := NewPackage(prog, ssaPkg, files) + if err != nil { + t.Fatal(err) + } + return prog, compiled.String() +} + +func newLocalityTypeInfo() *types.Info { + return &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + Scopes: make(map[ast.Node]*types.Scope), + Instances: make(map[*ast.Ident]types.Instance), + } +} + +func localityRuntimePackage() *types.Package { + pkg := types.NewPackage(llssa.PkgRuntime, "runtime") + unsafePointer := types.Typ[types.UnsafePointer] + blocks := types.NewField(token.NoPos, pkg, "blocks", unsafePointer, false) + localContextName := types.NewTypeName(token.NoPos, pkg, "LocalContext", nil) + localContext := types.NewNamed(localContextName, types.NewStruct([]*types.Var{blocks}, nil), nil) + pkg.Scope().Insert(localContextName) + + localPackageParams := types.NewTuple( + types.NewParam(token.NoPos, pkg, "cache", types.NewPointer(types.Typ[types.Uintptr])), + types.NewParam(token.NoPos, pkg, "size", types.Typ[types.Uintptr]), + types.NewParam(token.NoPos, pkg, "align", types.Typ[types.Uintptr]), + ) + localPackageResults := types.NewTuple(types.NewParam(token.NoPos, pkg, "", types.Typ[types.UnsafePointer])) + pkg.Scope().Insert(types.NewFunc(token.NoPos, pkg, "LocalPackage", types.NewSignatureType(nil, nil, nil, localPackageParams, localPackageResults, false))) + + callback := types.NewSignatureType(nil, nil, nil, nil, nil, false) + ensureParams := types.NewTuple( + types.NewParam(token.NoPos, pkg, "state", types.NewPointer(types.Typ[types.Uint8])), + types.NewParam(token.NoPos, pkg, "failureCache", types.NewPointer(types.Typ[types.Uintptr])), + types.NewParam(token.NoPos, pkg, "initialize", callback), + ) + pkg.Scope().Insert(types.NewFunc(token.NoPos, pkg, "EnsureLocalInitializer", types.NewSignatureType(nil, nil, nil, ensureParams, nil, false))) + + contextPointer := types.NewPointer(localContext) + enterParams := types.NewTuple(types.NewParam(token.NoPos, pkg, "ctx", contextPointer)) + enterResults := types.NewTuple(types.NewParam(token.NoPos, pkg, "previous", types.Typ[types.Uintptr])) + pkg.Scope().Insert(types.NewFunc(token.NoPos, pkg, "EnterLocalContext", types.NewSignatureType(nil, nil, nil, enterParams, enterResults, false))) + leaveParams := types.NewTuple( + types.NewParam(token.NoPos, pkg, "ctx", contextPointer), + types.NewParam(token.NoPos, pkg, "previous", types.Typ[types.Uintptr]), + ) + pkg.Scope().Insert(types.NewFunc(token.NoPos, pkg, "LeaveLocalContext", types.NewSignatureType(nil, nil, nil, leaveParams, nil, false))) + return pkg +} + +func TestLocalPackageAccessorUsesDirectTLSCache(t *testing.T) { + const src = `package locality + +//llgo:gls +var pointer *int + +func value() *int { return pointer } +` + _, ir := compileLocalitySource(t, src) + if !strings.Contains(ir, `@"example.com/locality.__llgo_local_cache" = thread_local global i64 0`) { + t.Fatalf("package direct cache not found:\n%s", ir) + } + accessor := llvmFunction(t, ir, "example.com/locality.__llgo_local_block") + cacheLoad := `load i64, ptr @"example.com/locality.__llgo_local_cache"` + slow := `call ptr @"` + llssa.PkgRuntime + `.LocalPackage"(ptr @"example.com/locality.__llgo_local_cache"` + if loadAt, slowAt := strings.Index(accessor, cacheLoad), strings.Index(accessor, slow); loadAt < 0 || slowAt < 0 || loadAt >= slowAt { + t.Fatalf("accessor does not load its direct cache before the cold path:\n%s", accessor) + } + if got := strings.Count(accessor, "icmp "); got != 1 { + t.Fatalf("accessor comparisons = %d, want one cache check:\n%s", got, accessor) + } + if strings.Contains(accessor, "currentLocalContext") { + t.Fatalf("accessor still depends on runtime context layout:\n%s", accessor) + } +} + +func llvmFunction(t *testing.T, ir, name string) string { + t.Helper() + markerAt := strings.Index(ir, `@"`+name+`"(`) + if markerAt < 0 { + markerAt = strings.Index(ir, `@`+name+`(`) + } + if markerAt < 0 { + t.Fatalf("function %s not found:\n%s", name, ir) + } + start := strings.LastIndex(ir[:markerAt], "define ") + if start < 0 { + t.Fatalf("definition for %s not found", name) + } + end := strings.Index(ir[markerAt:], "\n}") + if end < 0 { + t.Fatalf("end of %s not found", name) + } + return ir[start : markerAt+end+2] +} + +func TestLocalityPlansNativeTLSAndSharedPointerBlock(t *testing.T) { + prog, ir := compileLocalitySource(t, `package locality + +var backing int +func scalar() int { return 42 } +func pointer() *int { return &backing } + +//llgo:tls +var tlsScalar = scalar() +//llgo:tls +var tlsPointer = pointer() +//llgo:gls +var glsScalar = scalar() +//llgo:gls +var glsPointer = pointer() + +func values() (int, *int, int, *int) { + return tlsScalar, tlsPointer, glsScalar, glsPointer +} +`) + + checks := map[string]struct { + kind llssa.Locality + storage llssa.LocalStorage + }{ + "tlsScalar": {llssa.ThreadLocal, llssa.LocalStorageNativeTLS}, + "tlsPointer": {llssa.ThreadLocal, llssa.LocalStoragePackage}, + "glsScalar": {llssa.GoroutineLocal, llssa.LocalStorageNativeTLS}, + "glsPointer": {llssa.GoroutineLocal, llssa.LocalStoragePackage}, + } + for name, want := range checks { + got, ok := prog.VariableLocality("example.com/locality." + name) + if !ok || got.Locality != want.kind || got.LocalStorage != want.storage { + t.Fatalf("%s metadata = %+v, %v", name, got, ok) + } + } + for _, name := range []string{"tlsScalar", "glsScalar"} { + if !strings.Contains(ir, `@"example.com/locality.`+name+`" = thread_local global i64`) { + t.Fatalf("%s is not native TLS:\n%s", name, ir) + } + } + for _, name := range []string{"tlsPointer", "glsPointer"} { + if strings.Contains(ir, `@"example.com/locality.`+name+`" = thread_local`) { + t.Fatalf("%s retained a pointer-bearing TLS global:\n%s", name, ir) + } + } + if got := strings.Count(ir, `@"example.com/locality.__llgo_local_cache" = thread_local global i64 0`); got != 1 { + t.Fatalf("package block caches = %d, want 1:\n%s", got, ir) + } + if got := strings.Count(ir, `call ptr @"github.com/goplus/llgo/runtime/internal/runtime.LocalPackage"`); got != 1 { + t.Fatalf("LocalPackage calls = %d, want one accessor definition:\n%s", got, ir) + } + values := llvmFunction(t, ir, "example.com/locality.values") + if got := strings.Count(values, `call ptr @"example.com/locality.__llgo_local_block"()`); got != 1 { + t.Fatalf("values package-base calls = %d, want 1:\n%s", got, values) + } + if got := strings.Count(values, `call void @"example.com/locality.__llgo_tls_init$ensure"()`); got != 1 { + t.Fatalf("values TLS ensure calls = %d, want 1:\n%s", got, values) + } + if got := strings.Count(values, `call void @"example.com/locality.__llgo_gls_init$ensure"()`); got != 1 { + t.Fatalf("values GLS ensure calls = %d, want 1:\n%s", got, values) + } + if !prog.NeedsLocalContext() { + t.Fatal("pointer-bearing local variables did not enable a local context") + } +} + +func TestLocalityDebugInfoOnlyUsesFixedGlobals(t *testing.T) { + EnableDebug(true) + EnableDbgSyms(true) + defer EnableDebug(false) + defer EnableDbgSyms(false) + _, ir := compileLocalitySource(t, `package locality + +//llgo:tls +var direct int + +//llgo:gls +var pointer *int + +func values() (int, *int) { return direct, pointer } +`) + + direct := `@"example.com/locality.direct" = thread_local global i64` + start := strings.Index(ir, direct) + if start < 0 { + t.Fatalf("native TLS global not found:\n%s", ir) + } + end := strings.IndexByte(ir[start:], '\n') + if end < 0 || !strings.Contains(ir[start:start+end], "!dbg") { + t.Fatalf("native TLS global has no debug metadata:\n%s", ir[start:]) + } + if strings.Contains(ir, `@"example.com/locality.pointer" =`) { + t.Fatalf("package-local pointer was emitted as a fixed debug global:\n%s", ir) + } +} + +func TestLocalityInitializersPreserveGoOrderPerKind(t *testing.T) { + _, ir := compileLocalitySource(t, `package locality + +func mark(value int) int { return value } +//llgo:tls +var t0 = mark(0) +//llgo:gls +var g0 = mark(1) +//llgo:tls +var t1 = mark(2) +func values() (int, int, int) { return t0, t1, g0 } +`) + tls := llvmFunction(t, ir, "example.com/locality.__llgo_tls_init") + gls := llvmFunction(t, ir, "example.com/locality.__llgo_gls_init") + first := strings.Index(tls, `__llgo_local_init_0`) + second := strings.Index(tls, `__llgo_local_init_2`) + if first < 0 || second < first || strings.Contains(tls, `__llgo_local_init_1`) { + t.Fatalf("TLS dispatcher order is wrong:\n%s", tls) + } + if !strings.Contains(gls, `__llgo_local_init_1`) || strings.Contains(gls, `__llgo_local_init_0`) || strings.Contains(gls, `__llgo_local_init_2`) { + t.Fatalf("GLS dispatcher contains the wrong helpers:\n%s", gls) + } + initBody := llvmFunction(t, ir, "example.com/locality.init") + for _, guard := range []string{"__llgo_tls_init$guard", "__llgo_gls_init$guard"} { + if !strings.Contains(initBody, `store i8 2, ptr @"example.com/locality.`+guard+`"`) { + t.Fatalf("package init does not mark %s ready:\n%s", guard, initBody) + } + } +} + +func TestDirectInitializerStillRequiresFailureContext(t *testing.T) { + prog, ir := compileLocalitySource(t, `package locality +func value() int { return 1 } +//llgo:tls +var localValue = value() +func get() int { return localValue } +`) + if !prog.NeedsLocalContext() { + t.Fatal("initializer failure storage did not enable a local context") + } + if strings.Contains(ir, `__llgo_local_block`) { + t.Fatalf("pointer-free package unexpectedly has a value block:\n%s", ir) + } + if !strings.Contains(ir, `@"example.com/locality.localValue" = thread_local global i64`) || !strings.Contains(ir, `EnsureLocalInitializer`) { + t.Fatalf("direct initializer lowering is incomplete:\n%s", ir) + } +} + +func TestZeroValueDirectLocalsNeedNoContext(t *testing.T) { + prog, ir := compileLocalitySource(t, `package locality +//llgo:tls +var threadValue int +//llgo:gls +var goroutineValue uintptr +func values() (int, uintptr) { return threadValue, goroutineValue } +`) + if prog.NeedsLocalContext() { + t.Fatal("pointer-free zero-value locals enabled a local context") + } + if strings.Contains(ir, `LocalPackage`) || strings.Contains(ir, `EnsureLocalInitializer`) { + t.Fatalf("zero-value direct locals emitted cold-path support:\n%s", ir) + } +} + +func TestExportedFunctionInstallsLocalContext(t *testing.T) { + _, ir := compileLocalitySource(t, `package locality +//llgo:gls +var pointer *int +//export Exported +func Exported(useLocal bool) *int { + if useLocal { return pointer } + return nil +} +`) + exported := llvmFunction(t, ir, "Exported") + if got := strings.Count(exported, "EnterLocalContext"); got != 1 { + t.Fatalf("exported function context entries = %d, want 1:\n%s", got, exported) + } + if got := strings.Count(exported, "LeaveLocalContext"); got != 2 { + t.Fatalf("exported function context leaves = %d, want 2:\n%s", got, exported) + } + assertTextOrder(t, exported, + "EnterLocalContext", + "__llgo_local_block", + "LeaveLocalContext", + "ret ptr", + ) +} + +func TestExportedNativeTLSNeedsNoLocalContext(t *testing.T) { + prog, ir := compileLocalitySource(t, `package locality +//llgo:tls +var scalar int +//export Exported +func Exported() int { return scalar } +`) + if prog.NeedsLocalContext() { + t.Fatal("zero-value native TLS enabled a local context") + } + exported := llvmFunction(t, ir, "Exported") + if strings.Contains(exported, "LocalContext") { + t.Fatalf("native-TLS-only export installed a local context:\n%s", exported) + } +} + +func assertTextOrder(t *testing.T, text string, wants ...string) { + t.Helper() + offset := 0 + for _, want := range wants { + index := strings.Index(text[offset:], want) + if index < 0 { + t.Fatalf("%q not found after offset %d:\n%s", want, offset, text) + } + offset += index + len(want) + } +} + +func TestLocalityRejectsLinknameAlias(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", `package locality +//llgo:gls +var target *int +//go:linkname alias example.com/locality.target +var alias *int +`, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + info := newLocalityTypeInfo() + pkg, err := (&types.Config{}).Check("example.com/locality", fset, []*ast.File{file}, info) + if err != nil { + t.Fatal(err) + } + prog := ssatest.NewProgram(t, nil) + if err := ParsePkgSyntax(prog, fset, pkg, []*ast.File{file}); err != nil { + t.Fatal(err) + } + if err := prog.ValidateLocalities(pkg.Path()); err == nil || !strings.Contains(err.Error(), "cannot reference local variable") { + t.Fatalf("ValidateLocalities error = %v", err) + } +} + +func TestLocalityCrossPackageAccessUsesFunctions(t *testing.T) { + fset := token.NewFileSet() + parse := func(name, source string) *ast.File { + file, err := parser.ParseFile(fset, name, source, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + return file + } + depFile := parse("dep.go", `package dep +func initialScalar() int { return 1 } +//llgo:tls +var scalar = initialScalar() +//llgo:gls +var pointer *int +func Values() (int, *int) { return scalar, pointer } +`) + rootFile := parse("root.go", `package root +import "example.com/dep" +func Values() (int, *int) { return dep.Values() } +`) + check := func(path string, files []*ast.File, imp types.Importer) (*types.Package, *types.Info) { + info := newLocalityTypeInfo() + pkg, err := (&types.Config{Importer: imp}).Check(path, fset, files, info) + if err != nil { + t.Fatal(err) + } + return pkg, info + } + depPkg, depInfo := check("example.com/dep", []*ast.File{depFile}, nil) + rootPkg, rootInfo := check("example.com/root", []*ast.File{rootFile}, importerFunc(func(path string) (*types.Package, error) { + if path == depPkg.Path() { + return depPkg, nil + } + return nil, types.Error{Msg: "unexpected import " + path} + })) + + prog := ssatest.NewProgram(t, nil) + prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) + prog.SetRuntime(localityRuntimePackage()) + for _, input := range []struct { + pkg *types.Package + info *types.Info + files []*ast.File + }{ + {depPkg, depInfo, []*ast.File{depFile}}, + {rootPkg, rootInfo, []*ast.File{rootFile}}, + } { + if err := ParsePkgSyntax(prog, fset, input.pkg, input.files); err != nil { + t.Fatal(err) + } + if err := PrepareLocalVariables(prog, fset, input.pkg, input.info, input.files); err != nil { + t.Fatal(err) + } + } + + goProg := ssa.NewProgram(fset, ssa.SanityCheckFunctions) + depSSA := goProg.CreatePackage(depPkg, []*ast.File{depFile}, depInfo, true) + rootSSA := goProg.CreatePackage(rootPkg, []*ast.File{rootFile}, rootInfo, true) + goProg.Build() + if _, err := NewPackage(prog, depSSA, []*ast.File{depFile}); err != nil { + t.Fatal(err) + } + root, err := NewPackage(prog, rootSSA, []*ast.File{rootFile}) + if err != nil { + t.Fatal(err) + } + ir := root.String() + if !strings.Contains(ir, `@"example.com/dep.Values"`) { + t.Fatalf("root package did not call the dependency function:\n%s", ir) + } + for _, symbol := range []string{"example.com/dep.scalar", "example.com/dep.pointer", "example.com/dep.__llgo_"} { + if strings.Contains(ir, symbol) { + t.Fatalf("root package referenced dependency locality storage %q:\n%s", symbol, ir) + } + } +} + +func TestParseRejectsLocalAliasInitializer(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", `package locality +//llgo:tls +var target int +//go:linkname alias example.com/locality.target +//llgo:tls +var alias = 1 +`, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + pkg, err := (&types.Config{}).Check("example.com/locality", fset, files, newLocalityTypeInfo()) + if err != nil { + t.Fatal(err) + } + prog := llssa.NewProgram(nil) + if err := ParsePkgSyntax(prog, fset, pkg, files); err == nil || !strings.Contains(err.Error(), "cannot apply to a //go:linkname variable") { + t.Fatalf("ParsePkgSyntax error = %v", err) + } +} + +func TestValidateLocalInitializers(t *testing.T) { + pkg := types.NewPackage("example.com/locality", "locality") + prog := ssatest.NewProgram(t, nil) + name := llssa.FullName(pkg, "value") + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal, HasInitializer: true}) + if err := validateLocalInitializers(prog, pkg); err == nil || !strings.Contains(err.Error(), "inconsistent initializer metadata") { + t.Fatalf("validateLocalInitializers error = %v", err) + } + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal, HasInitializer: true, InitFunc: "example.com/locality.init", InitOrder: 1}) + if err := validateLocalInitializers(prog, pkg); err != nil { + t.Fatal(err) + } +} + +func TestNewPackageReportsLocalityPreparationErrors(t *testing.T) { + tests := []struct { + name string + src string + parseSyntax bool + wantError string + }{ + { + name: "invalid directive", + src: `package locality +//llgo:tls +func invalid() {} +`, + wantError: "applies only to package-level var declarations", + }, + { + name: "unprepared initializer", + src: `package locality +func initialValue() int { return 1 } +//llgo:tls +var value = initialValue() +`, + parseSyntax: true, + wantError: "inconsistent initializer metadata", + }, + { + name: "linkname locality", + src: `package locality +//llgo:tls +var target int +//go:linkname alias example.com/locality.target +//llgo:gls +var alias int +`, + wantError: "cannot apply to a //go:linkname variable", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", tt.src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + info := newLocalityTypeInfo() + pkg, err := (&types.Config{}).Check("example.com/locality", fset, files, info) + if err != nil { + t.Fatal(err) + } + goProg := ssa.NewProgram(fset, ssa.SanityCheckFunctions) + ssaPkg := goProg.CreatePackage(pkg, files, info, true) + ssaPkg.Build() + prog := ssatest.NewProgram(t, nil) + if tt.parseSyntax { + if err := ParsePkgSyntax(prog, fset, pkg, files); err != nil { + t.Fatal(err) + } + } + if _, err := NewPackage(prog, ssaPkg, files); err == nil || !strings.Contains(err.Error(), tt.wantError) { + t.Fatalf("NewPackage error = %v, want %q", err, tt.wantError) + } + }) + } +} + +func TestNewPackageValidatesPreloadedLocalityMetadata(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", `package locality +var value int +`, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + info := newLocalityTypeInfo() + pkg, err := (&types.Config{}).Check("example.com/locality", fset, files, info) + if err != nil { + t.Fatal(err) + } + goProg := ssa.NewProgram(fset, ssa.SanityCheckFunctions) + ssaPkg := goProg.CreatePackage(pkg, files, info, true) + ssaPkg.Build() + + prog := ssatest.NewProgram(t, nil) + name := llssa.FullName(pkg, "value") + prog.SetLocalityInfo(name, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + prog.SetLinkname(name, name+"Alias") + if _, err := NewPackage(prog, ssaPkg, files); err == nil || !strings.Contains(err.Error(), "cannot use go:linkname") { + t.Fatalf("NewPackage locality validation error = %v", err) + } +} + +func TestParseRejectsExportedLocalVariable(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", `package locality +//llgo:tls +var Value int +`, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + pkg, err := (&types.Config{}).Check("example.com/locality", fset, files, newLocalityTypeInfo()) + if err != nil { + t.Fatal(err) + } + prog := ssatest.NewProgram(t, nil) + if err := ParsePkgSyntax(prog, fset, pkg, files); err == nil || !strings.Contains(err.Error(), "requires an unexported package variable") { + t.Fatalf("ParsePkgSyntax error = %v", err) + } +} + +func TestPrepareLocalVariablesEarlyReturns(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + pkg := types.NewPackage("example.com/locality", "locality") + info := &types.Info{} + if err := PrepareLocalVariables(prog, nil, nil, info, nil); err != nil { + t.Fatal(err) + } + if err := PrepareLocalVariables(prog, nil, pkg, nil, nil); err != nil { + t.Fatal(err) + } + if err := PrepareLocalVariables(prog, nil, pkg, info, nil); err != nil { + t.Fatal(err) + } + value := types.NewVar(token.NoPos, pkg, "value", types.Typ[types.Int]) + pkg.Scope().Insert(value) + prog.SetLocalityInfo(llssa.FullName(pkg, value.Name()), llssa.LocalityInfo{Locality: llssa.ThreadLocal, HasInitializer: true}) + info.InitOrder = []*types.Initializer{{Lhs: []*types.Var{value}, Rhs: ast.NewIdent("rhs")}} + if err := PrepareLocalVariables(prog, nil, pkg, info, nil); err == nil || !strings.Contains(err.Error(), "without syntax files") { + t.Fatalf("PrepareLocalVariables without files error = %v", err) + } + (&context{}).initializeLocalGuards(nil) +} + +func TestPrepareLocalVariablesRejectsInvalidMetadata(t *testing.T) { + t.Run("missing object", func(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + pkg := types.NewPackage("example.com/missing", "missing") + prog.SetLocalityInfo(llssa.FullName(pkg, "Value"), llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + if err := PrepareLocalVariables(prog, nil, pkg, &types.Info{}, nil); err == nil || !strings.Contains(err.Error(), "has no variable") { + t.Fatalf("PrepareLocalVariables error = %v", err) + } + }) + t.Run("local linkname", func(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + pkg := types.NewPackage("example.com/cycle", "cycle") + first := llssa.FullName(pkg, "First") + second := llssa.FullName(pkg, "Second") + pkg.Scope().Insert(types.NewVar(token.NoPos, pkg, "First", types.Typ[types.Int])) + prog.SetLocalityInfo(first, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + prog.SetLinkname(first, second) + prog.SetLinkname(second, first) + if err := PrepareLocalVariables(prog, nil, pkg, &types.Info{}, nil); err == nil || !strings.Contains(err.Error(), "cannot use go:linkname") { + t.Fatalf("PrepareLocalVariables error = %v", err) + } + }) +} + +func TestPlanLocalPackageDiagnostics(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + if plan, err := planLocalPackage(prog, nil); err != nil || len(plan.Variables) != 0 { + t.Fatalf("nil package plan = %+v, %v", plan, err) + } + + pkg := types.NewPackage("example.com/plan", "plan") + ordinary := llssa.FullName(pkg, "Ordinary") + pkg.Scope().Insert(types.NewVar(token.NoPos, pkg, "Ordinary", types.Typ[types.Int])) + prog.SetLocalStorage(ordinary, llssa.LocalStorageNativeTLS) + if plan, err := planLocalPackage(prog, pkg); err != nil || len(plan.Variables) != 0 { + t.Fatalf("non-local metadata plan = %+v, %v", plan, err) + } + + missing := llssa.FullName(pkg, "Missing") + prog.SetLocalityInfo(missing, llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + if _, err := planLocalPackage(prog, pkg); err == nil || !strings.Contains(err.Error(), "has no variable") { + t.Fatalf("missing object plan error = %v", err) + } +} + +func TestLocalInitializerNameCollision(t *testing.T) { + prog, _ := compileLocalitySource(t, `package locality +func __llgo_local_init_0() {} +//llgo:tls +var value = 1 +`) + info, ok := prog.VariableLocality("example.com/locality.value") + if !ok || !strings.HasSuffix(info.InitFunc, ".__llgo_local_init_1") || info.InitOrder != 1 { + t.Fatalf("value metadata = %+v, %v", info, ok) + } +} + +func TestNamedPointerLocalUsesPackageStorage(t *testing.T) { + prog, ir := compileLocalitySource(t, `package locality +type Handle struct { Pointer *int } +func makeHandle() Handle { return Handle{} } +//llgo:tls +var value = makeHandle() +func get() Handle { return value } +`) + info, ok := prog.VariableLocality("example.com/locality.value") + if !ok || info.LocalStorage != llssa.LocalStoragePackage { + t.Fatalf("named pointer metadata = %+v, %v", info, ok) + } + if !strings.Contains(ir, `call ptr @"example.com/locality.__llgo_local_block"()`) { + t.Fatalf("named pointer did not use package storage:\n%s", ir) + } +} diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index 325f062078..731b4df429 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -741,8 +741,8 @@ func TestEmitDoWithExplicitDeferStack(t *testing.T) { b.SetBlockEx(owner.Block(0), llssa.BeforeLast, true) ctx := &context{} - ctx.emitDo(b, llssa.DeferInLoop, &explicitDeferStack{stack: stack, owner: owner}, callee.Expr, llssa.Builder.Call) - ctx.emitDo(b, llssa.DeferAlways, nil, callee.Expr, llssa.Builder.Call) + ctx.emitDo(b, llssa.DeferInLoop, &explicitDeferStack{stack: stack, owner: owner}, false, callee.Expr, llssa.Builder.Call) + ctx.emitDo(b, llssa.DeferAlways, nil, false, callee.Expr, llssa.Builder.Call) b.DeferStackDrain() b.RunDefers() b.Return() @@ -878,7 +878,7 @@ func TestEmitDoWithoutExplicitDeferStack(t *testing.T) { b := fn.MakeBody(1) ctx := &context{} - got := ctx.emitDo(b, llssa.Call, nil, callee.Expr, llssa.Builder.Call) + got := ctx.emitDo(b, llssa.Call, nil, false, callee.Expr, llssa.Builder.Call) if got.IsNil() { t.Fatal("emitDo without explicit defer stack should return direct call result") } diff --git a/cl/static_init.go b/cl/static_init.go index bbbe94cfb4..fd0f238151 100644 --- a/cl/static_init.go +++ b/cl/static_init.go @@ -85,6 +85,11 @@ func (p *context) collectStaticGlobalInits(pkg *ssa.Package) { if _, rewritten := p.rewriteValue(globalName); rewritten { continue } + if info, ok := p.resolveLocality(llssa.FullName(global.Pkg.Pkg, global.Name())); ok && info.Locality != llssa.LocalityNone { + // Local initializers must remain executable so they can populate the + // current context rather than a process-wide LLVM initializer. + continue + } globals[global] = none{} } } diff --git a/internal/build/build.go b/internal/build/build.go index 2de1f96536..fced4cb4b9 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -394,11 +394,27 @@ func Do(args []string, conf *Config) ([]Package, error) { return prog.TypeSizes(sizes) } dedup := packages.NewDeduper() + var syntaxErr error + var syntaxErrMu sync.Mutex + recordSyntaxErr := func(err error) { + syntaxErrMu.Lock() + defer syntaxErrMu.Unlock() + if syntaxErr == nil { + syntaxErr = err + } + } + loadSyntaxErr := func() error { + syntaxErrMu.Lock() + defer syntaxErrMu.Unlock() + return syntaxErr + } dedup.SetPreload(func(pkg *types.Package, files []*ast.File) { if llruntime.SkipToBuild(pkg.Path()) { return } - cl.ParsePkgSyntax(prog, pkg, files) + if err := cl.ParsePkgSyntax(prog, cfg.Fset, pkg, files); err != nil { + recordSyntaxErr(err) + } }) if patterns == nil { @@ -421,6 +437,9 @@ func Do(args []string, conf *Config) ([]Package, error) { if err != nil { return nil, err } + if err := loadSyntaxErr(); err != nil { + return nil, err + } if conf.AllowNoBody { allowMissingFunctionBodies(initial) } @@ -455,6 +474,9 @@ func Do(args []string, conf *Config) ([]Package, error) { if err != nil { return nil, err } + if err := loadSyntaxErr(); err != nil { + return nil, err + } prog.SetRuntime(func() *types.Package { return altPkgs[0].Types @@ -462,7 +484,9 @@ func Do(args []string, conf *Config) ([]Package, error) { prog.SetPython(func() *types.Package { return dedup.Check(llssa.PkgPython).Types }) - preCollectRuntimeLinknames(prog, altPkgs) + if err := prepareLocalVariables(prog, initial, altPkgs); err != nil { + return nil, err + } buildMode := ssaBuildMode cabiOptimize := true @@ -1872,13 +1896,22 @@ func altPkgs(initial []*packages.Package, conf *Config, alts ...string) []string return alts } -func preCollectRuntimeLinknames(prog llssa.Program, pkgs []*packages.Package) { - for _, pkg := range pkgs { - if pkg != nil && pkg.PkgPath == llssa.PkgRuntime && len(pkg.Syntax) != 0 { - cl.PreCollectLinknames(prog, pkg.PkgPath, pkg.Syntax) - return +func prepareLocalVariables(prog llssa.Program, groups ...[]*packages.Package) error { + seen := make(map[*types.Package]bool) + var firstErr error + for _, roots := range groups { + packages.Visit(roots, nil, func(p *packages.Package) { + if firstErr != nil || p.Types == nil || p.IllTyped || seen[p.Types] { + return + } + seen[p.Types] = true + firstErr = cl.PrepareLocalVariables(prog, p.Fset, p.Types, p.TypesInfo, p.Syntax) + }) + if firstErr != nil { + return firstErr } } + return nil } func altSSAPkgs(prog *ssa.Program, patches cl.Patches, alts []*packages.Package, conf *Config, verbose bool) { diff --git a/internal/build/build_test.go b/internal/build/build_test.go index c26241cc20..aba4518d47 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -10,6 +10,7 @@ import ( "go/ast" "go/parser" "go/token" + "go/types" "io" "os" "os/exec" @@ -19,6 +20,7 @@ import ( "strings" "testing" + "github.com/goplus/llgo/cl" "github.com/goplus/llgo/internal/buildenv" "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/lto" @@ -532,7 +534,7 @@ func TestCmpTestNonexistentPatternReturnsError(t *testing.T) { } } -func TestPreCollectRuntimeLinknames(t *testing.T) { +func TestParsePkgSyntaxCollectsRuntimeLinknames(t *testing.T) { prog := llssa.NewProgram(nil) fset := token.NewFileSet() file, err := parser.ParseFile(fset, "runtime.go", `package runtime @@ -543,15 +545,68 @@ func Sigsetjmp() if err != nil { t.Fatalf("ParseFile failed: %v", err) } - preCollectRuntimeLinknames(prog, []*packages.Package{{ - PkgPath: llssa.PkgRuntime, - Syntax: []*ast.File{file}, - }}) + pkg := types.NewPackage(llssa.PkgRuntime, "runtime") + if err := cl.ParsePkgSyntax(prog, fset, pkg, []*ast.File{file}); err != nil { + t.Fatal(err) + } if got, ok := prog.Linkname(llssa.PkgRuntime + ".Sigsetjmp"); !ok || got != "C.sigsetjmp" { t.Fatalf("pre-collected runtime linkname = (%q,%v), want (%q,%v)", got, ok, "C.sigsetjmp", true) } } +func TestPrepareLocalVariables(t *testing.T) { + newLocalPackage := func(path string, withSyntax bool) (*packages.Package, *ast.File) { + pkg := types.NewPackage(path, "local") + value := types.NewVar(token.NoPos, pkg, "value", types.Typ[types.Int]) + pkg.Scope().Insert(value) + info := &types.Info{ + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + InitOrder: []*types.Initializer{{Lhs: []*types.Var{value}, Rhs: ast.NewIdent("rhs")}}, + } + loaded := &packages.Package{Types: pkg, TypesInfo: info} + var file *ast.File + if withSyntax { + file = &ast.File{Name: ast.NewIdent("local")} + loaded.Syntax = []*ast.File{file} + } + return loaded, file + } + + t.Run("filters and deduplicates packages", func(t *testing.T) { + prog := llssa.NewProgram(nil) + loaded, file := newLocalPackage("example.com/local", true) + prog.SetLocalityInfo("example.com/local.value", llssa.LocalityInfo{Locality: llssa.ThreadLocal, HasInitializer: true}) + duplicate := *loaded + + err := prepareLocalVariables(prog, + []*packages.Package{{}, {Types: types.NewPackage("example.com/bad", "bad"), IllTyped: true}, loaded}, + []*packages.Package{&duplicate}, + ) + if err != nil { + t.Fatal(err) + } + if got := len(file.Decls); got != 1 { + t.Fatalf("generated initializer declarations = %d, want 1", got) + } + }) + + t.Run("returns dependency error", func(t *testing.T) { + prog := llssa.NewProgram(nil) + dependency, _ := newLocalPackage("example.com/dependency", false) + prog.SetLocalityInfo("example.com/dependency.value", llssa.LocalityInfo{Locality: llssa.GoroutineLocal, HasInitializer: true}) + root := &packages.Package{ + Types: types.NewPackage("example.com/root", "root"), + Imports: map[string]*packages.Package{"example.com/dependency": dependency}, + } + + err := prepareLocalVariables(prog, []*packages.Package{root}) + if err == nil || !strings.Contains(err.Error(), "without syntax files") { + t.Fatalf("prepareLocalVariables error = %v", err) + } + }) +} + func TestLTOEnabledDefault(t *testing.T) { host := &Config{Target: ""} if host.ltoEnabled() { @@ -879,6 +934,70 @@ func F() {} pkgs[0].LPkg.Prog.Dispose() } +func TestDoReportsLocalityDirectiveError(t *testing.T) { + file := filepath.Join(t.TempDir(), "invalid_locality.go") + if err := os.WriteFile(file, []byte(`package invalidlocality + +//llgo:tls +func Invalid() {} +`), 0o644); err != nil { + t.Fatal(err) + } + conf := NewDefaultConf(ModeGen) + if _, err := Do([]string{file}, conf); err == nil || !strings.Contains(err.Error(), "applies only to package-level var declarations") { + t.Fatalf("Do error = %v, want locality directive diagnostic", err) + } +} + +func TestDoRejectsLocalityLinkname(t *testing.T) { + file := filepath.Join(t.TempDir(), "invalid_locality_alias.go") + if err := os.WriteFile(file, []byte(`package invalidlocalityalias + +import _ "unsafe" + +//llgo:tls +var target int + +//go:linkname alias example.com/target.value +//llgo:tls +var alias = 1 +`), 0o644); err != nil { + t.Fatal(err) + } + conf := NewDefaultConf(ModeGen) + if _, err := Do([]string{file}, conf); err == nil || !strings.Contains(err.Error(), "cannot apply to a //go:linkname variable") { + t.Fatalf("Do error = %v, want locality linkname diagnostic", err) + } +} + +func TestDoReportsAltPackageLocalityDirectiveError(t *testing.T) { + root := t.TempDir() + runtimeDir := filepath.Join(root, "runtime") + runtimePkgDir := filepath.Join(runtimeDir, "internal", "runtime") + if err := os.MkdirAll(runtimePkgDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(runtimeDir, "go.mod"), []byte("module github.com/goplus/llgo/runtime\n\ngo 1.24.0\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(runtimePkgDir, "runtime.go"), []byte(`package runtime + +//llgo:gls +func Invalid() {} +`), 0o644); err != nil { + t.Fatal(err) + } + file := filepath.Join(root, "main.go") + if err := os.WriteFile(file, []byte("package main\nfunc main() {}\n"), 0o644); err != nil { + t.Fatal(err) + } + t.Setenv("LLGO_ROOT", root) + conf := NewDefaultConf(ModeGen) + if _, err := Do([]string{file}, conf); err == nil || !strings.Contains(err.Error(), "applies only to package-level var declarations") { + t.Fatalf("Do error = %v, want alternate-package locality directive diagnostic", err) + } +} + func TestFormatPackageError(t *testing.T) { tests := []struct { name string diff --git a/internal/build/collect.go b/internal/build/collect.go index f482762fb7..c01a813a26 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -111,6 +111,7 @@ func (c *context) collectCommonInputs(m *manifestBuilder) { m.common.GoGlobalDCE = c.buildConf.goGlobalDCEEnabled() m.common.EmitDWARF = shouldEmitDebugInfo(c.buildConf, &c.crossCompile) m.common.PCLNMode = effectivePCLNMode(c.buildConf).String() + m.common.LocalContext = c.prog != nil && c.prog.NeedsLocalContext() // Compiler configuration if c.crossCompile.CC != "" { diff --git a/internal/build/collect_test.go b/internal/build/collect_test.go index 752205d083..4103b1ec0d 100644 --- a/internal/build/collect_test.go +++ b/internal/build/collect_test.go @@ -30,6 +30,7 @@ import ( "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/lto" "github.com/goplus/llgo/internal/packages" + llssa "github.com/goplus/llgo/ssa" gopackages "golang.org/x/tools/go/packages" ) @@ -359,6 +360,78 @@ func TestCollectFingerprintCanonicalizesPCLNEnvironment(t *testing.T) { } } +func TestCollectFingerprintLocalContextMode(t *testing.T) { + td := t.TempDir() + goFile := filepath.Join(td, "state.go") + if err := os.WriteFile(goFile, []byte("package state"), 0644); err != nil { + t.Fatal(err) + } + newPackage := func() *aPackage { + return &aPackage{Package: &packages.Package{ + ID: "example.com/state", + PkgPath: "example.com/state", + GoFiles: []string{goFile}, + }} + } + newContext := func(prog llssa.Program) *context { + return &context{ + conf: &packages.Config{}, + prog: prog, + buildConf: &Config{Goos: "linux", Goarch: "amd64"}, + crossCompile: crosscompile.Export{LLVMTarget: "x86_64-unknown-linux"}, + } + } + fingerprint := func(prog llssa.Program) (*aPackage, manifestData) { + pkg := newPackage() + if err := newContext(prog).collectFingerprint(pkg); err != nil { + t.Fatal(err) + } + data, err := decodeManifest(pkg.Manifest) + if err != nil { + t.Fatal(err) + } + return pkg, data + } + + plain, plainManifest := fingerprint(llssa.NewProgram(nil)) + nativeProg := llssa.NewProgram(nil) + nativeProg.SetLocalityInfo("example.com/state.value", llssa.LocalityInfo{Locality: llssa.ThreadLocal}) + nativeProg.SetLocalStorage("example.com/state.value", llssa.LocalStorageNativeTLS) + native, nativeManifest := fingerprint(nativeProg) + contextProg := llssa.NewProgram(nil) + contextProg.SetLocalityInfo("example.com/state.value", llssa.LocalityInfo{Locality: llssa.GoroutineLocal}) + contextProg.SetLocalStorage("example.com/state.value", llssa.LocalStoragePackage) + withContext, contextManifest := fingerprint(contextProg) + initializedProg := llssa.NewProgram(nil) + initializedProg.SetLocalityInfo("example.com/state.value", llssa.LocalityInfo{ + Locality: llssa.ThreadLocal, + HasInitializer: true, + InitFunc: "example.com/state.__llgo_local_init_0", + InitOrder: 1, + }) + initializedProg.SetLocalStorage("example.com/state.value", llssa.LocalStorageNativeTLS) + initialized, initializedManifest := fingerprint(initializedProg) + + if plain.Fingerprint != native.Fingerprint { + t.Fatal("native TLS changed the package cache fingerprint") + } + if withContext.Fingerprint == plain.Fingerprint { + t.Fatal("local-context and plain builds shared a package cache fingerprint") + } + if initialized.Fingerprint == plain.Fingerprint { + t.Fatal("initialized native TLS and plain builds shared a package cache fingerprint") + } + if plainManifest.Common.LocalContext || nativeManifest.Common.LocalContext { + t.Fatal("plain or native-TLS manifest enabled the local context") + } + if !contextManifest.Common.LocalContext { + t.Fatal("context-backed locality was not recorded in the manifest") + } + if !initializedManifest.Common.LocalContext { + t.Fatal("native TLS initializer failure storage was not recorded in the manifest") + } +} + func TestDevLTOGlobalDCECollectFingerprint(t *testing.T) { td := t.TempDir() diff --git a/internal/build/fingerprint.go b/internal/build/fingerprint.go index aba29f23c1..82eeeda747 100644 --- a/internal/build/fingerprint.go +++ b/internal/build/fingerprint.go @@ -112,24 +112,25 @@ func (s *envSection) empty() bool { } type commonSection struct { - AbiMode string `yaml:"ABI_MODE,omitempty"` - BuildTags []string `yaml:"BUILD_TAGS,omitempty"` - Target string `yaml:"TARGET,omitempty"` - TargetABI string `yaml:"TARGET_ABI,omitempty"` - GoGlobalDCE bool `yaml:"GO_GLOBAL_DCE,omitempty"` - EmitDWARF bool `yaml:"EMIT_DWARF,omitempty"` - PCLNMode string `yaml:"PCLN_MODE,omitempty"` - CC string `yaml:"CC,omitempty"` - CCFlags []string `yaml:"CCFLAGS,omitempty"` - CFlags []string `yaml:"CFLAGS,omitempty"` - LDFlags []string `yaml:"LDFLAGS,omitempty"` - Linker string `yaml:"LINKER,omitempty"` - ExtraFiles []fileDigest `yaml:"EXTRA_FILES,omitempty"` + AbiMode string `yaml:"ABI_MODE,omitempty"` + BuildTags []string `yaml:"BUILD_TAGS,omitempty"` + Target string `yaml:"TARGET,omitempty"` + TargetABI string `yaml:"TARGET_ABI,omitempty"` + GoGlobalDCE bool `yaml:"GO_GLOBAL_DCE,omitempty"` + EmitDWARF bool `yaml:"EMIT_DWARF,omitempty"` + PCLNMode string `yaml:"PCLN_MODE,omitempty"` + LocalContext bool `yaml:"LOCAL_CONTEXT,omitempty"` + CC string `yaml:"CC,omitempty"` + CCFlags []string `yaml:"CCFLAGS,omitempty"` + CFlags []string `yaml:"CFLAGS,omitempty"` + LDFlags []string `yaml:"LDFLAGS,omitempty"` + Linker string `yaml:"LINKER,omitempty"` + ExtraFiles []fileDigest `yaml:"EXTRA_FILES,omitempty"` } func (s *commonSection) empty() bool { return s.AbiMode == "" && len(s.BuildTags) == 0 && s.Target == "" && s.TargetABI == "" && - !s.GoGlobalDCE && !s.EmitDWARF && s.PCLNMode == "" && s.CC == "" && len(s.CCFlags) == 0 && len(s.CFlags) == 0 && len(s.LDFlags) == 0 && + !s.GoGlobalDCE && !s.EmitDWARF && s.PCLNMode == "" && !s.LocalContext && s.CC == "" && len(s.CCFlags) == 0 && len(s.CFlags) == 0 && len(s.LDFlags) == 0 && s.Linker == "" && len(s.ExtraFiles) == 0 } diff --git a/internal/build/main_module.go b/internal/build/main_module.go index 5dbf0ad81f..8aae9cc52e 100644 --- a/internal/build/main_module.go +++ b/internal/build/main_module.go @@ -79,7 +79,7 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g if ctx.buildConf.BuildMode != BuildModeExe { if cfg.rtInit { - defineLibraryRuntimeInit(mainPkg, declareNoArgFunc(mainPkg, rtPkgPath+".init")) + defineLibraryRuntimeInit(mainPkg, rtPkgPath, declareNoArgFunc(mainPkg, rtPkgPath+".init")) } return mainAPkg } @@ -131,15 +131,20 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g } // defineLibraryRuntimeInit arranges for the LLGo runtime to be initialized -// before a C program calls an exported Go function. llvm.global_ctors is -// lowered to the platform's native constructor mechanism for both shared -// libraries and archive members. -func defineLibraryRuntimeInit(pkg llssa.Package, rtInit llssa.Function) { +// before a C program calls an exported Go function. When locality storage is +// present, the constructor also installs the process-lifetime context inherited +// by exported calls on its thread. llvm.global_ctors is lowered to the +// platform's native constructor mechanism for shared libraries and archives. +func defineLibraryRuntimeInit(pkg llssa.Package, rtPkgPath string, rtInit llssa.Function) { const ctorName = "__llgo_runtime_ctor" ctor := pkg.NewFunc(ctorName, llssa.NoArgsNoRet, llssa.InC) ctorValue := pkg.Module().NamedFunction(ctorName) ctorValue.SetLinkage(llvm.InternalLinkage) b := ctor.MakeBody(1) + if pkg.Prog.NeedsLocalContext() { + enterLocalContext := declareNoArgFunc(pkg, rtPkgPath+".enterLibraryLocalContext") + b.Call(enterLocalContext.Expr) + } b.Call(rtInit.Expr) b.Return() @@ -225,6 +230,11 @@ func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa fnVal.SetUnnamedAddr(true) } b := fn.MakeBody(1) + var localCtx, previousLocalCtx llssa.Expr + hasLocalContext := fns.rtInit != nil && prog.NeedsLocalContext() + if hasLocalContext { + localCtx, previousLocalCtx = b.EnterLocalContext() + } b.Store(argcVar.Expr, fn.Param(0)) b.Store(argvVar.Expr, fn.Param(1)) if IsStdioNobuf() { @@ -245,6 +255,9 @@ func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa if fns.pyFinalize != nil { b.Call(fns.pyFinalize.Expr) } + if hasLocalContext { + b.LeaveLocalContext(localCtx, previousLocalCtx) + } b.Return(prog.IntVal(0, prog.Int32())) return fn } diff --git a/internal/build/main_module_test.go b/internal/build/main_module_test.go index f803182482..3a6dfaccbc 100644 --- a/internal/build/main_module_test.go +++ b/internal/build/main_module_test.go @@ -4,6 +4,8 @@ package build import ( + "go/token" + "go/types" "strings" "testing" @@ -113,6 +115,60 @@ func TestGenMainModuleLibraryInitializesRuntime(t *testing.T) { } } +func TestGenMainModuleInstallsLocalContextWhenNeeded(t *testing.T) { + llvm.InitializeAllTargets() + t.Setenv(llgoStdioNobuf, "") + prog := llssa.NewProgram(nil) + runtimePkg := types.NewPackage(llssa.PkgRuntime, "runtime") + contextName := types.NewTypeName(token.NoPos, runtimePkg, "LocalContext", nil) + contextType := types.NewNamed(contextName, types.NewStruct(nil, nil), nil) + runtimePkg.Scope().Insert(contextName) + contextPointer := types.NewPointer(contextType) + enterParams := types.NewTuple(types.NewParam(token.NoPos, runtimePkg, "ctx", contextPointer)) + enterResults := types.NewTuple(types.NewParam(token.NoPos, runtimePkg, "previous", types.Typ[types.Uintptr])) + runtimePkg.Scope().Insert(types.NewFunc(token.NoPos, runtimePkg, "EnterLocalContext", types.NewSignatureType(nil, nil, nil, enterParams, enterResults, false))) + leaveParams := types.NewTuple( + types.NewParam(token.NoPos, runtimePkg, "ctx", contextPointer), + types.NewParam(token.NoPos, runtimePkg, "previous", types.Typ[types.Uintptr]), + ) + runtimePkg.Scope().Insert(types.NewFunc(token.NoPos, runtimePkg, "LeaveLocalContext", types.NewSignatureType(nil, nil, nil, leaveParams, nil, false))) + prog.SetRuntime(runtimePkg) + prog.SetLocalityInfo("example.com/state.Value", llssa.LocalityInfo{Locality: llssa.GoroutineLocal}) + prog.SetLocalStorage("example.com/state.Value", llssa.LocalStoragePackage) + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + pkg := &packages.Package{PkgPath: "example.com/foo", ExportFile: "foo.a"} + withoutRuntime := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{}).LPkg.String() + if strings.Contains(withoutRuntime, "EnterLocalContext") || strings.Contains(withoutRuntime, "LeaveLocalContext") { + t.Fatalf("main module without runtime should not install a local context:\n%s", withoutRuntime) + } + ir := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{rtInit: true}).LPkg.String() + assertInOrder(t, ir, + "EnterLocalContext", + `call void @"example.com/foo.init"()`, + `call void @"example.com/foo.main"()`, + "LeaveLocalContext", + ) + + for _, mode := range []BuildMode{BuildModeCArchive, BuildModeCShared} { + ctx.buildConf.BuildMode = mode + ir := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{rtInit: true}).LPkg.String() + assertInOrder(t, ir, + "enterLibraryLocalContext", + `call void @"github.com/goplus/llgo/runtime/internal/runtime.init"()`, + ) + if strings.Contains(ir, "LeaveLocalContext") { + t.Fatalf("library constructor should retain its local context:\n%s", ir) + } + } +} + func assertInOrder(t *testing.T, s string, wants ...string) { t.Helper() offset := 0 diff --git a/internal/directive/directive.go b/internal/directive/directive.go new file mode 100644 index 0000000000..2678eb69ff --- /dev/null +++ b/internal/directive/directive.go @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package directive parses Go and LLGo source directives without assigning +// feature-specific semantics to them. +package directive + +import ( + "go/ast" + "go/token" + "strings" +) + +// Directive is one normalized Go or LLGo source directive. +type Directive struct { + Name string + Args string + Raw string + Pos token.Pos +} + +// Parse normalizes comment when it uses a supported directive spelling. +func Parse(comment *ast.Comment) (Directive, bool) { + if comment == nil { + return Directive{}, false + } + raw := comment.Text + var namespace, body string + switch { + case strings.HasPrefix(raw, "//go:"): + namespace, body = "go:", raw[len("//go:"):] + case strings.HasPrefix(raw, "//llgo:"): + namespace, body = "llgo:", raw[len("//llgo:"):] + case strings.HasPrefix(raw, "// llgo:"): + namespace, body = "llgo:", raw[len("// llgo:"):] + case strings.HasPrefix(raw, "//export "): + return Directive{Name: "export", Args: strings.TrimSpace(raw[len("//export "):]), Raw: raw, Pos: comment.Pos()}, true + default: + return Directive{}, false + } + body = strings.TrimSpace(body) + if body == "" { + return Directive{}, false + } + name, args := body, "" + if idx := strings.IndexAny(body, " \t"); idx >= 0 { + name, args = body[:idx], strings.TrimSpace(body[idx+1:]) + } + return Directive{Name: namespace + name, Args: args, Raw: raw, Pos: comment.Pos()}, true +} + +// ParseGroup returns all normalized directives in doc in source order. +func ParseGroup(doc *ast.CommentGroup) []Directive { + if doc == nil { + return nil + } + ret := make([]Directive, 0, len(doc.List)) + for _, comment := range doc.List { + if parsed, ok := Parse(comment); ok { + ret = append(ret, parsed) + } + } + return ret +} diff --git a/internal/directive/directive_test.go b/internal/directive/directive_test.go new file mode 100644 index 0000000000..c9bb47fd07 --- /dev/null +++ b/internal/directive/directive_test.go @@ -0,0 +1,47 @@ +package directive + +import ( + "go/ast" + "testing" +) + +func TestParse(t *testing.T) { + tests := []struct { + text string + name string + args string + ok bool + }{ + {text: "// ordinary"}, + {text: "//go:"}, + {text: "//go:noinline", name: "go:noinline", ok: true}, + {text: "//llgo:tls", name: "llgo:tls", ok: true}, + {text: "// llgo:type C", name: "llgo:type", args: "C", ok: true}, + {text: "//llgo:link\tF C.f", name: "llgo:link", args: "F C.f", ok: true}, + {text: "//export F", name: "export", args: "F", ok: true}, + } + if _, ok := Parse(nil); ok { + t.Fatal("nil comment parsed as a directive") + } + if ParseGroup(nil) != nil { + t.Fatal("nil comment group returned directives") + } + for _, test := range tests { + got, ok := Parse(&ast.Comment{Text: test.text}) + if ok != test.ok || got.Name != test.name || got.Args != test.args || got.Raw != map[bool]string{true: test.text}[test.ok] { + t.Fatalf("Parse(%q) = %+v, %v", test.text, got, ok) + } + } +} + +func TestParseGroupPreservesSourceOrder(t *testing.T) { + doc := &ast.CommentGroup{List: []*ast.Comment{ + {Text: "// ordinary"}, + {Text: "//go:noinline"}, + {Text: "//llgo:tls"}, + }} + got := ParseGroup(doc) + if len(got) != 2 || got[0].Name != "go:noinline" || got[1].Name != "llgo:tls" { + t.Fatalf("ParseGroup = %+v", got) + } +} diff --git a/internal/locality/layout/layout.go b/internal/locality/layout/layout.go new file mode 100644 index 0000000000..2c8b8fbdfb --- /dev/null +++ b/internal/locality/layout/layout.go @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package layout plans the internal storage layout for one package's local variables. It +// is intentionally independent of LLGo SSA, LLVM, the runtime, and the build +// cache so every integration layer consumes the same policy result. +package layout + +import ( + "fmt" + "go/types" + "sort" + + "github.com/goplus/llgo/internal/locality" +) + +// Storage identifies the physical addressing strategy selected for a variable. +type Storage uint8 + +const ( + StorageUnknown Storage = iota + StorageNativeTLS + StoragePackage +) + +// Declaration is the source information needed to plan one variable. +type Declaration struct { + Name string + Type types.Type + Info locality.Info +} + +// Variable is one planned local variable. Field is valid for package storage. +type Variable struct { + Declaration + Storage Storage + Field int +} + +// Initializer identifies one replay helper in Go package initialization order. +type Initializer struct { + Name string + Order int +} + +// Package is a deterministic storage plan. Block contains all pointer-bearing +// TLS and GLS variables in their shared physical package block. +type Package struct { + Path string + Variables []Variable + Block []Variable + Thread []Initializer + Goroutine []Initializer + + byName map[string]int +} + +// Plan creates a deterministic package layout. TLS and GLS remain logically +// distinct, while the current one-thread-per-goroutine backend may share their +// pointer-bearing package block. +func Plan(path string, declarations []Declaration) (Package, error) { + ret := Package{Path: path} + decls := append([]Declaration(nil), declarations...) + sort.Slice(decls, func(i, j int) bool { return decls[i].Name < decls[j].Name }) + ret.byName = make(map[string]int, len(decls)) + initializers := map[locality.Kind]map[int]string{ + locality.Thread: {}, + locality.Goroutine: {}, + } + for _, decl := range decls { + if decl.Info.Locality == locality.None { + continue + } + if decl.Name == "" || decl.Type == nil { + return Package{}, fmt.Errorf("locality layout: incomplete declaration %q", decl.Name) + } + if _, exists := ret.byName[decl.Name]; exists { + return Package{}, fmt.Errorf("locality layout: duplicate declaration %s", decl.Name) + } + if decl.Info.Locality != locality.Thread && decl.Info.Locality != locality.Goroutine { + return Package{}, fmt.Errorf("locality layout: invalid locality for %s", decl.Name) + } + prepared := decl.Info.InitFunc != "" && decl.Info.InitOrder != 0 + if decl.Info.HasInitializer != prepared { + return Package{}, fmt.Errorf("locality layout: inconsistent initializer metadata for %s", decl.Name) + } + variable := Variable{Declaration: decl, Field: -1, Storage: StorageForType(decl.Type)} + if variable.Storage == StoragePackage { + variable.Field = len(ret.Block) + ret.Block = append(ret.Block, variable) + } + ret.byName[decl.Name] = len(ret.Variables) + ret.Variables = append(ret.Variables, variable) + if decl.Info.InitFunc != "" { + byOrder := initializers[decl.Info.Locality] + if current, exists := byOrder[decl.Info.InitOrder]; exists && current != decl.Info.InitFunc { + return Package{}, fmt.Errorf("locality layout: initializer order %d names both %s and %s", decl.Info.InitOrder, current, decl.Info.InitFunc) + } + byOrder[decl.Info.InitOrder] = decl.Info.InitFunc + } + } + ret.Thread = orderedInitializers(initializers[locality.Thread]) + ret.Goroutine = orderedInitializers(initializers[locality.Goroutine]) + return ret, nil +} + +// StorageForType returns the physical storage class for a local variable type. +// Pointer-free values use native LLVM TLS; values visible to the GC share the +// package block rooted by LocalContext. +func StorageForType(typ types.Type) Storage { + if hasPointers(typ) { + return StoragePackage + } + return StorageNativeTLS +} + +func hasPointers(typ types.Type) bool { + typ = types.Unalias(typ) + switch typ := typ.(type) { + case *types.Basic: + return typ.Kind() == types.String || typ.Kind() == types.UnsafePointer + case *types.Pointer, *types.Slice, *types.Map, *types.Chan, *types.Signature, *types.Interface: + return true + case *types.Array: + return typ.Len() != 0 && hasPointers(typ.Elem()) + case *types.Struct: + for i := 0; i < typ.NumFields(); i++ { + if hasPointers(typ.Field(i).Type()) { + return true + } + } + return false + case *types.Named: + return hasPointers(typ.Underlying()) + case *types.TypeParam: + return true + default: + return false + } +} + +func orderedInitializers(byOrder map[int]string) []Initializer { + ret := make([]Initializer, 0, len(byOrder)) + for order, name := range byOrder { + ret = append(ret, Initializer{Name: name, Order: order}) + } + sort.Slice(ret, func(i, j int) bool { return ret[i].Order < ret[j].Order }) + return ret +} + +// Lookup returns a variable from the package plan. +func (p Package) Lookup(name string) (Variable, bool) { + index, ok := p.byName[name] + if !ok { + return Variable{}, false + } + return p.Variables[index], true +} + +// Initializers returns the ordered replay helpers for kind. +func (p Package) Initializers(kind locality.Kind) []Initializer { + if kind == locality.Thread { + return p.Thread + } + if kind == locality.Goroutine { + return p.Goroutine + } + return nil +} + +// BlockName returns the shared package-block accessor symbol. +func BlockName(path string) string { return qualify(path, "__llgo_local_block") } + +// BlockCacheName returns the package block's owner-local direct-cache symbol. +func BlockCacheName(path string) string { return qualify(path, "__llgo_local_cache") } + +// InitName returns the package/kind initializer dispatcher symbol. +func InitName(path string, kind locality.Kind) string { + return qualify(path, "__llgo_"+kind.String()+"_init") +} + +// EnsureName returns the package/kind first-use initializer symbol. +func EnsureName(path string, kind locality.Kind) string { return InitName(path, kind) + "$ensure" } + +// GuardName returns the package/kind native TLS state symbol. +func GuardName(path string, kind locality.Kind) string { return InitName(path, kind) + "$guard" } + +// FailureCacheName returns the package/kind initializer failure-cache symbol. +func FailureCacheName(path string, kind locality.Kind) string { + return InitName(path, kind) + "$failure_cache" +} + +func qualify(path, name string) string { + if path == "" { + return name + } + return path + "." + name +} diff --git a/internal/locality/layout/layout_test.go b/internal/locality/layout/layout_test.go new file mode 100644 index 0000000000..af2527e3af --- /dev/null +++ b/internal/locality/layout/layout_test.go @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package layout + +import ( + "go/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/locality" +) + +func TestPlanSharesPointerStorageAndPreservesKinds(t *testing.T) { + plan, err := Plan("example.com/state", []Declaration{ + {Name: "example.com/state.Ignored", Type: types.Typ[types.Int]}, + {Name: "example.com/state.Z", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Goroutine}}, + {Name: "example.com/state.P", Type: types.NewPointer(types.Typ[types.Int]), Info: locality.Info{Locality: locality.Thread, HasInitializer: true, InitFunc: "p.init1", InitOrder: 2}}, + {Name: "example.com/state.A", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Goroutine, HasInitializer: true, InitFunc: "p.init0", InitOrder: 1}}, + {Name: "example.com/state.Q", Type: types.NewSlice(types.Typ[types.Byte]), Info: locality.Info{Locality: locality.Goroutine}}, + }) + if err != nil { + t.Fatal(err) + } + if len(plan.Variables) != 4 || len(plan.Block) != 2 { + t.Fatalf("plan sizes = %d/%d", len(plan.Variables), len(plan.Block)) + } + if got, _ := plan.Lookup("example.com/state.Z"); got.Storage != StorageNativeTLS || got.Info.Locality != locality.Goroutine { + t.Fatalf("scalar GLS plan = %+v", got) + } + if got, _ := plan.Lookup("example.com/state.P"); got.Storage != StoragePackage || got.Field != 0 { + t.Fatalf("pointer TLS plan = %+v", got) + } + if got, _ := plan.Lookup("example.com/state.Q"); got.Storage != StoragePackage || got.Field != 1 { + t.Fatalf("slice GLS plan = %+v", got) + } + if len(plan.Thread) != 1 || plan.Thread[0].Name != "p.init1" { + t.Fatalf("thread initializers = %+v", plan.Thread) + } + if len(plan.Goroutine) != 1 || plan.Goroutine[0].Name != "p.init0" { + t.Fatalf("goroutine initializers = %+v", plan.Goroutine) + } + if got := plan.Initializers(locality.Thread); len(got) != 1 || got[0].Name != "p.init1" { + t.Fatalf("Initializers(thread) = %+v", got) + } + if got := plan.Initializers(locality.Goroutine); len(got) != 1 || got[0].Name != "p.init0" { + t.Fatalf("Initializers(goroutine) = %+v", got) + } + if got := plan.Initializers(locality.None); got != nil { + t.Fatalf("Initializers(none) = %+v", got) + } + if _, ok := plan.Lookup("example.com/state.Missing"); ok { + t.Fatal("Lookup found a missing variable") + } +} + +func TestOrderedInitializers(t *testing.T) { + got := orderedInitializers(map[int]string{3: "p.third", 1: "p.first", 2: "p.second"}) + if len(got) != 3 || got[0].Name != "p.first" || got[1].Name != "p.second" || got[2].Name != "p.third" { + t.Fatalf("ordered initializers = %+v", got) + } +} + +func TestPlanRejectsInvalidDeclarations(t *testing.T) { + tests := []struct { + name string + in []Declaration + want string + }{ + {"missing type", []Declaration{{Name: "p.x", Info: locality.Info{Locality: locality.Thread}}}, "incomplete"}, + {"duplicate", []Declaration{{Name: "p.x", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Thread}}, {Name: "p.x", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Thread}}}, "duplicate"}, + {"invalid kind", []Declaration{{Name: "p.x", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Kind(99)}}}, "invalid locality"}, + {"unprepared", []Declaration{{Name: "p.x", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Thread, HasInitializer: true}}}, "inconsistent initializer metadata"}, + {"unexpected helper", []Declaration{{Name: "p.x", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Thread, InitFunc: "p.a", InitOrder: 1}}}, "inconsistent initializer metadata"}, + {"order conflict", []Declaration{{Name: "p.x", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Thread, HasInitializer: true, InitFunc: "p.a", InitOrder: 1}}, {Name: "p.y", Type: types.Typ[types.Int], Info: locality.Info{Locality: locality.Thread, HasInitializer: true, InitFunc: "p.b", InitOrder: 1}}}, "names both"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if _, err := Plan("p", test.in); err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("Plan error = %v, want %q", err, test.want) + } + }) + } +} + +func TestNames(t *testing.T) { + if got := BlockName("example.com/p"); got != "example.com/p.__llgo_local_block" { + t.Fatal(got) + } + if got := BlockCacheName("example.com/p"); got != "example.com/p.__llgo_local_cache" { + t.Fatal(got) + } + if got := InitName("example.com/p", locality.Thread); got != "example.com/p.__llgo_tls_init" { + t.Fatal(got) + } + if got := EnsureName("example.com/p", locality.Goroutine); got != "example.com/p.__llgo_gls_init$ensure" { + t.Fatal(got) + } + if got := GuardName("", locality.Thread); got != "__llgo_tls_init$guard" { + t.Fatal(got) + } + if got := FailureCacheName("", locality.Goroutine); got != "__llgo_gls_init$failure_cache" { + t.Fatal(got) + } +} + +func TestStorageForType(t *testing.T) { + if got := StorageForType(types.Typ[types.Uintptr]); got != StorageNativeTLS { + t.Fatalf("uintptr storage = %v", got) + } + if got := StorageForType(types.NewArray(types.NewPointer(types.Typ[types.Int]), 0)); got != StorageNativeTLS { + t.Fatalf("zero-length pointer array storage = %v", got) + } + if got := StorageForType(types.NewStruct([]*types.Var{ + types.NewField(0, nil, "value", types.NewPointer(types.Typ[types.Int]), false), + }, nil)); got != StoragePackage { + t.Fatalf("pointer struct storage = %v", got) + } +} + +func TestHasPointers(t *testing.T) { + pkg := types.NewPackage("example.com/types", "types") + namedInt := types.NewNamed(types.NewTypeName(token.NoPos, pkg, "Int", nil), types.Typ[types.Int], nil) + typeParam := types.NewTypeParam(types.NewTypeName(token.NoPos, pkg, "T", nil), types.NewInterfaceType(nil, nil).Complete()) + tests := []struct { + typ types.Type + want bool + }{ + {types.Typ[types.Int], false}, + {types.Typ[types.String], true}, + {types.Typ[types.UnsafePointer], true}, + {types.NewPointer(types.Typ[types.Int]), true}, + {types.NewSlice(types.Typ[types.Int]), true}, + {types.NewMap(types.Typ[types.Int], types.Typ[types.Int]), true}, + {types.NewChan(types.SendRecv, types.Typ[types.Int]), true}, + {types.NewSignatureType(nil, nil, nil, nil, nil, false), true}, + {types.NewInterfaceType(nil, nil).Complete(), true}, + {types.NewArray(types.Typ[types.Int], 1), false}, + {types.NewArray(types.NewPointer(types.Typ[types.Int]), 0), false}, + {types.NewArray(types.NewPointer(types.Typ[types.Int]), 1), true}, + {types.NewStruct([]*types.Var{types.NewVar(token.NoPos, pkg, "n", types.Typ[types.Int])}, nil), false}, + {types.NewStruct([]*types.Var{types.NewVar(token.NoPos, pkg, "p", types.NewPointer(types.Typ[types.Int]))}, nil), true}, + {namedInt, false}, + {typeParam, true}, + {types.NewTuple(), false}, + } + for _, test := range tests { + if got := hasPointers(test.typ); got != test.want { + t.Fatalf("hasPointers(%v) = %v, want %v", test.typ, got, test.want) + } + } +} diff --git a/internal/locality/locality.go b/internal/locality/locality.go new file mode 100644 index 0000000000..1e9a7bfca3 --- /dev/null +++ b/internal/locality/locality.go @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package locality defines the source-level TLS and GLS declaration model. +// It intentionally has no dependency on LLGo's SSA or LLVM lowering layers. +package locality + +import "fmt" + +const ( + ThreadDirective = "//llgo:tls" + GoroutineDirective = "//llgo:gls" + InitPrefix = "__llgo_local_init_" +) + +// Kind identifies the execution context that owns a package variable. +type Kind uint8 + +const ( + None Kind = iota + Thread + Goroutine +) + +// Info is the locality-specific part of a declaration's compiler metadata. +type Info struct { + Locality Kind + HasInitializer bool + InitFunc string + InitOrder int +} + +func (kind Kind) String() string { + switch kind { + case None: + return "" + case Thread: + return "tls" + case Goroutine: + return "gls" + default: + return fmt.Sprintf("invalid:%d", kind) + } +} + +// Parse converts the cache representation of a locality into a Kind. +func Parse(name string) (Kind, bool) { + switch name { + case "": + return None, true + case "tls": + return Thread, true + case "gls": + return Goroutine, true + default: + return None, false + } +} + +// Directive returns the source directive for kind. +func Directive(kind Kind) string { + if kind == Goroutine { + return GoroutineDirective + } + return ThreadDirective +} + +// Merge combines declaration- and spec-level locality directives. +func Merge(a, b Kind) (Kind, bool) { + if a != None && b != None && a != b { + return None, false + } + if b != None { + return b, true + } + return a, true +} diff --git a/internal/locality/locality_test.go b/internal/locality/locality_test.go new file mode 100644 index 0000000000..f46bcccc2d --- /dev/null +++ b/internal/locality/locality_test.go @@ -0,0 +1,489 @@ +package locality + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "strings" + "testing" +) + +func TestKindEncodingAndNames(t *testing.T) { + tests := []struct { + kind Kind + name string + }{ + {None, ""}, + {Thread, "tls"}, + {Goroutine, "gls"}, + } + for _, test := range tests { + if got := test.kind.String(); got != test.name { + t.Fatalf("Kind(%d).String() = %q, want %q", test.kind, got, test.name) + } + if got, ok := Parse(test.name); !ok || got != test.kind { + t.Fatalf("Parse(%q) = %v, %v", test.name, got, ok) + } + } + if got := Kind(99).String(); got != "invalid:99" { + t.Fatalf("invalid locality name = %q", got) + } + if _, ok := Parse("invalid:99"); ok { + t.Fatal("Parse accepted invalid locality") + } + if Directive(Thread) != ThreadDirective || Directive(Goroutine) != GoroutineDirective { + t.Fatal("unexpected locality directive names") + } +} + +func TestMerge(t *testing.T) { + tests := []struct { + a, b Kind + want Kind + ok bool + }{ + {want: None, ok: true}, + {a: Thread, want: Thread, ok: true}, + {b: Goroutine, want: Goroutine, ok: true}, + {a: Thread, b: Thread, want: Thread, ok: true}, + {a: Thread, b: Goroutine, want: None}, + } + for _, test := range tests { + got, ok := Merge(test.a, test.b) + if got != test.want || ok != test.ok { + t.Fatalf("Merge(%v, %v) = %v, %v", test.a, test.b, got, ok) + } + } +} + +func TestScanPackageVar(t *testing.T) { + fset, file := parseFile(t, `package p + +//llgo:tls +var ( + first int + //llgo:gls + second int +) +`) + decl := file.Decls[0].(*ast.GenDecl) + if _, err := ScanPackageVar(fset, decl); err == nil || !strings.Contains(err.Error(), "cannot apply to the same variable declaration") { + t.Fatalf("ScanPackageVar conflict error = %v", err) + } + + fset, file = parseFile(t, `package p + +var ( + //llgo:tls + first, second = 1, 2 +) +`) + vars, err := ScanPackageVar(fset, file.Decls[0].(*ast.GenDecl)) + if err != nil { + t.Fatal(err) + } + if len(vars) != 2 || vars[0].Info.Locality != Thread || !vars[0].Info.HasInitializer || vars[1].Name != "second" { + t.Fatalf("ScanPackageVar = %+v", vars) + } +} + +func TestScanPackageVarBranches(t *testing.T) { + comment := func(text string) *ast.CommentGroup { + doc := &ast.CommentGroup{} + for _, line := range strings.Split(text, "\n") { + doc.List = append(doc.List, &ast.Comment{Text: line}) + } + return doc + } + tests := []struct { + name string + decl *ast.GenDecl + want string + }{ + { + name: "declaration error", + decl: &ast.GenDecl{Tok: token.VAR, Doc: comment("//llgo:tls extra")}, + want: "does not accept arguments", + }, + { + name: "spec error", + decl: &ast.GenDecl{Tok: token.VAR, Specs: []ast.Spec{&ast.ValueSpec{Doc: comment("//llgo:gls extra")}}}, + want: "does not accept arguments", + }, + { + name: "embed conflict", + decl: &ast.GenDecl{Tok: token.VAR, Doc: comment("//llgo:tls\n//go:embed value.txt"), Specs: []ast.Spec{&ast.ValueSpec{Names: []*ast.Ident{ast.NewIdent("Value")}}}}, + want: "//go:embed", + }, + { + name: "blank name", + decl: &ast.GenDecl{Tok: token.VAR, Doc: comment("//llgo:tls"), Specs: []ast.Spec{&ast.ValueSpec{Names: []*ast.Ident{ast.NewIdent("_")}}}}, + want: "blank identifier", + }, + { + name: "exported name", + decl: &ast.GenDecl{Tok: token.VAR, Doc: comment("//llgo:gls"), Specs: []ast.Spec{&ast.ValueSpec{Names: []*ast.Ident{ast.NewIdent("Value")}}}}, + want: "requires an unexported package variable", + }, + { + name: "linkname", + decl: &ast.GenDecl{Tok: token.VAR, Doc: comment("//go:linkname value example.com/p.value\n//llgo:tls"), Specs: []ast.Spec{&ast.ValueSpec{Names: []*ast.Ident{ast.NewIdent("value")}}}}, + want: "cannot apply to a //go:linkname variable", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if _, err := ScanPackageVar(nil, test.decl); err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("ScanPackageVar error = %v, want %q", err, test.want) + } + }) + } + + ordinary := &ast.GenDecl{Tok: token.VAR, Specs: []ast.Spec{ + &ast.ImportSpec{}, + &ast.ValueSpec{Names: []*ast.Ident{ast.NewIdent("Value")}}, + }} + if vars, err := ScanPackageVar(nil, ordinary); err != nil || len(vars) != 0 { + t.Fatalf("ordinary ScanPackageVar = %+v, %v", vars, err) + } +} + +func TestDirectivePlacementDiagnostics(t *testing.T) { + tests := []struct { + name string + src string + want string + }{ + { + name: "grouped import spec", + src: `package p +import ( + //llgo:tls + "unsafe" +) +var _ = unsafe.Sizeof(0) +`, + want: "package-level var", + }, + { + name: "local var", + src: `package p +func f() { + //llgo:gls + var value int + _ = value +} +`, + want: "package-level var", + }, + { + name: "nested function local", + src: `package p +func f() { + _ = func() { + //llgo:tls extra + var value int + _ = value + } +} +`, + want: "does not accept arguments", + }, + { + name: "nested function in local initializer", + src: `package p +func f() { + var nested = func() { + //llgo:gls + var value int + _ = value + } + _ = nested +} +`, + want: "package-level var", + }, + { + name: "function", + src: `package p +//llgo:tls +func f() {} +`, + want: "package-level var", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fset, file := parseFile(t, test.src) + err := validateFile(fset, file) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("validation error = %v, want %q", err, test.want) + } + }) + } +} + +func TestDirectiveDiagnostics(t *testing.T) { + tests := []struct { + comment string + want string + }{ + {"//llgo:threadlocal", "use //llgo:tls"}, + {"//llgo:goroutinelocal", "use //llgo:gls"}, + {"//llgo:tls extra", "does not accept arguments"}, + {"//llgo:tls\n//llgo:gls", "cannot apply to the same variable declaration"}, + } + for _, test := range tests { + doc := &ast.CommentGroup{} + for _, line := range strings.Split(test.comment, "\n") { + doc.List = append(doc.List, &ast.Comment{Text: line}) + } + if _, _, err := FromDoc(nil, doc); err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("FromDoc(%q) error = %v", test.comment, err) + } + } + if err := ValidateDoc(nil, nil); err != nil { + t.Fatal(err) + } + if kind, _, err := FromDoc(nil, &ast.CommentGroup{List: []*ast.Comment{{Text: "//go:noinline"}}}); err != nil || kind != None { + t.Fatalf("ordinary directive = %v, %v", kind, err) + } + if err := ValidateFuncBody(nil, nil); err != nil { + t.Fatal(err) + } + typeDecl := &ast.GenDecl{Tok: token.TYPE, Specs: []ast.Spec{ + &ast.TypeSpec{Name: ast.NewIdent("T"), Doc: &ast.CommentGroup{List: []*ast.Comment{{Text: "//llgo:tls"}}}}, + }} + if err := ValidateNonPackageVar(nil, typeDecl); err == nil || !strings.Contains(err.Error(), "package-level var") { + t.Fatalf("type spec validation error = %v", err) + } + if hasDirective(&ast.CommentGroup{List: []*ast.Comment{{Text: "//go:noinline"}}}, "go:embed") { + t.Fatal("hasDirective matched an unrelated directive") + } +} + +func TestInitializerLocalityDiagnostics(t *testing.T) { + pkg := types.NewPackage("example.com/p", "p") + thread := types.NewVar(token.NoPos, pkg, "thread", types.Typ[types.Int]) + goroutine := types.NewVar(token.NoPos, pkg, "goroutine", types.Typ[types.Int]) + ordinary := types.NewVar(token.NoPos, pkg, "ordinary", types.Typ[types.Int]) + vars := map[string]Info{ + "thread": {Locality: Thread, HasInitializer: true}, + "goroutine": {Locality: Goroutine, HasInitializer: true}, + } + rhs := ast.NewIdent("rhs") + tests := []struct { + lhs []*types.Var + want string + }{ + {[]*types.Var{thread, ordinary}, "mix local and ordinary"}, + {[]*types.Var{ordinary, thread}, "mix local and ordinary"}, + {[]*types.Var{thread, goroutine}, "mix thread-local and goroutine-local"}, + } + for _, test := range tests { + if _, _, err := initializerLocality(nil, &types.Initializer{Lhs: test.lhs, Rhs: rhs}, vars); err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("initializerLocality error = %v, want %q", err, test.want) + } + } + if kind, found, err := initializerLocality(nil, &types.Initializer{Lhs: []*types.Var{ordinary}, Rhs: rhs}, vars); err != nil || found || kind != None { + t.Fatalf("ordinary initializer = %v, %v, %v", kind, found, err) + } +} + +func TestPrepareIsIdempotentAcrossPrograms(t *testing.T) { + fset, file := parseFile(t, `package p +func makeValue() *int { value := 42; return &value } +//llgo:tls +var value = makeValue() +`) + files := []*ast.File{file} + info := newTypeInfo() + pkg, err := (&types.Config{}).Check("example.com/p", fset, files, info) + if err != nil { + t.Fatal(err) + } + vars, err := ScanPackageVar(fset, file.Decls[1].(*ast.GenDecl)) + if err != nil { + t.Fatal(err) + } + raw := make(map[string]Info) + for _, variable := range vars { + raw[variable.Name] = variable.Info + } + + prepared, err := Prepare(fset, pkg.Path(), pkg, info, files, raw) + if err != nil { + t.Fatal(err) + } + declCount := len(file.Decls) + scopeCount := len(pkg.Scope().Names()) + initName := prepared["value"].InitFunc + if initName != "example.com/p.__llgo_local_init_0" || prepared["value"].InitOrder != 1 { + t.Fatalf("prepared metadata = %+v", prepared["value"]) + } + + again, err := Prepare(fset, pkg.Path(), pkg, info, files, prepared) + if err != nil { + t.Fatal(err) + } + reused, err := Prepare(fset, pkg.Path(), pkg, info, files, raw) + if err != nil { + t.Fatal(err) + } + if len(file.Decls) != declCount || len(pkg.Scope().Names()) != scopeCount { + t.Fatalf("repeated Prepare changed syntax/scope: decls=%d/%d scope=%d/%d", len(file.Decls), declCount, len(pkg.Scope().Names()), scopeCount) + } + if again["value"] != prepared["value"] || reused["value"] != prepared["value"] { + t.Fatalf("repeated metadata = %+v, reused = %+v, want %+v", again["value"], reused["value"], prepared["value"]) + } +} + +func TestPrepareZeroValuePointerAndValidation(t *testing.T) { + pkg := types.NewPackage("example.com/p", "p") + value := types.NewVar(token.NoPos, pkg, "Value", types.NewPointer(types.Typ[types.Int])) + pkg.Scope().Insert(value) + vars := map[string]Info{"Value": {Locality: Goroutine}} + prepared, err := Prepare(nil, pkg.Path(), pkg, &types.Info{}, nil, vars) + if err != nil { + t.Fatal(err) + } + if got := prepared["Value"]; got.InitFunc != "" || got.InitOrder != 0 { + t.Fatalf("zero-value initializer = %+v", got) + } + if err := ValidatePrepared(pkg.Path(), map[string]Info{"Value": {Locality: Thread, HasInitializer: true}}); err == nil { + t.Fatal("ValidatePrepared accepted missing initializer metadata") + } + if err := ValidatePrepared(pkg.Path(), map[string]Info{"Value": {Locality: Thread, InitFunc: "p.init", InitOrder: 1}}); err == nil { + t.Fatal("ValidatePrepared accepted initializer metadata on a zero-value declaration") + } + if err := ValidatePrepared(pkg.Path(), map[string]Info{ + "Ordinary": {}, + "Value": {Locality: Thread, HasInitializer: true, InitFunc: "p.init", InitOrder: 1}, + }); err != nil { + t.Fatal(err) + } +} + +func TestPrepareEarlyReturnsAndMissingFiles(t *testing.T) { + if got, err := Prepare(nil, "", nil, nil, nil, nil); err != nil || len(got) != 0 { + t.Fatalf("nil Prepare = %+v, %v", got, err) + } + pkg := types.NewPackage("example.com/p", "p") + value := types.NewVar(token.NoPos, pkg, "Value", types.Typ[types.Int]) + pkg.Scope().Insert(value) + info := &types.Info{InitOrder: []*types.Initializer{{Lhs: []*types.Var{value}, Rhs: ast.NewIdent("rhs")}}} + vars := map[string]Info{"Value": {Locality: Thread, HasInitializer: true}} + if _, err := Prepare(nil, pkg.Path(), pkg, info, nil, vars); err == nil || !strings.Contains(err.Error(), "without syntax files") { + t.Fatalf("Prepare without files error = %v", err) + } + if got, err := Prepare(nil, pkg.Path(), pkg, &types.Info{}, nil, nil); err != nil || len(got) != 0 { + t.Fatalf("Prepare without localities = %+v, %v", got, err) + } +} + +func TestPrepareInitializerBranches(t *testing.T) { + pkg := types.NewPackage("example.com/p", "p") + local := types.NewVar(token.NoPos, pkg, "Local", types.Typ[types.Int]) + ordinary := types.NewVar(token.NoPos, pkg, "Ordinary", types.Typ[types.Int]) + pkg.Scope().Insert(local) + pkg.Scope().Insert(ordinary) + rhs := ast.NewIdent("rhs") + info := &types.Info{InitOrder: []*types.Initializer{ + {Lhs: []*types.Var{ordinary}, Rhs: ast.NewIdent("ordinary")}, + {Lhs: []*types.Var{local, ordinary}, Rhs: rhs}, + }} + vars := map[string]Info{"Local": {Locality: Thread, HasInitializer: true}} + if _, err := Prepare(nil, pkg.Path(), pkg, info, []*ast.File{{}}, vars); err == nil || !strings.Contains(err.Error(), "mix local and ordinary") { + t.Fatalf("mixed Prepare error = %v", err) + } + + info.InitOrder = []*types.Initializer{{Lhs: []*types.Var{local}, Rhs: rhs}} + prepared, err := Prepare(nil, pkg.Path(), pkg, info, []*ast.File{{}}, vars) + if err != nil { + t.Fatal(err) + } + if prepared["Local"].InitFunc == "" || info.Uses == nil || info.Defs == nil { + t.Fatalf("Prepare did not initialize type maps: %+v", prepared["Local"]) + } + + conflicting := &types.Initializer{Lhs: []*types.Var{local, ordinary}, Rhs: rhs} + if got := preparedInitName(conflicting, map[string]Info{ + "Local": {InitFunc: "p.first", InitOrder: 1}, + "Ordinary": {InitFunc: "p.second", InitOrder: 1}, + }, 1); got != "" { + t.Fatalf("preparedInitName accepted conflicting helpers: %q", got) + } + if got := qualify("", "Value"); got != "Value" { + t.Fatalf("qualify empty package = %q", got) + } +} + +func TestFindLocalInitializerRejectsLookalikes(t *testing.T) { + pkg := types.NewPackage("example.com/p", "p") + value := types.NewVar(token.NoPos, pkg, "Value", types.Typ[types.Int]) + rhs := ast.NewIdent("rhs") + initializer := &types.Initializer{Lhs: []*types.Var{value}, Rhs: rhs} + files := []*ast.File{{Decls: []ast.Decl{ + &ast.FuncDecl{ + Name: ast.NewIdent(InitPrefix + "0"), + Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: rhs}}}, + }, + &ast.FuncDecl{ + Name: ast.NewIdent(InitPrefix + "1"), + Body: &ast.BlockStmt{List: []ast.Stmt{&ast.AssignStmt{ + Lhs: []ast.Expr{&ast.BasicLit{Kind: token.INT, Value: "1"}}, + Tok: token.ASSIGN, + Rhs: []ast.Expr{rhs}, + }}}, + }, + }}} + if got := findLocalInitializer(pkg, &types.Info{}, files, initializer); got != "" { + t.Fatalf("findLocalInitializer accepted lookalike %q", got) + } +} + +func parseFile(t *testing.T, src string) (*token.FileSet, *ast.File) { + t.Helper() + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "source.go", src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + return fset, file +} + +func validateFile(fset *token.FileSet, file *ast.File) error { + for _, node := range file.Decls { + switch decl := node.(type) { + case *ast.FuncDecl: + if err := ValidateDoc(fset, decl.Doc); err != nil { + return err + } + if err := ValidateFuncBody(fset, decl.Body); err != nil { + return err + } + case *ast.GenDecl: + if decl.Tok == token.VAR { + if _, err := ScanPackageVar(fset, decl); err != nil { + return err + } + } else if err := ValidateNonPackageVar(fset, decl); err != nil { + return err + } + } + } + return nil +} + +func newTypeInfo() *types.Info { + return &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + Scopes: make(map[ast.Node]*types.Scope), + Instances: make(map[*ast.Ident]types.Instance), + } +} diff --git a/internal/locality/prepare.go b/internal/locality/prepare.go new file mode 100644 index 0000000000..429dd78834 --- /dev/null +++ b/internal/locality/prepare.go @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package locality + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "sort" +) + +// Prepare rewrites local package initializers into replayable synthetic +// functions and returns updated metadata. It is idempotent for repeated calls, +// including calls made by another compiler Program reusing the same syntax and +// types.Package objects. +func Prepare(fset *token.FileSet, pkgPath string, pkg *types.Package, typeInfo *types.Info, files []*ast.File, vars map[string]Info) (map[string]Info, error) { + ret := cloneInfo(vars) + if pkg == nil || typeInfo == nil || !hasLocality(ret) { + return ret, nil + } + + nextName := 0 + for order, initializer := range typeInfo.InitOrder { + _, found, err := initializerLocality(fset, initializer, ret) + if err != nil { + return nil, err + } + if !found { + continue + } + initOrder := order + 1 + if initName := preparedInitName(initializer, ret, initOrder); initName != "" { + setInitializerNames(initializer, ret, initName, initOrder) + continue + } + if len(files) == 0 { + return nil, fmt.Errorf("cannot prepare local initializer for package %q without syntax files", pkgPath) + } + name := findLocalInitializer(pkg, typeInfo, files, initializer) + if name == "" { + for { + name = fmt.Sprintf("%s%d", InitPrefix, nextName) + nextName++ + if pkg.Scope().Lookup(name) == nil { + break + } + } + fnObj, decl := makeLocalInitializer(pkg, typeInfo, name, initializer) + pkg.Scope().Insert(fnObj) + files[len(files)-1].Decls = append(files[len(files)-1].Decls, decl) + } + setInitializerNames(initializer, ret, qualify(pkgPath, name), initOrder) + } + + return ret, nil +} + +// ValidatePrepared verifies that every explicit local initializer was prepared +// before Go SSA construction. +func ValidatePrepared(pkgPath string, vars map[string]Info) error { + names := make([]string, 0, len(vars)) + for name := range vars { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + info := vars[name] + if info.Locality == None { + continue + } + prepared := info.InitFunc != "" && info.InitOrder != 0 + if info.HasInitializer != prepared { + return fmt.Errorf("local variable %s has inconsistent initializer metadata before SSA compilation", qualify(pkgPath, name)) + } + } + return nil +} + +func initializerLocality(fset *token.FileSet, initializer *types.Initializer, vars map[string]Info) (Kind, bool, error) { + var kind Kind + localCount := 0 + for _, variable := range initializer.Lhs { + info, ok := vars[variable.Name()] + if !ok || info.Locality == None { + if localCount != 0 { + return None, false, errorAt(fset, initializer.Rhs.Pos(), "one initializer cannot mix local and ordinary package variables") + } + continue + } + if localCount == 0 { + kind = info.Locality + } else if kind != info.Locality { + return None, false, errorAt(fset, initializer.Rhs.Pos(), "one initializer cannot mix thread-local and goroutine-local variables") + } + localCount++ + } + if localCount != 0 && len(initializer.Lhs) != localCount { + return None, false, errorAt(fset, initializer.Rhs.Pos(), "one initializer cannot mix local and ordinary package variables") + } + return kind, localCount != 0, nil +} + +func preparedInitName(initializer *types.Initializer, vars map[string]Info, order int) string { + var name string + for _, variable := range initializer.Lhs { + info := vars[variable.Name()] + if info.InitFunc == "" || info.InitOrder != order { + return "" + } + if name == "" { + name = info.InitFunc + } else if name != info.InitFunc { + return "" + } + } + return name +} + +func findLocalInitializer(pkg *types.Package, info *types.Info, files []*ast.File, initializer *types.Initializer) string { + for _, file := range files { + for _, node := range file.Decls { + decl, ok := node.(*ast.FuncDecl) + if !ok || len(decl.Name.Name) < len(InitPrefix) || decl.Name.Name[:len(InitPrefix)] != InitPrefix || decl.Body == nil || len(decl.Body.List) != 1 { + continue + } + assign, ok := decl.Body.List[0].(*ast.AssignStmt) + if !ok || assign.Tok != token.ASSIGN || len(assign.Rhs) != 1 || assign.Rhs[0] != initializer.Rhs || len(assign.Lhs) != len(initializer.Lhs) { + continue + } + matches := true + for i, lhs := range assign.Lhs { + ident, ok := lhs.(*ast.Ident) + if !ok || info.Uses[ident] != initializer.Lhs[i] { + matches = false + break + } + } + if matches { + if object := pkg.Scope().Lookup(decl.Name.Name); object == info.Defs[decl.Name] { + return decl.Name.Name + } + } + } + } + return "" +} + +func makeLocalInitializer(pkg *types.Package, info *types.Info, name string, initializer *types.Initializer) (*types.Func, *ast.FuncDecl) { + if info.Uses == nil { + info.Uses = make(map[*ast.Ident]types.Object) + } + if info.Defs == nil { + info.Defs = make(map[*ast.Ident]types.Object) + } + lhs := make([]ast.Expr, len(initializer.Lhs)) + for i, variable := range initializer.Lhs { + ident := ast.NewIdent(variable.Name()) + info.Uses[ident] = variable + lhs[i] = ident + } + nameIdent := ast.NewIdent(name) + sig := types.NewSignatureType(nil, nil, nil, nil, nil, false) + fnObj := types.NewFunc(token.NoPos, pkg, name, sig) + info.Defs[nameIdent] = fnObj + decl := &ast.FuncDecl{ + Name: nameIdent, + Type: &ast.FuncType{Params: &ast.FieldList{}}, + Body: &ast.BlockStmt{List: []ast.Stmt{ + &ast.AssignStmt{Lhs: lhs, Tok: token.ASSIGN, Rhs: []ast.Expr{initializer.Rhs}}, + }}, + } + return fnObj, decl +} + +func setInitializerNames(initializer *types.Initializer, vars map[string]Info, initName string, order int) { + for _, variable := range initializer.Lhs { + info := vars[variable.Name()] + info.InitFunc = initName + info.InitOrder = order + vars[variable.Name()] = info + } +} + +func cloneInfo(vars map[string]Info) map[string]Info { + ret := make(map[string]Info, len(vars)) + for name, info := range vars { + ret[name] = info + } + return ret +} + +func hasLocality(vars map[string]Info) bool { + for _, info := range vars { + if info.Locality != None { + return true + } + } + return false +} + +func qualify(pkgPath, name string) string { + if pkgPath == "" { + return name + } + return pkgPath + "." + name +} diff --git a/internal/locality/scan.go b/internal/locality/scan.go new file mode 100644 index 0000000000..5032133baa --- /dev/null +++ b/internal/locality/scan.go @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package locality + +import ( + "fmt" + "go/ast" + "go/token" + + "github.com/goplus/llgo/internal/directive" +) + +const ( + legacyThread = "llgo:threadlocal" + legacyGoroutine = "llgo:goroutinelocal" +) + +// Variable records locality metadata collected for one package variable. +type Variable struct { + Name string + Info Info +} + +// ScanPackageVar validates and collects locality directives on a package-level +// var declaration. +func ScanPackageVar(fset *token.FileSet, decl *ast.GenDecl) ([]Variable, error) { + declKind, declPos, err := FromDoc(fset, decl.Doc) + if err != nil { + return nil, err + } + var ret []Variable + for _, node := range decl.Specs { + spec, ok := node.(*ast.ValueSpec) + if !ok { + continue + } + specKind, specPos, err := FromDoc(fset, spec.Doc) + if err != nil { + return nil, err + } + kind, _, err := mergeAt(fset, declKind, declPos, specKind, specPos) + if err != nil { + return nil, err + } + if kind == None { + continue + } + if hasDirective(decl.Doc, "go:embed") || hasDirective(spec.Doc, "go:embed") { + return nil, errorAt(fset, spec.Pos(), "%s and //go:embed cannot apply to the same variable declaration", Directive(kind)) + } + if hasDirective(decl.Doc, "go:linkname") || hasDirective(spec.Doc, "go:linkname") { + return nil, errorAt(fset, spec.Pos(), "%s cannot apply to a //go:linkname variable", Directive(kind)) + } + for _, ident := range spec.Names { + if ident.Name == "_" { + return nil, errorAt(fset, ident.Pos(), "locality directive cannot apply to the blank identifier") + } + if ast.IsExported(ident.Name) { + return nil, errorAt(fset, ident.Pos(), "%s requires an unexported package variable", Directive(kind)) + } + ret = append(ret, Variable{ + Name: ident.Name, + Info: Info{Locality: kind, HasInitializer: len(spec.Values) != 0}, + }) + } + } + return ret, nil +} + +// ValidateNonPackageVar rejects locality directives on declarations other +// than package-level vars, including grouped import/type/const specs. +func ValidateNonPackageVar(fset *token.FileSet, decl *ast.GenDecl) error { + if err := ValidateDoc(fset, decl.Doc); err != nil { + return err + } + for _, node := range decl.Specs { + var doc *ast.CommentGroup + switch spec := node.(type) { + case *ast.ImportSpec: + doc = spec.Doc + case *ast.TypeSpec: + doc = spec.Doc + case *ast.ValueSpec: + doc = spec.Doc + } + if err := ValidateDoc(fset, doc); err != nil { + return err + } + } + return nil +} + +// ValidateFuncBody rejects locality directives on declarations nested inside +// a function or function literal. +func ValidateFuncBody(fset *token.FileSet, body *ast.BlockStmt) error { + if body == nil { + return nil + } + var firstErr error + ast.Inspect(body, func(node ast.Node) bool { + if firstErr != nil { + return false + } + stmt, ok := node.(*ast.DeclStmt) + if !ok { + return true + } + decl, ok := stmt.Decl.(*ast.GenDecl) + if ok { + firstErr = ValidateNonPackageVar(fset, decl) + } + return firstErr == nil + }) + return firstErr +} + +// FromDoc returns the locality directive attached to doc. +func FromDoc(fset *token.FileSet, doc *ast.CommentGroup) (Kind, token.Pos, error) { + var kind Kind + var pos token.Pos + for _, directive := range directive.ParseGroup(doc) { + var next Kind + switch directive.Name { + case "llgo:tls": + next = Thread + case "llgo:gls": + next = Goroutine + case legacyThread: + return None, token.NoPos, errorAt(fset, directive.Pos, "//%s is not supported; use %s", legacyThread, ThreadDirective) + case legacyGoroutine: + return None, token.NoPos, errorAt(fset, directive.Pos, "//%s is not supported; use %s", legacyGoroutine, GoroutineDirective) + default: + continue + } + if directive.Args != "" { + return None, token.NoPos, errorAt(fset, directive.Pos, "//%s does not accept arguments", directive.Name) + } + var err error + kind, pos, err = mergeAt(fset, kind, pos, next, directive.Pos) + if err != nil { + return None, token.NoPos, err + } + } + return kind, pos, nil +} + +// ValidateDoc rejects a locality directive outside a package-level var. +func ValidateDoc(fset *token.FileSet, doc *ast.CommentGroup) error { + kind, pos, err := FromDoc(fset, doc) + if err != nil { + return err + } + if kind != None { + return errorAt(fset, pos, "%s applies only to package-level var declarations", Directive(kind)) + } + return nil +} + +func mergeAt(fset *token.FileSet, a Kind, apos token.Pos, b Kind, bpos token.Pos) (Kind, token.Pos, error) { + merged, ok := Merge(a, b) + if !ok { + return None, token.NoPos, errorAt(fset, bpos, "%s and %s cannot apply to the same variable declaration", ThreadDirective, GoroutineDirective) + } + if b != None { + return merged, bpos, nil + } + return merged, apos, nil +} + +func hasDirective(doc *ast.CommentGroup, name string) bool { + for _, directive := range directive.ParseGroup(doc) { + if directive.Name == name { + return true + } + } + return false +} + +func errorAt(fset *token.FileSet, pos token.Pos, format string, args ...any) error { + msg := fmt.Sprintf(format, args...) + if fset == nil || pos == token.NoPos { + return fmt.Errorf("%s", msg) + } + return fmt.Errorf("%s: %s", fset.Position(pos), msg) +} diff --git a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go index c74b6d1421..8d4987fea2 100644 --- a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go +++ b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go @@ -5,6 +5,7 @@ package runtime import ( "unsafe" + latomic "github.com/goplus/llgo/runtime/internal/lib/sync/atomic" llrt "github.com/goplus/llgo/runtime/internal/runtime" ) @@ -91,26 +92,36 @@ func SetCPUProfileRate(hz int) {} const funcForPCCacheSets = 1024 const funcForPCCacheWays = 4 -type funcForPCCacheEntry struct { - pc uintptr - fn *Func -} +// Cache immutable Func pointers as single atomic words. The queried pc lives +// in Func itself, so concurrent replacement cannot combine fields from two +// different cache entries. +var funcForPCCache [funcForPCCacheSets][funcForPCCacheWays]unsafe.Pointer +var funcForPCCacheNext [funcForPCCacheSets]uint32 +var funcForPCLast unsafe.Pointer -var funcForPCCache [funcForPCCacheSets][funcForPCCacheWays]funcForPCCacheEntry -var funcForPCCacheNext [funcForPCCacheSets]uint8 -var funcForPCLast funcForPCCacheEntry +func cachedFuncForPC(entry *unsafe.Pointer, pc uintptr) *Func { + p := latomic.LoadPointer(entry) + if p == nil { + return nil + } + fn := (*Func)(p) + if fn.pc != pc { + return nil + } + return fn +} func FuncForPC(pc uintptr) *Func { // External metadata must be installed before consulting either cache: // caching a pre-load miss would otherwise survive a successful load. ensureRuntimePCLN() - if fn := funcForPCLast.fn; fn != nil && funcForPCLast.pc == pc { + if fn := cachedFuncForPC(&funcForPCLast, pc); fn != nil { return fn } set := &funcForPCCache[funcForPCCacheIndex(pc)] for i := 0; i < funcForPCCacheWays; i++ { - if fn := set[i].fn; fn != nil && set[i].pc == pc { - funcForPCLast = funcForPCCacheEntry{pc: pc, fn: fn} + if fn := cachedFuncForPC(&set[i], pc); fn != nil { + latomic.StorePointer(&funcForPCLast, unsafe.Pointer(fn)) return fn } } @@ -226,12 +237,12 @@ func newFuncForPC(pc uintptr, sym pcSymbol) *Func { // symbolized, going through the FuncForPC cache so repeated CallersFrames // walks over the same PCs stop allocating a Func per frame. func frameFuncForPC(pc uintptr, sym pcSymbol, name string) *Func { - if fn := funcForPCLast.fn; fn != nil && funcForPCLast.pc == pc { + if fn := cachedFuncForPC(&funcForPCLast, pc); fn != nil { return fn } set := &funcForPCCache[funcForPCCacheIndex(pc)] for i := 0; i < funcForPCCacheWays; i++ { - if fn := set[i].fn; fn != nil && set[i].pc == pc { + if fn := cachedFuncForPC(&set[i], pc); fn != nil { return fn } } @@ -255,16 +266,16 @@ func cacheFuncForPC(pc uintptr, fn *Func) { setIndex := funcForPCCacheIndex(pc) set := &funcForPCCache[setIndex] for i := 0; i < funcForPCCacheWays; i++ { - if set[i].fn == nil || set[i].pc == pc { - set[i] = funcForPCCacheEntry{pc: pc, fn: fn} - funcForPCLast = set[i] + p := latomic.LoadPointer(&set[i]) + if p == nil || (*Func)(p).pc == pc { + latomic.StorePointer(&set[i], unsafe.Pointer(fn)) + latomic.StorePointer(&funcForPCLast, unsafe.Pointer(fn)) return } } - way := funcForPCCacheNext[setIndex] & (funcForPCCacheWays - 1) - funcForPCCacheNext[setIndex] = way + 1 - set[way] = funcForPCCacheEntry{pc: pc, fn: fn} - funcForPCLast = set[way] + way := (latomic.AddUint32(&funcForPCCacheNext[setIndex], 1) - 1) & (funcForPCCacheWays - 1) + latomic.StorePointer(&set[way], unsafe.Pointer(fn)) + latomic.StorePointer(&funcForPCLast, unsafe.Pointer(fn)) } func funcForPCCacheIndex(pc uintptr) uintptr { diff --git a/runtime/internal/lib/runtime/symtab.go b/runtime/internal/lib/runtime/symtab.go index b4db828e5c..883b8df45e 100644 --- a/runtime/internal/lib/runtime/symtab.go +++ b/runtime/internal/lib/runtime/symtab.go @@ -808,22 +808,22 @@ var runtimePrebuiltEntriesOnce uint32 // lookups. The set-associative pc cache in FuncForPC thrashes once the live // pc population outgrows it (thousands of distinct functions queried in a // loop); this cache is keyed by table row, so batch workloads stay O(search) -// after the first pass regardless of scale. Same benign-race model as the -// pc cache: word-sized pointer stores of identical values. +// after the first pass regardless of scale. Atomic pointer publication keeps +// concurrently created Func values fully initialized before readers use them. var runtimePrebuiltFuncs []unsafe.Pointer func prebuiltFuncCacheLoad(idx int) unsafe.Pointer { if idx < 0 || idx >= len(runtimePrebuiltFuncs) { return nil } - return runtimePrebuiltFuncs[idx] + return latomic.LoadPointer(&runtimePrebuiltFuncs[idx]) } func prebuiltFuncCacheStore(idx int, fn unsafe.Pointer) { if idx < 0 || idx >= len(runtimePrebuiltFuncs) { return } - runtimePrebuiltFuncs[idx] = fn + latomic.StorePointer(&runtimePrebuiltFuncs[idx], fn) } // prebuiltFrameIndexForEntry returns the ftab row whose entry is exactly pc, @@ -1576,9 +1576,9 @@ func init() { // Write-warm the FuncForPC cache: its first stores otherwise take // zero-fill write faults, one per page, on the first few lookups. for i := 0; i < funcForPCCacheSets; i += 4096 / int(unsafe.Sizeof(funcForPCCache[0])) { - funcForPCCache[i][0].pc = 0 + latomic.StorePointer(&funcForPCCache[i][0], nil) } - funcForPCCache[funcForPCCacheSets-1][0].pc = 0 + latomic.StorePointer(&funcForPCCache[funcForPCCacheSets-1][0], nil) } func coldFuncInfoEntryLookup(pc uintptr) (pcSymbol, bool) { diff --git a/runtime/internal/runtime/defer_tls_baremetal.go b/runtime/internal/runtime/defer_tls_baremetal.go deleted file mode 100644 index d8c4938cca..0000000000 --- a/runtime/internal/runtime/defer_tls_baremetal.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:build baremetal - -/* - * Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package runtime - -// globalDeferHead stores the current defer chain head. -// In baremetal single-threaded environment, a global variable -// replaces pthread TLS. -var globalDeferHead *Defer - -// SetThreadDefer associates the current thread with the given defer chain. -func SetThreadDefer(head *Defer) { - globalDeferHead = head -} - -// GetThreadDefer returns the current thread's defer chain head. -func GetThreadDefer() *Defer { - return globalDeferHead -} - -// ClearThreadDefer resets the current thread's defer chain to nil. -func ClearThreadDefer() { - globalDeferHead = nil -} diff --git a/runtime/internal/runtime/errors.go b/runtime/internal/runtime/errors.go index 7b6776d99a..6fe6436219 100644 --- a/runtime/internal/runtime/errors.go +++ b/runtime/internal/runtime/errors.go @@ -222,6 +222,52 @@ func (e *TypeAssertionError) Error() string { ": missing method " + e.missingMethod } +func TypeAssertError(have, want, iface *Type) any { + if have == nil { + return &TypeAssertionError{iface, nil, want, ""} + } + missingMethod := "" + if want.Kind() == abi.Interface { + missingMethod = typeAssertMissingMethod((*interfacetype)(unsafe.Pointer(want)), have) + } + return &TypeAssertionError{iface, have, want, missingMethod} +} + +func typeAssertMissingMethod(inter *interfacetype, typ *_type) string { + if len(inter.Methods) == 0 { + return "" + } + if typ.Kind() == abi.Interface { + v := (*interfacetype)(unsafe.Pointer(typ)) + for _, tm := range inter.Methods { + if !ifaceHasMethod(v.Methods, tm) { + return tm.Name() + } + } + return "" + } + u := typ.Uncommon() + if u == nil { + return inter.Methods[0].Name() + } + methods := u.Methods() + for _, tm := range inter.Methods { + if _, ok := findMethod(methods, tm); !ok { + return tm.Name() + } + } + return "" +} + +func ifaceHasMethod(methods []abi.Imethod, target abi.Imethod) bool { + for _, method := range methods { + if method.Name_ == target.Name_ && method.Typ_ == target.Typ_ { + return true + } + } + return false +} + func pkgpath(t *_type) string { if u := t.Uncommon(); u != nil { return u.PkgPath_ diff --git a/runtime/internal/runtime/defer_tls.go b/runtime/internal/runtime/g.go similarity index 53% rename from runtime/internal/runtime/defer_tls.go rename to runtime/internal/runtime/g.go index 6a430e9924..0584c55b87 100644 --- a/runtime/internal/runtime/defer_tls.go +++ b/runtime/internal/runtime/g.go @@ -1,7 +1,5 @@ -//go:build !baremetal - /* - * Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved. + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,25 +16,32 @@ package runtime -import "github.com/goplus/llgo/runtime/internal/clite/tls" +import "unsafe" + +// g holds the runtime state owned by one LLGo goroutine. +type g struct { + defer_ *Defer + panic_ unsafe.Pointer + recoverFrame unsafe.Pointer + goexit bool + isMain bool +} -var deferTLS = tls.Alloc[*Defer](func(head **Defer) { - if head != nil { - *head = nil - } -}) +func getg() *g { + return ¤tG +} -// SetThreadDefer associates the current thread with the given defer chain. +// SetThreadDefer associates the current goroutine with the given defer chain. func SetThreadDefer(head *Defer) { - deferTLS.Set(head) + getg().defer_ = head } -// GetThreadDefer returns the current thread's defer chain head. +// GetThreadDefer returns the current goroutine's defer chain head. func GetThreadDefer() *Defer { - return deferTLS.Get() + return getg().defer_ } -// ClearThreadDefer resets the current thread's defer chain to nil. +// ClearThreadDefer resets the current goroutine's defer chain to nil. func ClearThreadDefer() { - deferTLS.Clear() + getg().defer_ = nil } diff --git a/runtime/internal/runtime/g_global.go b/runtime/internal/runtime/g_global.go new file mode 100644 index 0000000000..efe70d3182 --- /dev/null +++ b/runtime/internal/runtime/g_global.go @@ -0,0 +1,22 @@ +//go:build !llgo || baremetal || nintendoswitch + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +// Host builds and single-context targets share one process-wide runtime state. +var currentG g diff --git a/runtime/internal/runtime/g_gls.go b/runtime/internal/runtime/g_gls.go new file mode 100644 index 0000000000..db7bb5da39 --- /dev/null +++ b/runtime/internal/runtime/g_gls.go @@ -0,0 +1,22 @@ +//go:build llgo && !baremetal && !nintendoswitch + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +//llgo:gls +var currentG g diff --git a/runtime/internal/runtime/local_context.go b/runtime/internal/runtime/local_context.go new file mode 100644 index 0000000000..1dbf48190b --- /dev/null +++ b/runtime/internal/runtime/local_context.go @@ -0,0 +1,149 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import "unsafe" + +// LocalContext is rooted by the outer Go entry stack frame. The current +// one-thread-per-goroutine backend maps both logical locality kinds to this one +// physical package store. +type LocalContext struct { + // blocks points at the payload of the most recently allocated local block. + // The list keeps every block reachable from the outer Go entry stack frame. + blocks unsafe.Pointer +} + +type localBlock struct { + // next points at the next block's payload, not its header. + next unsafe.Pointer + cacheSlot *uintptr +} + +var libraryLocalContext LocalContext + +// enterLibraryLocalContext installs a process-lifetime context for the thread +// that runs a C library constructor. Exported Go calls on that thread inherit +// it; calls from other C threads install their own entry-scoped contexts. +func enterLibraryLocalContext() { + EnterLocalContext(&libraryLocalContext) +} + +// EnterLocalContext installs ctx when the current thread has no local owner. +// A nonzero result means this is a nested Go entry that inherited the returned +// context; in that case ctx is not installed. +func EnterLocalContext(ctx *LocalContext) uintptr { + previous := currentLocalContext + if previous == 0 { + if ctx == nil { + panic("runtime: nil local context") + } + currentLocalContext = uintptr(unsafe.Pointer(ctx)) + } + return previous +} + +// LeaveLocalContext finishes an entry paired with EnterLocalContext. A nested +// entry verifies and retains its inherited context. An outer entry clears ctx +// and releases its package-block roots. +func LeaveLocalContext(ctx *LocalContext, previous uintptr) { + if previous != 0 { + if currentLocalContext != previous { + panic("runtime: local context changed by nested entry") + } + return + } + if currentLocalContext != uintptr(unsafe.Pointer(ctx)) { + panic("runtime: leaving inactive local context") + } + currentLocalContext = 0 + releaseLocalBlocks(ctx) +} + +func leaveCurrentLocalContext() { + ctx := (*LocalContext)(unsafe.Pointer(currentLocalContext)) + if ctx == nil { + return + } + currentLocalContext = 0 + releaseLocalBlocks(ctx) +} + +func releaseLocalBlocks(ctx *LocalContext) { + data := ctx.blocks + ctx.blocks = nil + for data != nil { + block := localBlockHeader(data) + next := block.next + // Do not free block here: an address of a local variable may outlive its + // owner. Breaking the links lets the GC retain only escaped blocks. + *block.cacheSlot = 0 + block.next = nil + block.cacheSlot = nil + data = next + } +} + +// LocalPackage creates stable, zeroed storage for one generated cache slot in +// the current physical owner. Generated accessors load the slot directly after +// first touch; the block list is retained only as a GC root and teardown list. +// +//go:noinline +func LocalPackage(cacheSlot *uintptr, size, align uintptr) unsafe.Pointer { + ctx := (*LocalContext)(unsafe.Pointer(currentLocalContext)) + if ctx == nil { + panic("runtime: local variable accessed outside a Go entry context") + } + if cacheSlot == nil { + panic("runtime: nil local cache slot") + } + if data := unsafe.Pointer(*cacheSlot); data != nil { + return data + } + if align == 0 || align&(align-1) != 0 { + panic("runtime: invalid local package alignment") + } + data := newLocalBlock(cacheSlot, size, align) + localBlockHeader(data).next = ctx.blocks + ctx.blocks = data + *cacheSlot = uintptr(data) + return data +} + +func newLocalBlock(cacheSlot *uintptr, size, align uintptr) unsafe.Pointer { + header := unsafe.Sizeof(localBlock{}) + padding := align - 1 + if size == 0 { + size = 1 + } + if header > ^uintptr(0)-padding || header+padding > ^uintptr(0)-size { + panic("runtime: local package size overflow") + } + allocation := AllocZ(header + padding + size) + if allocation == nil { + panic("runtime: failed to allocate local package") + } + data := unsafe.Pointer((uintptr(allocation) + header + padding) &^ padding) + block := localBlockHeader(data) + block.cacheSlot = cacheSlot + return data +} + +func localBlockHeader(data unsafe.Pointer) *localBlock { + return (*localBlock)(unsafe.Pointer(uintptr(data) - unsafe.Sizeof(localBlock{}))) +} diff --git a/runtime/internal/runtime/local_context_baremetal.go b/runtime/internal/runtime/local_context_baremetal.go new file mode 100644 index 0000000000..30557385ec --- /dev/null +++ b/runtime/internal/runtime/local_context_baremetal.go @@ -0,0 +1,23 @@ +//go:build llgo && baremetal + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +// Bare-metal runtimes have one execution context and do not introduce a native +// TLS relocation merely by linking the locality runtime support. +var currentLocalContext uintptr diff --git a/runtime/internal/runtime/local_context_stub.go b/runtime/internal/runtime/local_context_stub.go new file mode 100644 index 0000000000..4dd029e4ef --- /dev/null +++ b/runtime/internal/runtime/local_context_stub.go @@ -0,0 +1,42 @@ +//go:build !llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import "unsafe" + +type LocalContext struct{} + +func EnterLocalContext(ctx *LocalContext) uintptr { + return 0 +} + +func LeaveLocalContext(ctx *LocalContext, previous uintptr) {} + +func leaveCurrentLocalContext() {} + +func LocalPackage(cacheSlot *uintptr, size, align uintptr) unsafe.Pointer { + if cacheSlot != nil && *cacheSlot != 0 { + return unsafe.Pointer(*cacheSlot) + } + data := AllocZ(size) + if cacheSlot != nil { + *cacheSlot = uintptr(data) + } + return data +} diff --git a/runtime/internal/runtime/local_context_tls.go b/runtime/internal/runtime/local_context_tls.go new file mode 100644 index 0000000000..a06aa1cd93 --- /dev/null +++ b/runtime/internal/runtime/local_context_tls.go @@ -0,0 +1,26 @@ +//go:build llgo && !baremetal + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +// currentLocalContext is only an address cache. The context itself is rooted by +// the outer entry frame, so this native TLS slot intentionally has no GC-visible +// pointer type. +// +//llgo:tls +var currentLocalContext uintptr diff --git a/runtime/internal/runtime/local_initializer.go b/runtime/internal/runtime/local_initializer.go new file mode 100644 index 0000000000..a0aeac2ebb --- /dev/null +++ b/runtime/internal/runtime/local_initializer.go @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import "unsafe" + +// The compiler emits localInitReady directly from cl/locality_lower.go. Keep +// these numeric values stable and update the compiler constant if they change. +const ( + localInitUninitialized uint8 = iota + localInitInitializing + localInitReady + localInitFailed +) + +// EnsureLocalInitializer executes one package/locality dispatcher at most once +// in the current owner. Recursive access observes partial initialization; a +// recovered failure remains failed and re-panics on every later access. +func EnsureLocalInitializer(state *uint8, failureCache *uintptr, initialize func()) { + switch *state { + case localInitReady: + return + case localInitInitializing: + return + case localInitFailed: + panic(*localInitializerFailure(failureCache)) + case localInitUninitialized: + default: + panic("runtime: invalid local initializer state") + } + *state = localInitInitializing + completed := false + defer func() { + if completed { + return + } + value := recover() + *localInitializerFailure(failureCache) = value + *state = localInitFailed + panic(value) + }() + initialize() + completed = true + *state = localInitReady +} + +func localInitializerFailure(cache *uintptr) *any { + var value any + return (*any)(LocalPackage(cache, unsafe.Sizeof(value), unsafe.Alignof(value))) +} diff --git a/runtime/internal/runtime/z_baremetal.go b/runtime/internal/runtime/z_baremetal.go index a862225852..e8bb76f386 100644 --- a/runtime/internal/runtime/z_baremetal.go +++ b/runtime/internal/runtime/z_baremetal.go @@ -21,6 +21,10 @@ func Rethrow(link *Defer) { c.Printf(c.Str("fatal error\n")) c.Exit(2) } else { + if ptr := getg().panic_; ptr != nil && link == GetThreadDefer() { + node := (*panicNode)(ptr) + node.defer_ = link + } setjmp.Longjmp((*setjmp.JmpBuf)(link.Addr), 1) } } diff --git a/runtime/internal/runtime/z_default.go b/runtime/internal/runtime/z_default.go index d0757f9cb1..842a950403 100644 --- a/runtime/internal/runtime/z_default.go +++ b/runtime/internal/runtime/z_default.go @@ -3,6 +3,8 @@ package runtime import ( + "unsafe" + c "github.com/goplus/llgo/runtime/internal/clite" "github.com/goplus/llgo/runtime/internal/clite/debug" "github.com/goplus/llgo/runtime/internal/clite/pthread" @@ -16,18 +18,24 @@ var ( // Rethrow rethrows a panic. func Rethrow(link *Defer) { - if ptr := excepKey.Get(); ptr != nil { + gp := getg() + if ptr := gp.panic_; ptr != nil { if link == nil { - TracePanic(*(*any)(ptr)) + node := (*panicNode)(ptr) + TracePanic(node.arg) if PanicTraceback == nil || !PanicTraceback(2) { debug.PrintStack(2) } - c.Free(ptr) + c.Free(unsafe.Pointer(node)) c.Exit(2) } else { + node := (*panicNode)(ptr) + if link == gp.defer_ { + node.defer_ = link + } c.Siglongjmp(link.Addr, 1) } - } else if ptr := goexitKey.Get(); ptr != nil { + } else if gp.goexit { // Goexit must run deferred functions before terminating the current // goroutine. Reuse the longjmp-based defer unwinding: // 1) If we have a defer frame, longjmp to it so it can execute defers. @@ -36,10 +44,11 @@ func Rethrow(link *Defer) { if link != nil { c.Siglongjmp(link.Addr, 1) } - if pthread.Equal(mainThread, pthread.Self()) != 0 { + if gp.isMain { fatal("no goroutines (main called runtime.Goexit) - deadlock!") c.Exit(2) } + leaveCurrentLocalContext() pthread.Exit(nil) } } diff --git a/runtime/internal/runtime/z_rt.go b/runtime/internal/runtime/z_rt.go index 17c89f55da..8e1c893ea3 100644 --- a/runtime/internal/runtime/z_rt.go +++ b/runtime/internal/runtime/z_rt.go @@ -20,7 +20,6 @@ import ( "unsafe" c "github.com/goplus/llgo/runtime/internal/clite" - "github.com/goplus/llgo/runtime/internal/clite/pthread" "github.com/goplus/llgo/runtime/internal/clite/setjmp" ) @@ -36,13 +35,28 @@ type Defer struct { Args unsafe.Pointer // defer func and args links } +type panicNode struct { + prev unsafe.Pointer + arg any + defer_ *Defer +} + // Recover recovers a panic. -func Recover() (ret any) { - ptr := excepKey.Get() +func Recover(token unsafe.Pointer) (ret any) { + gp := getg() + if token == nil || token != gp.recoverFrame { + return nil + } + ptr := gp.panic_ if ptr != nil { - excepKey.Set(nil) - ret = *(*any)(ptr) - c.Free(ptr) + node := (*panicNode)(ptr) + if node.defer_ != gp.defer_ { + return nil + } + gp.panic_ = node.prev + gp.recoverFrame = nil + ret = node.arg + c.Free(unsafe.Pointer(node)) if PanicRecovered != nil { PanicRecovered() } @@ -50,34 +64,54 @@ func Recover() (ret any) { return } +// StartRecoverFrame enables direct recover calls made by the deferred function +// currently being invoked from frame. +func StartRecoverFrame(frame unsafe.Pointer) unsafe.Pointer { + gp := getg() + old := gp.recoverFrame + gp.recoverFrame = frame + return old +} + +// EndRecoverFrame restores direct recover permission after a deferred call. +func EndRecoverFrame(frame unsafe.Pointer) { + getg().recoverFrame = frame +} + +// StartRecoverFrameAlias maps a direct deferred closure wrapper to the wrapped +// function while the wrapper calls into it. +func StartRecoverFrameAlias(from, to unsafe.Pointer) unsafe.Pointer { + gp := getg() + old := gp.recoverFrame + if old == from { + gp.recoverFrame = to + } + return old +} + // Panic panics with a value. func Panic(v any) { if v == nil { v = &PanicNilError{} } SavePanicCallerFrames() - ptr := c.Malloc(unsafe.Sizeof(v)) - *(*any)(ptr) = v - excepKey.Set(ptr) - - Rethrow((*Defer)(c.GoDeferData())) + gp := getg() + ptr := (*panicNode)(c.Malloc(unsafe.Sizeof(panicNode{}))) + ptr.prev = gp.panic_ + ptr.arg = v + ptr.defer_ = gp.defer_ + gp.panic_ = unsafe.Pointer(ptr) + + Rethrow(gp.defer_) } - -var ( - excepKey pthread.Key - goexitKey pthread.Key - mainThread pthread.Thread -) - func Goexit() { - goexitKey.Set(unsafe.Pointer(&goexitKey)) - Rethrow((*Defer)(c.GoDeferData())) + gp := getg() + gp.goexit = true + Rethrow(gp.defer_) } func init() { - excepKey.Create(nil) - goexitKey.Create(nil) - mainThread = pthread.Self() + getg().isMain = true } // ----------------------------------------------------------------------------- diff --git a/runtime/internal/runtime/z_signal.go b/runtime/internal/runtime/z_signal.go index 1283dff626..e0e98bce9a 100644 --- a/runtime/internal/runtime/z_signal.go +++ b/runtime/internal/runtime/z_signal.go @@ -38,11 +38,15 @@ const ( // For wasm platform compatibility, signal handling is excluded via build tags. // See PR #1059 for wasm platform requirements. func init() { - signal.Signal(SIGSEGV, func(v c.Int) { - if v == SIGSEGV { + handleFault := func(v c.Int) { + if v == SIGSEGV || v == SIGBUS { panic(errorString("invalid memory address or nil pointer dereference")) } var buf [20]byte panic(errorString("unexpected signal value: " + string(itoa(buf[:], uint64(v))))) - }) + } + signal.Signal(SIGSEGV, handleFault) + if SIGBUS != 0 { + signal.Signal(SIGBUS, handleFault) + } } diff --git a/runtime/internal/runtime/z_signal_darwin.go b/runtime/internal/runtime/z_signal_darwin.go new file mode 100644 index 0000000000..d34ee86607 --- /dev/null +++ b/runtime/internal/runtime/z_signal_darwin.go @@ -0,0 +1,7 @@ +//go:build darwin && !wasm && !baremetal + +package runtime + +import c "github.com/goplus/llgo/runtime/internal/clite" + +const SIGBUS = c.Int(0xa) diff --git a/runtime/internal/runtime/z_signal_linux.go b/runtime/internal/runtime/z_signal_linux.go new file mode 100644 index 0000000000..ff74f64db4 --- /dev/null +++ b/runtime/internal/runtime/z_signal_linux.go @@ -0,0 +1,7 @@ +//go:build linux && !wasm && !baremetal + +package runtime + +import c "github.com/goplus/llgo/runtime/internal/clite" + +const SIGBUS = c.Int(0x7) diff --git a/runtime/internal/runtime/z_signal_other.go b/runtime/internal/runtime/z_signal_other.go new file mode 100644 index 0000000000..5be18c7bb6 --- /dev/null +++ b/runtime/internal/runtime/z_signal_other.go @@ -0,0 +1,7 @@ +//go:build !darwin && !linux && !wasm && !baremetal + +package runtime + +import c "github.com/goplus/llgo/runtime/internal/clite" + +const SIGBUS = c.Int(0) diff --git a/ssa/closure_wrap.go b/ssa/closure_wrap.go index 470a299caf..abac18e99a 100644 --- a/ssa/closure_wrap.go +++ b/ssa/closure_wrap.go @@ -53,20 +53,38 @@ func closureWrapArgs(fn Function) []Expr { return args } -// closureWrapReturn returns from wrapper, preserving tail-call eligibility. -func closureWrapReturn(b Builder, sig *types.Signature, ret Expr) { +// closureWrapReturn returns from wrapper, preserving tail-call eligibility when +// the wrapper does not need post-call cleanup. +func closureWrapReturn(b Builder, sig *types.Signature, ret Expr, tail bool) { n := sig.Results().Len() if n == 0 { - if !ret.impl.IsNil() { + if tail && !ret.impl.IsNil() { ret.impl.SetTailCall(true) } b.impl.CreateRetVoid() return } - ret.impl.SetTailCall(true) + if tail { + ret.impl.SetTailCall(true) + } b.impl.CreateRet(ret.impl) } +func closureWrapCall(b Builder, wrap Function, fn Expr, args []Expr, aliasRecover bool) (Expr, bool) { + if !aliasRecover || (b.Prog.rt == nil && b.Prog.rtget == nil) { + return b.Call(fn, args...), true + } + prog := b.Prog + prev := b.Call( + b.Pkg.rtFunc("StartRecoverFrameAlias"), + b.PtrCast(prog.VoidPtr(), wrap.Expr), + b.PtrCast(prog.VoidPtr(), fn), + ) + ret := b.Call(fn, args...) + b.Call(b.Pkg.rtFunc("EndRecoverFrame"), prev) + return ret, false +} + // closureWrapDecl wraps a function declaration that lacks __llgo_ctx. // It directly calls the target symbol and ignores the ctx parameter. func (p Package) closureWrapDecl(fn Expr, sig *types.Signature) Function { @@ -80,8 +98,8 @@ func (p Package) closureWrapDecl(fn Expr, sig *types.Signature) Function { wrap.impl.SetLinkage(llvm.LinkOnceAnyLinkage) b := wrap.MakeBody(1) args := closureWrapArgs(wrap) - ret := b.Call(fn, args...) - closureWrapReturn(b, sig, ret) + ret, tail := closureWrapCall(b, wrap, fn, args, fn.mayRecover()) + closureWrapReturn(b, sig, ret, tail) return wrap } @@ -105,7 +123,7 @@ func (p Package) closureWrapPtr(sig *types.Signature) Function { fnPtr := b.Convert(fnPtrType, ctxArg) fnVal := b.Load(fnPtr) args := closureWrapArgs(wrap) - ret := b.Call(fnVal, args...) - closureWrapReturn(b, sig, ret) + ret, tail := closureWrapCall(b, wrap, fnVal, args, true) + closureWrapReturn(b, sig, ret, tail) return wrap } diff --git a/ssa/decl.go b/ssa/decl.go index df70a66a0e..a575dd1bee 100644 --- a/ssa/decl.go +++ b/ssa/decl.go @@ -117,9 +117,25 @@ func (p Package) NewVarEx(name string, t Type) Global { return p.doNewVar(name, t) } +// NewThreadLocalVar creates a native TLS variable. Unlike NewVar, it keeps +// independent storage for zero-sized values instead of using the module-wide +// zero-sized allocation sentinel. +func (p Package) NewThreadLocalVar(name string, typ types.Type, bg Background) Global { + if v, ok := p.vars[name]; ok { + v.impl.SetThreadLocal(true) + return v + } + t := p.Prog.Type(typ, bg) + return p.doNewVarEx(name, t, true) +} + func (p Package) doNewVar(name string, t Type) Global { + return p.doNewVarEx(name, t, false) +} + +func (p Package) doNewVarEx(name string, t Type, threadLocal bool) Global { typ := p.Prog.Elem(t).ll - if p.Prog.td.TypeAllocSize(typ) == 0 { + if !threadLocal && p.Prog.td.TypeAllocSize(typ) == 0 { var rt *types.Package if p.Prog.rt != nil || p.Prog.rtget != nil { rt = p.Prog.runtime() @@ -141,6 +157,7 @@ func (p Package) doNewVar(name string, t Type) Global { } } gbl := llvm.AddGlobal(p.mod, typ, name) + gbl.SetThreadLocal(threadLocal) alignment := p.Prog.td.ABITypeAlignment(typ) gbl.SetAlignment(alignment) ret := &aGlobal{Expr{gbl, t}} diff --git a/ssa/eh.go b/ssa/eh.go index b8ead4eb64..74b43a1c97 100644 --- a/ssa/eh.go +++ b/ssa/eh.go @@ -165,11 +165,12 @@ type aDefer struct { // The id uniquely identifies the defer call site for dispatch during drain. // typ is the node struct type needed to decode the linked-list node. type loopDeferCase struct { - id Expr - typ Type - fn Expr - args []Expr - buildCall func(Builder, Expr, ...Expr) Expr + id Expr + typ Type + mayRecover bool + fn Expr + args []Expr + buildCall func(Builder, Expr, ...Expr) Expr } const ( @@ -319,6 +320,11 @@ func (b Builder) DeferStackDrain() { // Defer emits a defer instruction. func (b Builder) Defer(kind DoAction, fn Expr, buildCall func(Builder, Expr, ...Expr) Expr, args ...Expr) { + b.DeferRecover(kind, deferMayRecover(fn), fn, buildCall, args...) +} + +// DeferRecover emits a defer instruction with explicit recover capability. +func (b Builder) DeferRecover(kind DoAction, mayRecover bool, fn Expr, buildCall func(Builder, Expr, ...Expr) Expr, args ...Expr) { dbgInstrCall("Defer", fn, args) var prog Program var nextbit Expr @@ -346,14 +352,20 @@ func (b Builder) Defer(kind DoAction, fn Expr, buildCall func(Builder, Expr, ... } typ := b.saveDeferArgs(self, kind, id, fn, args) if kind == DeferInLoop { - loopCase := loopDeferCase{id: id, typ: typ, fn: fn, args: args, buildCall: buildCall} + loopCase := loopDeferCase{id: id, typ: typ, mayRecover: mayRecover, fn: fn, args: args, buildCall: buildCall} self.loopCases = append(self.loopCases, loopCase) } - b.appendDeferStmt(self, kind, typ, buildCall, fn, args, nextbit) + b.appendDeferStmt(self, kind, typ, mayRecover, buildCall, fn, args, nextbit) } // DeferTo emits a defer instruction into an explicit runtime defer stack. func (b Builder) DeferTo(owner Function, stack Expr, fn Expr, buildCall func(Builder, Expr, ...Expr) Expr, args ...Expr) { + b.DeferToRecover(owner, stack, deferMayRecover(fn), fn, buildCall, args...) +} + +// DeferToRecover emits a defer instruction into an explicit runtime defer +// stack with explicit recover capability. +func (b Builder) DeferToRecover(owner Function, stack Expr, mayRecover bool, fn Expr, buildCall func(Builder, Expr, ...Expr) Expr, args ...Expr) { if debugInstr { logCall("DeferTo", fn, args) } @@ -367,11 +379,12 @@ func (b Builder) DeferTo(owner Function, stack Expr, fn Expr, buildCall func(Bui argsPtr := b.PtrCast(b.Prog.Pointer(b.Prog.VoidPtr()), stack) typ := b.saveDeferArgsTo(argsPtr, DeferInLoop, id, fn, args) loopCase := loopDeferCase{ - id: id, - typ: typ, - fn: fn, - args: args, - buildCall: buildCall, + id: id, + typ: typ, + mayRecover: mayRecover, + fn: fn, + args: args, + buildCall: buildCall, } if self == nil { owner.pendingLoopCases = append(owner.pendingLoopCases, loopCase) @@ -380,7 +393,7 @@ func (b Builder) DeferTo(owner Function, stack Expr, fn Expr, buildCall func(Bui self.loopCases = append(self.loopCases, loopCase) } -func (b Builder) appendDeferStmt(self *aDefer, kind DoAction, typ Type, buildCall func(Builder, Expr, ...Expr) Expr, fn Expr, args []Expr, nextbit Expr) { +func (b Builder) appendDeferStmt(self *aDefer, kind DoAction, typ Type, mayRecover bool, buildCall func(Builder, Expr, ...Expr) Expr, fn Expr, args []Expr, nextbit Expr) { self.stmts = append(self.stmts, func(bits Expr) { switch kind { case DeferInCond: @@ -391,13 +404,13 @@ func (b Builder) appendDeferStmt(self *aDefer, kind DoAction, typ Type, buildCal zero := prog.Val(uintptr(0)) has := b.BinOp(token.NEQ, b.BinOp(token.AND, bits, nextbit), zero) b.IfThen(has, func() { - b.callDefer(self, typ, buildCall, fn, args) + b.callDefer(self, typ, mayRecover, buildCall, fn, args) }) case DeferAlways: // Leaving a run of loop defers; allow the next loop-defer statement // (earlier in source order) to generate its own drainer. self.loopDrainerGenerated = false - b.callDefer(self, typ, buildCall, fn, args) + b.callDefer(self, typ, mayRecover, buildCall, fn, args) case DeferInLoop: b.loopDeferDrainer(self) } @@ -457,7 +470,7 @@ func (b Builder) loopDeferDrainer(self *aDefer) { b.SetBlockEx(caseBlks[i], AtEnd, true) b.Store(self.rethPtr, drainEntryAddr) - b.callDefer(self, c.typ, c.buildCall, c.fn, c.args) + b.callDefer(self, c.typ, c.mayRecover, c.buildCall, c.fn, c.args) b.Jump(condBlk) } @@ -512,9 +525,11 @@ func (b Builder) saveDeferArgsTo(argsPtr Expr, kind DoAction, id Expr, fn Expr, return typ } -func (b Builder) callDefer(self *aDefer, typ Type, buildCall func(Builder, Expr, ...Expr) Expr, fn Expr, args []Expr) { +func (b Builder) callDefer(self *aDefer, typ Type, mayRecover bool, buildCall func(Builder, Expr, ...Expr) Expr, fn Expr, args []Expr) { if typ == nil { - buildCall(b, fn, args...) + b.callRecoverScopedDefer(fn, mayRecover, func() { + buildCall(b, fn, args...) + }) return } prog := b.Prog @@ -537,10 +552,69 @@ func (b Builder) callDefer(self *aDefer, typ Type, buildCall func(Builder, Expr, args[i] = b.getField(data, i+offset) } b.Call(b.Pkg.rtFunc("FreeDeferNode"), ptr) - buildCall(b, fn, args...) + b.callRecoverScopedDefer(fn, mayRecover, func() { + buildCall(b, fn, args...) + }) }) } +func (b Builder) callRecoverScopedDefer(fn Expr, mayRecover bool, call func()) { + if fn.IsNil() || fn.impl.IsNil() || isRecoverBuiltin(fn) { + call() + return + } + token := b.recoverDeferToken(fn, mayRecover) + if token.IsNil() { + call() + return + } + prev := b.Call(b.Pkg.rtFunc("StartRecoverFrame"), token) + call() + b.Call(b.Pkg.rtFunc("EndRecoverFrame"), prev) +} + +func (b Builder) recoverDeferToken(fn Expr, mayRecover bool) Expr { + switch fn.kind { + case vkClosure: + if !mayRecover { + return Nil + } + return b.PtrCast(b.Prog.VoidPtr(), b.Field(fn, 0)) + case vkFuncDecl: + if !mayRecover { + return Nil + } + return b.PtrCast(b.Prog.VoidPtr(), fn) + case vkFuncPtr: + return b.PtrCast(b.Prog.VoidPtr(), fn) + } + return Nil +} + +func deferMayRecover(fn Expr) bool { + if fn.IsNil() || fn.Type == nil { + return false + } + switch fn.kind { + case vkClosure, vkFuncDecl: + return fn.mayRecover() + case vkFuncPtr: + return true + } + return false +} + +func isRecoverBuiltin(fn Expr) bool { + if fn.IsNil() { + return false + } + if fn.kind != vkBuiltin { + return false + } + bi, ok := fn.raw.Type.(*builtinTy) + return ok && bi.name == "recover" +} + // RunDefers emits instructions to run deferred instructions. func (b Builder) RunDefers() { self := b.getDefer(DeferInCond) @@ -610,8 +684,7 @@ func (b Builder) Unreachable() { // Recover emits a recover instruction. func (b Builder) Recover() Expr { dbgInstrln("Recover") - // TODO(xsw): recover can't be a function call in Go - return b.Call(b.Pkg.rtFunc("Recover")) + return b.Call(b.Pkg.rtFunc("Recover"), b.PtrCast(b.Prog.VoidPtr(), b.Func.Expr)) } // Panic emits a panic instruction. diff --git a/ssa/eh_defer_test.go b/ssa/eh_defer_test.go deleted file mode 100644 index 5f99729b1e..0000000000 --- a/ssa/eh_defer_test.go +++ /dev/null @@ -1,158 +0,0 @@ -//go:build !llgo -// +build !llgo - -package ssa_test - -import ( - "strings" - "testing" - - "github.com/goplus/llgo/ssa" - "github.com/goplus/llgo/ssa/ssatest" -) - -func TestExplicitDeferStackIR(t *testing.T) { - prog := ssatest.NewProgram(t, nil) - pkg := prog.NewPackage("foo", "foo") - - callee := pkg.NewFunc("callee", ssa.NoArgsNoRet, ssa.InGo) - cb := callee.MakeBody(1) - cb.Return() - cb.EndBuild() - - fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) - b := fn.MakeBody(1) - fn.SetRecover(fn.MakeBlock()) - - stack := b.BuiltinCall("ssa:deferstack") - b.Return() - b.SetBlockEx(fn.Block(0), ssa.BeforeLast, true) - b.DeferTo(fn, stack, callee.Expr, ssa.Builder.Call) - b.DeferStackDrain() - b.RunDefers() - b.Return() - b.EndBuild() - - ir := pkg.Module().String() - if !strings.Contains(ir, "FreeDeferNode") { - t.Fatalf("expected explicit defer stack node cleanup in IR, got:\n%s", ir) - } - if !strings.Contains(ir, "sigsetjmp") && !strings.Contains(ir, "setjmp") { - t.Fatalf("expected explicit defer stack setup in IR, got:\n%s", ir) - } -} - -func TestExplicitDeferStackFallbackAndNilBuiltin(t *testing.T) { - prog := ssatest.NewProgram(t, nil) - pkg := prog.NewPackage("foo", "foo") - - callee := pkg.NewFunc("callee", ssa.NoArgsNoRet, ssa.InGo) - cb := callee.MakeBody(1) - cb.Return() - cb.EndBuild() - - fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) - b := fn.MakeBody(1) - stack := b.BuiltinCall("ssa:deferstack") - if stack.Type != prog.VoidPtr() { - t.Fatalf("ssa:deferstack without recover returned %v, want %v", stack.Type, prog.VoidPtr()) - } - b.DeferTo(nil, stack, callee.Expr, ssa.Builder.Call) - b.Return() - b.EndBuild() - - ir := pkg.Module().String() - if strings.Contains(ir, "sigsetjmp") || strings.Contains(ir, "setjmp") { - t.Fatalf("unexpected defer stack setup without recover, got:\n%s", ir) - } -} - -func TestExplicitDeferStackDrainWithoutLoopCases(t *testing.T) { - prog := ssatest.NewProgram(t, nil) - pkg := prog.NewPackage("foo", "foo") - - fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) - b := fn.MakeBody(1) - fn.SetRecover(fn.MakeBlock()) - - _ = b.BuiltinCall("ssa:deferstack") - b.DeferStackDrain() - b.RunDefers() - b.Return() - b.EndBuild() - - ir := pkg.Module().String() - if strings.Contains(ir, "FreeDeferNode") { - t.Fatalf("unexpected explicit defer cleanup without loop cases, got:\n%s", ir) - } - if !strings.Contains(ir, "sigsetjmp") && !strings.Contains(ir, "setjmp") { - t.Fatalf("expected defer stack setup with recover, got:\n%s", ir) - } -} - -func TestExplicitDeferStackDrainWithoutRecoverNoop(t *testing.T) { - prog := ssatest.NewProgram(t, nil) - pkg := prog.NewPackage("foo", "foo") - - fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) - b := fn.MakeBody(1) - b.DeferStackDrain() - b.Return() - b.EndBuild() - - ir := pkg.Module().String() - if strings.Contains(ir, "FreeDeferNode") || strings.Contains(ir, "sigsetjmp") || strings.Contains(ir, "setjmp") { - t.Fatalf("unexpected defer stack machinery without recover, got:\n%s", ir) - } -} - -func TestPlainDeferWithoutSavedArgsIR(t *testing.T) { - prog := ssatest.NewProgram(t, nil) - pkg := prog.NewPackage("foo", "foo") - - callee := pkg.NewFunc("callee", ssa.NoArgsNoRet, ssa.InGo) - cb := callee.MakeBody(1) - cb.Return() - cb.EndBuild() - - fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) - b := fn.MakeBody(1) - fn.SetRecover(fn.MakeBlock()) - b.Defer(ssa.DeferAlways, callee.Expr, ssa.Builder.Call) - b.RunDefers() - b.Return() - b.EndBuild() - - ir := pkg.Module().String() - if strings.Contains(ir, "FreeDeferNode") { - t.Fatalf("plain zero-arg defer should not allocate defer nodes, got:\n%s", ir) - } - if !strings.Contains(ir, "call void @callee()") { - t.Fatalf("expected direct deferred call in IR, got:\n%s", ir) - } -} - -func TestConditionalDeferIR(t *testing.T) { - prog := ssatest.NewProgram(t, nil) - pkg := prog.NewPackage("foo", "foo") - - callee := pkg.NewFunc("callee", ssa.NoArgsNoRet, ssa.InGo) - cb := callee.MakeBody(1) - cb.Return() - cb.EndBuild() - - fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) - b := fn.MakeBody(1) - fn.SetRecover(fn.MakeBlock()) - b.Return() - b.SetBlockEx(fn.Block(0), ssa.BeforeLast, true) - b.Defer(ssa.DeferInCond, callee.Expr, ssa.Builder.Call) - b.RunDefers() - b.Return() - b.EndBuild() - - ir := pkg.Module().String() - if !strings.Contains(ir, "or i64") || !strings.Contains(ir, "and i64") { - t.Fatalf("expected conditional defer bitmask operations in IR, got:\n%s", ir) - } -} diff --git a/ssa/expr.go b/ssa/expr.go index caee097c06..28252936fe 100644 --- a/ssa/expr.go +++ b/ssa/expr.go @@ -48,6 +48,8 @@ type Expr struct { var Nil Expr // Zero value is a nil Expr +const mayRecoverAttr = "llgo.may-recover" + // IsNil checks if the expression is nil or not. func (v Expr) IsNil() bool { return v.Type == nil @@ -59,6 +61,33 @@ func (v Expr) SetOrdering(ordering AtomicOrdering) Expr { return v } +// SetVolatile marks a load or store as volatile. +func (v Expr) SetVolatile(volatile bool) Expr { + v.impl.SetVolatile(volatile) + return v +} + +// MarkMayRecover marks a function or closure that may call recover directly. +func (v Expr) MarkMayRecover() Expr { + if v.Type == nil || v.impl.IsNil() { + return v + } + fn := v.impl.IsAFunction() + if !fn.IsNil() && fn.GetStringAttributeAtIndex(-1, mayRecoverAttr).IsNil() { + ctx := fn.GlobalParent().Context() + fn.AddFunctionAttr(ctx.CreateStringAttribute(mayRecoverAttr, "true")) + } + return v +} + +func (v Expr) mayRecover() bool { + if v.Type == nil || v.impl.IsNil() { + return false + } + fn := v.impl.IsAFunction() + return !fn.IsNil() && !fn.GetStringAttributeAtIndex(-1, mayRecoverAttr).IsNil() +} + func (v Expr) SetName(alias string) Expr { v.impl.SetName(alias) return v diff --git a/ssa/goroutine.go b/ssa/goroutine.go index 1c9f5708c3..8f2c30a578 100644 --- a/ssa/goroutine.go +++ b/ssa/goroutine.go @@ -110,6 +110,11 @@ func (p Package) routine(t Type, fn Expr, buildCall func(Builder, Expr, ...Expr) prog := p.Prog routine := p.NewFunc(p.routineName(), prog.tyRoutine(), InC) b := routine.MakeBody(1) + var localCtx, previousLocalCtx Expr + hasLocalContext := prog.NeedsLocalContext() + if hasLocalContext { + localCtx, previousLocalCtx = b.EnterLocalContext() + } param := routine.Param(0) data := Expr{llvm.CreateLoad(b.impl, t.ll, param.impl), t} args := make([]Expr, n) @@ -125,6 +130,9 @@ func (p Package) routine(t Type, fn Expr, buildCall func(Builder, Expr, ...Expr) buildCall(b, fn, args...) lastInst := b.impl.GetInsertBlock().LastInstruction() if lastInst.IsNil() || lastInst.IsAUnreachableInst().IsNil() { + if hasLocalContext { + b.LeaveLocalContext(localCtx, previousLocalCtx) + } b.Return(prog.Nil(prog.VoidPtr())) } return routine.Expr diff --git a/ssa/goroutine_patch_test.go b/ssa/goroutine_patch_test.go index 16af8643c9..28f0b8bf8f 100644 --- a/ssa/goroutine_patch_test.go +++ b/ssa/goroutine_patch_test.go @@ -53,6 +53,29 @@ func TestGoClosureStartupUsesGCManagedMemory(t *testing.T) { if got := strings.Count(ir, `"github.com/goplus/llgo/runtime/internal/runtime.AllocU"`); got < 1 { t.Fatalf("expected closure ctx to use AllocU, got %d:\n%s", got, ir) } + if strings.Contains(ir, "EnterLocalContext") { + t.Fatalf("program without context-backed locals paid locality entry cost:\n%s", ir) + } +} + +func TestGoInstallsContextForContextBackedLocals(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + prog.SetLocalityInfo("example.com/state.Value", ssa.LocalityInfo{Locality: ssa.GoroutineLocal}) + prog.SetLocalStorage("example.com/state.Value", ssa.LocalStoragePackage) + pkg := prog.NewPackage("bar", "foo/bar") + outer := pkg.NewFunc("outer", ssa.NoArgsNoRet, ssa.InGo) + b := outer.MakeBody(1) + b.Go(ssa.Nil, func(b ssa.Builder, _ ssa.Expr, args ...ssa.Expr) ssa.Expr { + return ssa.Expr{} + }) + b.Return() + + ir := pkg.String() + for _, want := range []string{"LocalContext", "EnterLocalContext", "LeaveLocalContext"} { + if !strings.Contains(ir, want) { + t.Fatalf("goroutine wrapper missing %q:\n%s", want, ir) + } + } } func TestGoPanicRoutineDoesNotReturnAfterUnreachable(t *testing.T) { diff --git a/ssa/interface.go b/ssa/interface.go index 2539c3187e..9d18128be0 100644 --- a/ssa/interface.go +++ b/ssa/interface.go @@ -331,8 +331,15 @@ func (b Builder) TypeAssert(x Expr, assertedTyp Type, commaOk bool) Expr { blks := b.Func.MakeBlocks(2) b.If(eq, blks[0], blks[1]) b.SetBlockEx(blks[1], AtEnd, false) - b.Call(b.Pkg.rtFunc("PanicTypeAssert"), tx, b.Str(assertedTyp.RawType().String()), b.Str(typeAssertMissingMethod(assertedTyp))) - b.Unreachable() + // Panic with a real *runtime.TypeAssertionError (gc semantics: the + // recovered value implements runtime.Error; the missing method is + // computed at runtime from the abi tables). The source-interface + // abi type is deliberately not passed: materializing abiType for + // arbitrary static interface types here can reference another + // package's private local-generic symbols (undefined at link); + // the message's interface name is the documented mdempsky/16 + // residual, pending that abi emission fix. + b.Panic(b.InlineCall(b.Pkg.rtFunc("TypeAssertError"), tx, tabi, b.Prog.Nil(b.Prog.AbiTypePtr()))) b.SetBlockEx(blks[0], AtEnd, false) b.blk.last = blks[0].last return val() diff --git a/ssa/local_context.go b/ssa/local_context.go new file mode 100644 index 0000000000..184b2b5ec4 --- /dev/null +++ b/ssa/local_context.go @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ssa + +import ( + "go/token" + "go/types" +) + +const runtimeLocalContext = "LocalContext" + +// EnterLocalContext creates the stack root used by TLS/GLS locality blocks and +// installs it for the current outermost Go entry. previous is nonzero only for +// a nested entry that inherited an existing context. +func (b Builder) EnterLocalContext() (ctx, previous Expr) { + fn := b.Pkg.rtFunc("EnterLocalContext") + params := fn.raw.Type.(*types.Signature).Params() + ctxPtr := b.Prog.rawType(params.At(0).Type()) + ctxType := b.Prog.rawType(ctxPtr.RawType().(*types.Pointer).Elem()) + ctx = b.Alloc(ctxType, false) + previous = b.Call(fn, ctx) + return +} + +// LeaveLocalContext restores an inherited context or drops the stack roots +// installed by EnterLocalContext. +func (b Builder) LeaveLocalContext(ctx, previous Expr) { + b.Call(b.Pkg.rtFunc("LeaveLocalContext"), ctx, previous) +} + +// BuildLocalPackageAccessor builds an always-hot package block lookup around a +// generated owner-local cache slot. The runtime owns allocation and rooting; +// the compiler needs no LocalContext or block-header layout knowledge. +func (p Function) BuildLocalPackageAccessor(cache, size, align Expr) { + prog := p.Prog + b := p.MakeBody(3) + hit := p.Block(1) + slow := p.Block(2) + cached := b.Load(cache) + b.If(b.BinOp(token.NEQ, cached, prog.IntVal(0, prog.Uintptr())), hit, slow) + + b.SetBlock(hit) + result := p.raw.Type.(*types.Signature).Results().At(0).Type() + b.Return(b.Convert(prog.rawType(result), cached)) + + b.SetBlock(slow) + raw := b.Call(p.Pkg.rtFunc("LocalPackage"), cache, size, align) + b.Return(b.Convert(prog.rawType(result), raw)) + b.EndBuild() +} diff --git a/ssa/local_context_test.go b/ssa/local_context_test.go new file mode 100644 index 0000000000..624696e635 --- /dev/null +++ b/ssa/local_context_test.go @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ssa + +import ( + "go/token" + "go/types" + "strings" + "testing" +) + +func TestBuildLocalPackageAccessor(t *testing.T) { + prog := NewProgram(nil) + prog.SetRuntime(localContextTestRuntime()) + pkg := prog.NewPackage("accessor", "example.com/accessor") + cache := pkg.NewThreadLocalVar("example.com/accessor.cache", types.NewPointer(types.Typ[types.Uintptr]), InGo) + cache.InitNil() + field := types.NewField(token.NoPos, nil, "pointer", types.NewPointer(types.Typ[types.Int]), false) + block := types.NewStruct([]*types.Var{field}, nil) + blockType := prog.Type(block, InGo) + result := types.NewPointer(block) + results := types.NewTuple(types.NewVar(token.NoPos, nil, "", result)) + accessor := pkg.NewFunc("example.com/accessor.block", types.NewSignatureType(nil, nil, nil, nil, results, false), InGo) + accessor.BuildLocalPackageAccessor( + cache.Expr, + prog.IntVal(prog.SizeOf(blockType), prog.Uintptr()), + prog.IntVal(prog.AlignOf(blockType), prog.Uintptr()), + ) + + ir := pkg.String() + if !strings.Contains(ir, `@"example.com/accessor.cache" = thread_local global i64 0`) { + t.Fatalf("direct cache definition not found:\n%s", ir) + } + if got := strings.Count(ir, "icmp "); got != 1 { + t.Fatalf("accessor comparisons = %d, want one cache check:\n%s", got, ir) + } + if !strings.Contains(ir, `call ptr @"`+PkgRuntime+`.LocalPackage"(ptr @"example.com/accessor.cache"`) { + t.Fatalf("accessor has no cache-backed LocalPackage slow path:\n%s", ir) + } +} + +func localContextTestRuntime() *types.Package { + pkg := types.NewPackage(PkgRuntime, "runtime") + unsafePointer := types.Typ[types.UnsafePointer] + + params := types.NewTuple( + types.NewVar(token.NoPos, pkg, "cache", types.NewPointer(types.Typ[types.Uintptr])), + types.NewVar(token.NoPos, pkg, "size", types.Typ[types.Uintptr]), + types.NewVar(token.NoPos, pkg, "align", types.Typ[types.Uintptr]), + ) + results := types.NewTuple(types.NewVar(token.NoPos, pkg, "", unsafePointer)) + pkg.Scope().Insert(types.NewFunc(token.NoPos, pkg, "LocalPackage", types.NewSignatureType(nil, nil, nil, params, results, false))) + return pkg +} diff --git a/ssa/locality.go b/ssa/locality.go new file mode 100644 index 0000000000..897d5b94bd --- /dev/null +++ b/ssa/locality.go @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ssa + +import ( + "fmt" + "go/types" + "sort" + "strings" + "sync" + + "github.com/goplus/llgo/internal/locality" + localitylayout "github.com/goplus/llgo/internal/locality/layout" +) + +type Locality = locality.Kind + +const ( + LocalityNone = locality.None + ThreadLocal = locality.Thread + GoroutineLocal = locality.Goroutine +) + +type LocalityInfo = locality.Info +type LocalStorage = localitylayout.Storage + +const ( + LocalStorageUnknown = localitylayout.StorageUnknown + LocalStorageNativeTLS = localitylayout.StorageNativeTLS + LocalStoragePackage = localitylayout.StoragePackage +) + +// VariableLocality is the locality metadata attached to one package variable. +type VariableLocality struct { + LocalStorage LocalStorage + locality.Info +} + +type localityInfos struct { + mu sync.RWMutex + entries map[string]VariableLocality + parsedPackages map[*types.Package]struct{} +} + +func newLocalityInfos() *localityInfos { + return &localityInfos{ + entries: make(map[string]VariableLocality), + parsedPackages: make(map[*types.Package]struct{}), + } +} + +func (p *localityInfos) update(name string, update func(*VariableLocality)) { + p.mu.Lock() + info := p.entries[name] + update(&info) + p.entries[name] = info + p.mu.Unlock() +} + +func (p Program) SetLocalityInfo(name string, info LocalityInfo) { + p.localities.update(name, func(current *VariableLocality) { current.Info = info }) +} + +func (p Program) SetLocalStorage(name string, storage LocalStorage) { + p.localities.update(name, func(info *VariableLocality) { info.LocalStorage = storage }) +} + +func (p Program) VariableLocality(name string) (VariableLocality, bool) { + p.localities.mu.RLock() + info, ok := p.localities.entries[name] + p.localities.mu.RUnlock() + return info, ok +} + +// ResolveLocality returns locality metadata for one declaration. Locality +// variables cannot participate in go:linkname alias chains. +func (p Program) ResolveLocality(name string) (string, VariableLocality, bool, error) { + lookup := func(name string) (VariableLocality, bool) { + p.localities.mu.RLock() + info, ok := p.localities.entries[name] + p.localities.mu.RUnlock() + return info, ok + } + return resolveLocality(lookup, p.Linkname, name) +} + +func resolveLocality(lookup func(string) (VariableLocality, bool), linkname func(string) (string, bool), name string) (string, VariableLocality, bool, error) { + result, ok := lookup(name) + if !ok { + result = VariableLocality{} + } + var seen map[string]bool + current := name + for { + target, hasLink := linkname(current) + target = strings.TrimPrefix(target, "go:") + if !hasLink || target == "" { + return current, result, ok, nil + } + if seen == nil { + seen = make(map[string]bool) + } + if seen[current] { + return "", VariableLocality{}, false, fmt.Errorf("declaration linkname cycle involving %s", current) + } + seen[current] = true + if currentInfo, exists := lookup(current); exists && currentInfo.Locality != locality.None { + return "", VariableLocality{}, false, fmt.Errorf("local variable %s cannot use go:linkname", current) + } + if targetInfo, exists := lookup(target); exists && targetInfo.Locality != locality.None { + return "", VariableLocality{}, false, fmt.Errorf("go:linkname alias %s cannot reference local variable %s", name, target) + } + if target == current { + return current, result, ok, nil + } + current = target + } +} + +func hasInitialization(info locality.Info) bool { + return info.HasInitializer || info.InitFunc != "" || info.InitOrder != 0 +} + +func (p Program) ValidateLocalities(pkgPath string) error { + prefix := pkgPath + "." + p.localities.mu.RLock() + if len(p.localities.entries) == 0 { + p.localities.mu.RUnlock() + return nil + } + nameSet := make(map[string]bool) + localNames := make(map[string]bool) + for name, info := range p.localities.entries { + if info.Locality != locality.None { + localNames[name] = true + } + if strings.HasPrefix(name, prefix) { + nameSet[name] = true + } + } + p.localities.mu.RUnlock() + if len(localNames) == 0 { + return nil + } + p.linknameMu.RLock() + links := make(map[string]string, len(p.linkname)) + for name, target := range p.linkname { + links[name] = strings.TrimPrefix(target, "go:") + } + p.linknameMu.RUnlock() + for name := range links { + if strings.HasPrefix(name, prefix) && linknameReachesLocal(name, links, localNames) { + nameSet[name] = true + } + } + names := make([]string, 0, len(nameSet)) + for name := range nameSet { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + if _, _, _, err := p.ResolveLocality(name); err != nil { + return err + } + } + return nil +} + +func linknameReachesLocal(name string, links map[string]string, localNames map[string]bool) bool { + seen := make(map[string]bool) + for name != "" && !seen[name] { + if localNames[name] { + return true + } + seen[name] = true + name = links[name] + } + return false +} + +func (p Program) PackageSyntaxParsed(pkg *types.Package) bool { + p.localities.mu.RLock() + _, ok := p.localities.parsedPackages[pkg] + p.localities.mu.RUnlock() + return ok +} + +func (p Program) MarkPackageSyntaxParsed(pkg *types.Package) { + p.localities.mu.Lock() + p.localities.parsedPackages[pkg] = struct{}{} + p.localities.mu.Unlock() +} + +func (p Program) PackageLocalities(pkgPath string) map[string]VariableLocality { + prefix := pkgPath + "." + ret := make(map[string]VariableLocality) + p.localities.mu.RLock() + for name, info := range p.localities.entries { + if info.Locality != locality.None && strings.HasPrefix(name, prefix) { + ret[name] = info + } + } + p.localities.mu.RUnlock() + return ret +} + +func (p Program) NeedsLocalContext() bool { + p.localities.mu.RLock() + defer p.localities.mu.RUnlock() + for _, info := range p.localities.entries { + if info.Locality != locality.None && (info.LocalStorage != LocalStorageNativeTLS || hasInitialization(info.Info)) { + return true + } + } + return false +} diff --git a/ssa/locality_test.go b/ssa/locality_test.go new file mode 100644 index 0000000000..b15f59e513 --- /dev/null +++ b/ssa/locality_test.go @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ssa + +import ( + "go/types" + "strings" + "testing" +) + +func TestLocalityInfos(t *testing.T) { + prog := NewProgram(nil) + pkg := types.NewPackage("example.com/p", "p") + if prog.PackageSyntaxParsed(pkg) { + t.Fatal("new package was already marked as parsed") + } + prog.MarkPackageSyntaxParsed(pkg) + if !prog.PackageSyntaxParsed(pkg) { + t.Fatal("package syntax parsed marker was not retained") + } + if prog.PackageSyntaxParsed(types.NewPackage("example.com/p", "p")) { + t.Fatal("syntax marker was shared by distinct package objects") + } + + name := "example.com/p.value" + prog.SetLocalityInfo(name, LocalityInfo{ + Locality: GoroutineLocal, + HasInitializer: true, + InitFunc: "example.com/p.initLocal", + InitOrder: 1, + }) + prog.SetLocalStorage(name, LocalStoragePackage) + + want, ok := prog.VariableLocality(name) + if !ok || want.Locality != GoroutineLocal || want.LocalStorage != LocalStoragePackage || !want.HasInitializer || want.InitFunc == "" || want.InitOrder != 1 { + t.Fatalf("VariableLocality(%q) = %+v, %v", name, want, ok) + } + + prog.SetLocalStorage("example.com/p.ordinary", LocalStorageNativeTLS) + decls := prog.PackageLocalities("example.com/p") + if len(decls) != 1 || decls[name] != want { + t.Fatalf("PackageLocalities = %+v", decls) + } + delete(decls, name) + if _, ok := prog.VariableLocality(name); !ok { + t.Fatal("mutating PackageLocalities changed program metadata") + } +} + +func TestNeedsLocalContext(t *testing.T) { + prog := NewProgram(nil) + if prog.NeedsLocalContext() { + t.Fatal("empty program needs a local context") + } + name := "example.com/p.value" + prog.SetLocalityInfo(name, LocalityInfo{Locality: ThreadLocal}) + if !prog.NeedsLocalContext() { + t.Fatal("unknown local storage did not conservatively require a context") + } + prog.SetLocalStorage(name, LocalStorageNativeTLS) + if prog.NeedsLocalContext() { + t.Fatal("native TLS required a local context") + } + prog.SetLocalityInfo(name, LocalityInfo{Locality: ThreadLocal, HasInitializer: true, InitFunc: "example.com/p.initValue", InitOrder: 1}) + if !prog.NeedsLocalContext() { + t.Fatal("native TLS initializer failure storage did not require a context") + } + prog.SetLocalityInfo(name, LocalityInfo{Locality: ThreadLocal}) + prog.SetLocalStorage(name, LocalStoragePackage) + if !prog.NeedsLocalContext() { + t.Fatal("context storage was not detected") + } +} + +func TestRejectsLinknameLocality(t *testing.T) { + prog := NewProgram(nil) + target := "example.com/target.value" + alias := "example.com/alias.value" + prog.SetLocalityInfo(target, LocalityInfo{Locality: ThreadLocal, HasInitializer: true, InitFunc: "example.com/target.initValue", InitOrder: 1}) + prog.SetLocalStorage(target, LocalStoragePackage) + if canonical, got, ok, err := prog.ResolveLocality(target); err != nil || canonical != target || !ok || got.LocalStorage != LocalStoragePackage { + t.Fatalf("direct ResolveLocality(%q) = %q, %+v, %v, %v", target, canonical, got, ok, err) + } + + prog.SetLinkname(alias, target) + if err := prog.ValidateLocalities("example.com/alias"); err == nil || !strings.Contains(err.Error(), "cannot reference local variable") { + t.Fatalf("alias-to-local error = %v", err) + } + + localAlias := "example.com/alias.local" + prog.SetLocalityInfo(localAlias, LocalityInfo{Locality: GoroutineLocal}) + prog.SetLinkname(localAlias, "example.com/target.ordinary") + if err := prog.ValidateLocalities("example.com/alias"); err == nil || !strings.Contains(err.Error(), "cannot use go:linkname") { + t.Fatalf("local-alias error = %v", err) + } +} + +func TestValidateLocalitiesIgnoresOrdinaryLinknameCycle(t *testing.T) { + prog := NewProgram(nil) + first := "example.com/p.first" + second := "example.com/p.second" + prog.SetLinkname(first, second) + prog.SetLinkname(second, first) + if err := prog.ValidateLocalities("example.com/p"); err != nil { + t.Fatalf("ordinary linkname cycle affected locality validation: %v", err) + } + if _, _, _, err := prog.ResolveLocality(first); err == nil || !strings.Contains(err.Error(), "linkname cycle") { + t.Fatalf("ResolveLocality cycle error = %v", err) + } +} + +func TestValidateLocalitySelfLinkname(t *testing.T) { + prog := NewProgram(nil) + name := "example.com/p.value" + prog.SetLinkname(name, name) + if err := prog.ValidateLocalities("example.com/p"); err != nil { + t.Fatal(err) + } + if canonical, _, ok, err := prog.ResolveLocality(name); err != nil || canonical != name || ok { + t.Fatalf("ordinary self-link ResolveLocality(%q) = %q, %v, %v", name, canonical, ok, err) + } + prog.SetLocalityInfo(name, LocalityInfo{Locality: ThreadLocal}) + if err := prog.ValidateLocalities("example.com/p"); err == nil || !strings.Contains(err.Error(), "cannot use go:linkname") { + t.Fatalf("local self-linkname error = %v", err) + } +} diff --git a/ssa/package.go b/ssa/package.go index 8282a676d6..edc89d1c12 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -23,6 +23,7 @@ import ( "log" "runtime" "strconv" + "sync" "unsafe" "github.com/goplus/llgo/internal/env" @@ -212,6 +213,7 @@ type aProgram struct { memsetInlineTy *types.Signature stackSaveTy *types.Signature stackRestoreTy *types.Signature + frameAddressTy *types.Signature createKeyTy *types.Signature getSpecTy *types.Signature @@ -224,7 +226,9 @@ type aProgram struct { printfTy *types.Signature paramObjPtr_ *types.Var - linkname map[string]string // pkgPath.nameInPkg => linkname + linknameMu sync.RWMutex + linkname map[string]string // pkgPath.nameInPkg => linkname + localities *localityInfos noInterface map[string]none // pkgPath.T.method or pkgPath.(*T).method abiSymbol map[string]*AbiSymbol // abi symbol name => AbiSymbol @@ -313,7 +317,8 @@ func NewProgram(target *Target) Program { ctx: ctx, gocvt: newGoTypes(), target: target, td: td, tm: tm, is32Bits: is32Bits, ptrSize: td.PointerSize(), named: make(map[string]Type), fnnamed: make(map[string]int), - linkname: make(map[string]string), noInterface: make(map[string]none), abiSymbol: make(map[string]*AbiSymbol), + linkname: make(map[string]string), localities: newLocalityInfos(), + noInterface: make(map[string]none), abiSymbol: make(map[string]*AbiSymbol), debugInfoOptimized: target.effectiveOptLevel() != optlevel.O0, } prog.abi.Init(uintptr(prog.ptrSize), (*goProgram)(unsafe.Pointer(prog))) @@ -395,11 +400,15 @@ func (p Program) SetTypeBackground(fullName string, bg Background) { } func (p Program) SetLinkname(name, link string) { + p.linknameMu.Lock() p.linkname[name] = link + p.linknameMu.Unlock() } func (p Program) Linkname(name string) (link string, ok bool) { + p.linknameMu.RLock() link, ok = p.linkname[name] + p.linknameMu.RUnlock() return } @@ -856,6 +865,11 @@ func (p Package) rtFunc(fnName string) Expr { return p.NewFunc(name, sig, InGo).Expr } +// RuntimeFunc returns a declaration for a function in LLGo's internal runtime. +func (p Package) RuntimeFunc(fnName string) Expr { + return p.rtFunc(fnName) +} + func (p Package) cFunc(fullName string, sig *types.Signature) Expr { return p.NewFunc(fullName, sig, InC).Expr } diff --git a/ssa/ssa_test.go b/ssa/ssa_test.go index c45e71035c..99e1f40040 100644 --- a/ssa/ssa_test.go +++ b/ssa/ssa_test.go @@ -165,6 +165,75 @@ func TestTooManyConditionalDefers(t *testing.T) { } } +func TestRecoverDeferTokenHelpers(t *testing.T) { + prog := NewProgram(nil) + pkg := prog.NewPackage("foo", "foo") + + callee := pkg.NewFunc("callee", NoArgsNoRet, InGo) + b := callee.MakeBody(1) + + if Nil.mayRecover() { + t.Fatal("nil expression should not be marked recover-capable") + } + if marked := Nil.MarkMayRecover(); !marked.IsNil() { + t.Fatalf("marked nil expression = %v, want nil", marked) + } + if deferMayRecover(callee.Expr) { + t.Fatal("unmarked function declaration should not be recover-capable") + } + if token := b.recoverDeferToken(callee.Expr, false); !token.IsNil() { + t.Fatalf("recover token without mayRecover = %v, want nil", token) + } + + callee.Expr.MarkMayRecover() + if !deferMayRecover(callee.Expr) { + t.Fatal("marked function declaration should be recover-capable") + } + if attr := callee.Expr.impl.GetStringAttributeAtIndex(-1, mayRecoverAttr); attr.IsNil() || attr.GetStringValue() != "true" { + t.Fatalf("recover marker attribute = %v, want %q", attr, "true") + } + callee.Expr.MarkMayRecover() + if !deferMayRecover(callee.Expr) { + t.Fatal("repeated recover marking cleared the marker") + } + if token := b.recoverDeferToken(callee.Expr, true); token.IsNil() { + t.Fatal("marked function declaration should produce a recover token") + } + + fnPtr := b.ChangeType(prog.rawType(NoArgsNoRet), callee.Expr) + if !deferMayRecover(fnPtr) { + t.Fatal("function pointer should be treated as recover-capable") + } + if token := b.recoverDeferToken(fnPtr, false); token.IsNil() { + t.Fatal("function pointer should produce a recover token") + } + if token := b.recoverDeferToken(prog.Val(1), true); !token.IsNil() { + t.Fatalf("non-function recover token = %v, want nil", token) + } + + if deferMayRecover(Nil) { + t.Fatal("nil expression should not be recover-capable") + } + if deferMayRecover(prog.Val(1)) { + t.Fatal("non-function expression should not be recover-capable") + } + if !isRecoverBuiltin(Builtin("recover")) { + t.Fatal("recover builtin should be recognized") + } + if isRecoverBuiltin(Builtin("panic")) { + t.Fatal("non-recover builtin should not be recognized") + } + if isRecoverBuiltin(Nil) { + t.Fatal("nil expression should not be a recover builtin") + } + if isRecoverBuiltin(callee.Expr) { + t.Fatal("function declaration should not be a recover builtin") + } + + b.Return() + b.EndBuild() +} + func TestPointerSize(t *testing.T) { expected := unsafe.Sizeof(uintptr(0)) if size := NewProgram(nil).PointerSize(); size != int(expected) { @@ -1087,15 +1156,23 @@ _llgo_0: define linkonce i64 @%s(ptr %%0, i64 %%1) { _llgo_0: %%2 = load ptr, ptr %%0, align 8 - %%3 = tail call i64 %%2(i64 %%1) - ret i64 %%3 + %%3 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.StartRecoverFrameAlias"(ptr @%s, ptr %%2) + %%4 = call i64 %%2(i64 %%1) + call void @"github.com/goplus/llgo/runtime/internal/runtime.EndRecoverFrame"(ptr %%3) + ret i64 %%4 } +; Function Attrs: null_pointer_is_valid +declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.StartRecoverFrameAlias"(ptr, ptr) #0 + +; Function Attrs: null_pointer_is_valid +declare void @"github.com/goplus/llgo/runtime/internal/runtime.EndRecoverFrame"(ptr) #0 + ; Function Attrs: null_pointer_is_valid declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64) #0 attributes #0 = { null_pointer_is_valid "frame-pointer"="non-leaf" } -`, wrapRef, wrapRef) +`, wrapRef, wrapRef, wrapRef) assertPkg(t, pkg, expected) } @@ -1457,7 +1534,7 @@ func TestClosureWrapHelpers(t *testing.T) { if args := closureWrapArgs(wrap); len(args) != 0 { t.Fatalf("closureWrapArgs should return 0 args, got %d", len(args)) } - closureWrapReturn(b, sig, Expr{}) + closureWrapReturn(b, sig, Expr{}, true) } func TestClosureWrapCache(t *testing.T) { @@ -1896,6 +1973,25 @@ source_filename = "foo/bar" `) } +func TestThreadLocalVar(t *testing.T) { + prog := NewProgram(nil) + pkg := prog.NewPackage("bar", "foo/bar") + a := pkg.NewThreadLocalVar("a", types.NewPointer(types.Typ[types.Int]), InGo) + if got := pkg.NewThreadLocalVar("a", types.NewPointer(types.Typ[types.Int]), InGo); got != a { + t.Fatal("NewThreadLocalVar(a) did not reuse the existing global") + } + a.InitNil() + empty := types.NewStruct(nil, nil) + z := pkg.NewThreadLocalVar("z", types.NewPointer(empty), InGo) + z.InitNil() + assertPkg(t, pkg, `; ModuleID = 'foo/bar' +source_filename = "foo/bar" + +@a = thread_local global i64 0, align 8 +@z = thread_local global {} zeroinitializer, align 1 +`) +} + func TestConst(t *testing.T) { prog := NewProgram(nil) pkg := prog.NewPackage("bar", "foo/bar") @@ -2675,7 +2771,7 @@ func TestRtFuncResolvesLinkname(t *testing.T) { return name }) - if got := pkg.rtFunc("Sigsetjmp").impl.Name(); got != "sigsetjmp" { + if got := pkg.RuntimeFunc("Sigsetjmp").impl.Name(); got != "sigsetjmp" { t.Fatalf("rtFunc linkname = %q, want %q", got, "sigsetjmp") } } diff --git a/ssa/type.go b/ssa/type.go index f90f8de380..89e309194c 100644 --- a/ssa/type.go +++ b/ssa/type.go @@ -194,6 +194,11 @@ func (p Program) SizeOf(typ Type, n ...int64) uint64 { return size } +// AlignOf returns the ABI alignment of typ for the current target. +func (p Program) AlignOf(typ Type) uint64 { + return uint64(p.td.ABITypeAlignment(typ.ll)) +} + // OffsetOf returns the offset of a field in a struct. func (p Program) OffsetOf(typ Type, i int) uint64 { return p.td.ElementOffset(typ.ll, i) diff --git a/test/go/recover_defer_fixedbugs_test.go b/test/go/recover_defer_fixedbugs_test.go new file mode 100644 index 0000000000..95a2453697 --- /dev/null +++ b/test/go/recover_defer_fixedbugs_test.go @@ -0,0 +1,158 @@ +package gotest + +import ( + "runtime" + "strings" + "testing" +) + +type fixedbug4066Panic struct{} + +func fixedbug4066NamedReturn() (val int) { + val = 0 + defer func() { + if x := recover(); x != nil { + _ = x.(fixedbug4066Panic) + } + }() + for { + val = 2 + fixedbug4066Throw() + } +} + +func fixedbug4066Throw() { + panic(fixedbug4066Panic{}) +} + +func TestRecoverFixedbug4066NamedReturn(t *testing.T) { + if got := fixedbug4066NamedReturn(); got != 2 { + t.Fatalf("named return after recover = %d, want 2", got) + } +} + +func TestRecoverFixedbugDirectDeferredFuncValue(t *testing.T) { + recovered := false + func() { + f := func() { + if recover() != nil { + recovered = true + } + } + defer f() + panic("direct deferred func value") + }() + if !recovered { + t.Fatal("direct deferred func value did not recover") + } +} + +func TestRecoverFixedbugNestedDeferInDeferredFuncDoesNotRecover(t *testing.T) { + nested := any("unset") + func() { + defer func() { + if r := recover(); r != "outer" { + t.Fatalf("outer recover = %v, want outer", r) + } + }() + defer func() { + defer func() { + nested = recover() + }() + }() + panic("outer") + }() + if nested != nil { + t.Fatalf("nested recover = %v, want nil", nested) + } +} + +var fixedbugReturnedRecover any + +func fixedbugReturnedRecoverFunc() func() { + return func() { + fixedbugReturnedRecover = recover() + } +} + +func TestRecoverFixedbugReturnedDeferredFuncValue(t *testing.T) { + fixedbugReturnedRecover = nil + func() { + defer fixedbugReturnedRecoverFunc()() + panic("returned deferred func value") + }() + if fixedbugReturnedRecover != "returned deferred func value" { + t.Fatalf("returned deferred recover = %v, want panic value", fixedbugReturnedRecover) + } +} + +var fixedbug73916Recovered bool + +func fixedbug73916CallRecover() { + if recover() != nil { + fixedbug73916Recovered = true + } +} + +func fixedbug73916Deferred(int) { + fixedbug73916CallRecover() +} + +func fixedbug73916MustPanic(t *testing.T, fn func()) any { + t.Helper() + defer func() { + if r := recover(); r == nil { + t.Fatal("deferred indirect recover swallowed panic") + } + }() + fn() + return nil +} + +func TestRecoverFixedbug73916IndirectRecoverDoesNotRecover(t *testing.T) { + skipBeforeGo126(t) + fixedbug73916Recovered = false + fixedbug73916MustPanic(t, func() { + defer fixedbug73916Deferred(1) + panic("fixedbug73916") + }) + if fixedbug73916Recovered { + t.Fatal("indirect recover returned non-nil") + } +} + +var fixedbug73916bRecovered bool + +func fixedbug73916bCallRecover() { + func() { + if recover() != nil { + fixedbug73916bRecovered = true + } + }() +} + +func fixedbug73916bDeferred() int { + fixedbug73916bCallRecover() + return 0 +} + +func TestRecoverFixedbug73916NestedRecoverDoesNotRecover(t *testing.T) { + skipBeforeGo126(t) + fixedbug73916bRecovered = false + fixedbug73916MustPanic(t, func() { + defer fixedbug73916bDeferred() + panic("fixedbug73916b") + }) + if fixedbug73916bRecovered { + t.Fatal("nested recover returned non-nil") + } +} + +func skipBeforeGo126(t *testing.T) { + t.Helper() + version := runtime.Version() + if strings.HasPrefix(version, "go1.26") || strings.HasPrefix(version, "devel") { + return + } + t.Skip("requires Go 1.26 recover semantics") +} diff --git a/test/go/recover_defer_test.go b/test/go/recover_defer_test.go new file mode 100644 index 0000000000..9e011c2f95 --- /dev/null +++ b/test/go/recover_defer_test.go @@ -0,0 +1,289 @@ +package gotest + +import ( + "reflect" + "runtime" + "testing" +) + +func recoverIndirect() any { + return recover() +} + +func recoverRecursive(n int) any { + if n == 0 { + return recoverRecursive(1) + } + return recover() +} + +func TestRecoverOnlyDirectDeferredCall(t *testing.T) { + var indirect, direct, second any + func() { + defer func() { + indirect = recoverIndirect() + direct = recover() + second = recover() + }() + panic("direct-sentinel") + }() + + if indirect != nil { + t.Fatalf("indirect recover = %v, want nil", indirect) + } + if direct != "direct-sentinel" { + t.Fatalf("direct recover = %v, want direct-sentinel", direct) + } + if second != nil { + t.Fatalf("second recover = %v, want nil", second) + } +} + +func TestRecoverRejectsRecursiveIndirectCall(t *testing.T) { + var indirect, direct any + func() { + defer func() { + indirect = recoverRecursive(0) + direct = recover() + }() + panic("recursive-sentinel") + }() + + if indirect != nil { + t.Fatalf("recursive indirect recover = %v, want nil", indirect) + } + if direct != "recursive-sentinel" { + t.Fatalf("direct recover = %v, want recursive-sentinel", direct) + } +} + +func TestNestedPanicRecoverStack(t *testing.T) { + var recovered []any + func() { + defer func() { + recovered = append(recovered, recover()) + }() + defer func() { + defer func() { + recovered = append(recovered, recover()) + }() + panic("inner") + }() + panic("outer") + }() + + want := []any{"inner", "outer"} + if !reflect.DeepEqual(recovered, want) { + t.Fatalf("recover stack = %v, want %v", recovered, want) + } +} + +func TestDeferredRecoverBuiltinKeepsNestedPanicForNextDefer(t *testing.T) { + t.Skip("recursive-panic / method-wrapper recover ownership: recover1.go-class residual on the Defer-node model (#2033 follow-up)") + var recovered []any + func() { + defer func() { + recovered = append(recovered, recover()) + }() + defer func() { + defer func() { + recovered = append(recovered, recover()) + }() + defer recover() + panic("inner") + }() + panic("outer") + }() + + want := []any{"inner", "outer"} + if !reflect.DeepEqual(recovered, want) { + t.Fatalf("recover stack after deferred recover builtin = %v, want %v", recovered, want) + } +} + +func TestDeferredRecoverBuiltinCanRecoverOuterPanicAfterNestedRecover(t *testing.T) { + t.Skip("recursive-panic / method-wrapper recover ownership: recover1.go-class residual on the Defer-node model (#2033 follow-up)") + var recovered []any + func() { + defer func() { + recovered = append(recovered, recover()) + }() + defer func() { + defer recover() + defer func() { + recovered = append(recovered, recover()) + }() + panic("inner") + }() + panic("outer") + }() + + want := []any{"inner", nil} + if !reflect.DeepEqual(recovered, want) { + t.Fatalf("recover stack after outer deferred recover builtin = %v, want %v", recovered, want) + } +} + +func TestRecoverAfterPanicDoesNotKeepPartialResultWrites(t *testing.T) { + if got := recoverAfterResultAssignmentPanic(); got { + t.Fatalf("assignment result = %v, want false", got) + } + if got, _ := recoverAfterReturnExpressionPanic(); got { + t.Fatalf("return expression result = %v, want false", got) + } + if got, _ := recoverAfterNamedReturnExpressionPanic(); got { + t.Fatalf("named return expression result = %v, want false", got) + } +} + +func recoverAfterResultAssignmentPanic() (bad bool) { + defer func() { + recover() + }() + var p *int + bad, _ = true, *p + return +} + +func recoverAfterReturnExpressionPanic() (bool, int) { + defer func() { + recover() + }() + var p *int + return true, *p +} + +func recoverAfterNamedReturnExpressionPanic() (_ bool, _ int) { + defer func() { + recover() + }() + var p *int + return true, *p +} + +type recoverValueMethod uintptr + +var methodWrapperRecovered any + +func (recoverValueMethod) recoverViaValueMethod() { + methodWrapperRecovered = recover() +} + +func TestRecoverThroughDeferredPointerToValueMethodWrapper(t *testing.T) { + t.Skip("recursive-panic / method-wrapper recover ownership: recover1.go-class residual on the Defer-node model (#2033 follow-up)") + methodWrapperRecovered = nil + var x recoverValueMethod + func() { + defer (*recoverValueMethod).recoverViaValueMethod(&x) + panic("method-wrapper-sentinel") + }() + + if methodWrapperRecovered != "method-wrapper-sentinel" { + t.Fatalf("method wrapper recover = %v, want method-wrapper-sentinel", methodWrapperRecovered) + } +} + +func TestRecoverThroughMethodWrapperStillRequiresDirectDeferredCall(t *testing.T) { + methodWrapperRecovered = "unset" + var direct any + var x recoverValueMethod + func() { + defer func() { + (*recoverValueMethod).recoverViaValueMethod(&x) + direct = recover() + }() + panic("outer-sentinel") + }() + + if methodWrapperRecovered != nil { + t.Fatalf("nested method wrapper recover = %v, want nil", methodWrapperRecovered) + } + if direct != "outer-sentinel" { + t.Fatalf("direct recover after nested method wrapper = %v, want outer-sentinel", direct) + } +} + +type embeddedRecoverTarget int + +// Keep issue73917/issue73920 as a real helper call outside the wrapper target. +// +//go:noinline +func recoverForEmbeddedWrapper() { + if r := recover(); r != nil { + methodWrapperRecovered = r + } +} + +func (*embeddedRecoverTarget) recoverViaIndirectCall() { + recoverForEmbeddedWrapper() +} + +type embeddedValueWrapper struct{ *embeddedRecoverTarget } +type embeddedPointerWrapper struct{ *embeddedRecoverTarget } + +func requireGo126RecoverWrapperSemantics(t *testing.T) { + t.Helper() + + const prefix = "go1." + version := runtime.Version() + if len(version) <= len(prefix) || version[:len(prefix)] != prefix { + return + } + + minor := 0 + for _, c := range version[len(prefix):] { + if c < '0' || c > '9' { + break + } + minor = minor*10 + int(c-'0') + } + if minor != 0 && minor < 26 { + t.Skipf("%s has pre-Go 1.26 embedded wrapper recover semantics", version) + } +} + +func TestDeferredEmbeddedValueMethodWrapperKeepsIndirectRecoverNil(t *testing.T) { + requireGo126RecoverWrapperSemantics(t) + + methodWrapperRecovered = nil + var direct any + x := embeddedValueWrapper{new(embeddedRecoverTarget)} + fn := embeddedValueWrapper.recoverViaIndirectCall + func() { + defer func() { + direct = recover() + }() + defer fn(x) + panic("embedded-value-wrapper-sentinel") + }() + + if methodWrapperRecovered != nil { + t.Fatalf("indirect recover through embedded value wrapper = %v, want nil", methodWrapperRecovered) + } + if direct != "embedded-value-wrapper-sentinel" { + t.Fatalf("direct recover after embedded value wrapper = %v, want embedded-value-wrapper-sentinel", direct) + } +} + +func TestDeferredEmbeddedPointerMethodWrapperKeepsIndirectRecoverNil(t *testing.T) { + requireGo126RecoverWrapperSemantics(t) + + methodWrapperRecovered = nil + var direct any + x := &embeddedPointerWrapper{new(embeddedRecoverTarget)} + fn := (*embeddedPointerWrapper).recoverViaIndirectCall + func() { + defer func() { + direct = recover() + }() + defer fn(x) + panic("embedded-pointer-wrapper-sentinel") + }() + + if methodWrapperRecovered != nil { + t.Fatalf("indirect recover through embedded pointer wrapper = %v, want nil", methodWrapperRecovered) + } + if direct != "embedded-pointer-wrapper-sentinel" { + t.Fatalf("direct recover after embedded pointer wrapper = %v, want embedded-pointer-wrapper-sentinel", direct) + } +} diff --git a/test/go/recover_fault_unix_test.go b/test/go/recover_fault_unix_test.go new file mode 100644 index 0000000000..96ecaa2a91 --- /dev/null +++ b/test/go/recover_fault_unix_test.go @@ -0,0 +1,49 @@ +//go:build linux || darwin + +package gotest + +import ( + "runtime/debug" + "syscall" + "testing" +) + +func faultCopy(dst, src []byte) (n int, err error) { + defer func() { + if r, ok := recover().(error); ok { + err = r + } + }() + + for i := 0; i < len(dst) && i < len(src); i++ { + dst[i] = src[i] + n++ + } + return +} + +func TestRecoverAfterFaultPreservesNamedResult(t *testing.T) { + old := debug.SetPanicOnFault(true) + defer debug.SetPanicOnFault(old) + + size := syscall.Getpagesize() + data, err := syscall.Mmap(-1, 0, 16*size, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_ANON|syscall.MAP_PRIVATE) + if err != nil { + t.Fatalf("mmap: %v", err) + } + defer syscall.Munmap(data) + + hole := data[len(data)/2 : 3*(len(data)/4)] + if err := syscall.Mprotect(hole, syscall.PROT_NONE); err != nil { + t.Fatalf("mprotect: %v", err) + } + + const offset = 5 + n, err := faultCopy(data[offset:], make([]byte, len(data))) + if err == nil { + t.Fatal("no error from copy across memory hole") + } + if want := len(data)/2 - offset; n != want { + t.Fatalf("copy returned %d, want %d", n, want) + } +} diff --git a/test/go/runtime_error_recover_test.go b/test/go/runtime_error_recover_test.go index 546deee367..51985b844a 100644 --- a/test/go/runtime_error_recover_test.go +++ b/test/go/runtime_error_recover_test.go @@ -6,116 +6,157 @@ import ( "testing" ) +type runtimeErrorMissingMethod interface { + runtimeErrorMissingMethod() +} + var ( + runtimeErrorSink any runtimeErrorIntSink int - runtimeErrorAnySink any runtimeErrorArrayPtr *[10]int runtimeErrorBigArrayPtr *[10000]int ) -func TestRecoverRuntimeErrorClassification(t *testing.T) { - var zero int - var zero64 int64 +func TestRecoveredRuntimePanicsAreErrors(t *testing.T) { var index = 99999 arrayPtr := new([10]int) - var slice []int - var iface any = 1 tests := []struct { name string - want string + want []string f func() }{ { - name: "int-div-zero", - want: "integer divide by zero", + name: "index", + want: []string{"runtime error:", "index out of range"}, f: func() { - runtimeErrorIntSink = 1 / zero + s := []byte{1} + i := 2 + runtimeErrorSink = s[i] }, }, { - name: "int64-div-zero", - want: "integer divide by zero", + name: "array bounds", + want: []string{"runtime error:", "index out of range"}, f: func() { - runtimeErrorIntSink = int(1 / zero64) + runtimeErrorIntSink = arrayPtr[index] }, }, { - name: "nil-array-pointer-index-zero", - want: "nil pointer dereference", + name: "slice", + want: []string{"runtime error:", "slice bounds out of range"}, f: func() { - runtimeErrorIntSink = runtimeErrorArrayPtr[0] + s := []byte{1} + hi := 2 + runtimeErrorSink = s[:hi] }, }, { - name: "nil-array-pointer-index-one", - want: "nil pointer dereference", + name: "divide", + want: []string{"runtime error:", "integer divide by zero"}, f: func() { - runtimeErrorIntSink = runtimeErrorArrayPtr[1] + z := 0 + runtimeErrorSink = 1 / z }, }, { - name: "nil-array-pointer-index-large", - want: "nil pointer dereference", + name: "nil dereference", + want: []string{"runtime error:", "nil pointer dereference"}, f: func() { - runtimeErrorIntSink = runtimeErrorBigArrayPtr[5000] + var p *int + runtimeErrorSink = *p }, }, { - name: "array-bounds", - want: "index out of range", + name: "nil array pointer index zero", + want: []string{"runtime error:", "nil pointer dereference"}, f: func() { - runtimeErrorIntSink = arrayPtr[index] + runtimeErrorIntSink = runtimeErrorArrayPtr[0] }, }, { - name: "slice-bounds", - want: "index out of range", + name: "nil array pointer index one", + want: []string{"runtime error:", "nil pointer dereference"}, f: func() { - runtimeErrorIntSink = slice[index] + runtimeErrorIntSink = runtimeErrorArrayPtr[1] }, }, { - name: "type-concrete", - want: "int, not string", + name: "nil array pointer index large", + want: []string{"runtime error:", "nil pointer dereference"}, f: func() { - runtimeErrorAnySink = iface.(string) + runtimeErrorIntSink = runtimeErrorBigArrayPtr[5000] }, }, { - name: "type-interface", - want: "missing method runtimeErrorMissingMethod", + name: "slice to array", + want: []string{"runtime error:", "cannot convert slice with length 1 to array or pointer to array with length 2"}, f: func() { - runtimeErrorAnySink = iface.(runtimeErrorMissingMethod) + s := []byte{1} + runtimeErrorSink = [2]byte(s) }, }, } - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - expectRecoverRuntimeError(t, tt.want, tt.f) + err := recoverRuntimeErrorValue(t, tt.f) + assertRuntimeErrorContains(t, err, tt.want...) }) } } -func expectRecoverRuntimeError(t *testing.T, want string, f func()) { +func TestRecoveredTypeAssertionPanicsAreRuntimeErrors(t *testing.T) { + t.Run("concrete", func(t *testing.T) { + var v any = 1 + err := recoverRuntimeErrorValue(t, func() { + runtimeErrorSink = v.(string) + }) + assertRuntimeErrorContains(t, err, "interface conversion", "int", "not string") + }) + + t.Run("nil interface", func(t *testing.T) { + var v any + err := recoverRuntimeErrorValue(t, func() { + runtimeErrorSink = v.(string) + }) + assertRuntimeErrorContains(t, err, "interface conversion", "is nil", "not string") + }) + + t.Run("missing method", func(t *testing.T) { + var v any = 1 + err := recoverRuntimeErrorValue(t, func() { + runtimeErrorSink = v.(runtimeErrorMissingMethod) + }) + assertRuntimeErrorContains(t, err, "interface conversion", "int is not", "missing method runtimeErrorMissingMethod") + }) +} + +func recoverRuntimeErrorValue(t *testing.T, f func()) runtime.Error { t.Helper() - defer func() { - err := recover() - if err == nil { - t.Fatalf("expected runtime panic containing %q", want) - } - runtimeErr, ok := err.(runtime.Error) - if !ok { - t.Fatalf("panic type = %T, want runtime.Error", err) - } - if got := runtimeErr.Error(); !strings.Contains(got, want) { - t.Fatalf("panic = %q, want contains %q", got, want) - } + var rec any + func() { + defer func() { + rec = recover() + }() + f() }() - f() + if rec == nil { + t.Fatal("expected panic") + } + err := rec.(error) + rerr := rec.(runtime.Error) + if err.Error() != rerr.Error() { + t.Fatalf("error text mismatch: error=%q runtime.Error=%q", err.Error(), rerr.Error()) + } + return rerr } -type runtimeErrorMissingMethod interface { - runtimeErrorMissingMethod() +func assertRuntimeErrorContains(t *testing.T, err error, wants ...string) { + t.Helper() + got := err.Error() + for _, want := range wants { + if !strings.Contains(got, want) { + t.Fatalf("panic = %q, want contains %q", got, want) + } + } } diff --git a/test/go/runtime_g_state_test.go b/test/go/runtime_g_state_test.go new file mode 100644 index 0000000000..8d808ac5df --- /dev/null +++ b/test/go/runtime_g_state_test.go @@ -0,0 +1,80 @@ +package gotest + +import ( + "runtime" + "testing" +) + +type runtimeGStateResult struct { + id int + recovered any + order string +} + +func runtimeGStatePanic(id int, ready chan<- struct{}, start <-chan struct{}, results chan<- runtimeGStateResult) { + order := "" + defer func() { + result := runtimeGStateResult{id: id, recovered: recover(), order: order + "r"} + results <- result + }() + defer func() { + order += "d" + }() + ready <- struct{}{} + <-start + order += "p" + panic(id) +} + +func TestRuntimeGStateIsolation(t *testing.T) { + ready := make(chan struct{}, 2) + start := make(chan struct{}) + results := make(chan runtimeGStateResult, 2) + for id := 1; id <= 2; id++ { + go runtimeGStatePanic(id, ready, start, results) + } + <-ready + <-ready + close(start) + + seen := [3]bool{} + for i := 0; i < 2; i++ { + result := <-results + if result.id < 1 || result.id > 2 { + t.Fatalf("unexpected goroutine id %d", result.id) + } + if seen[result.id] { + t.Fatalf("duplicate result from goroutine %d", result.id) + } + seen[result.id] = true + if result.recovered != result.id { + t.Fatalf("goroutine %d recovered %v", result.id, result.recovered) + } + if result.order != "pdr" { + t.Fatalf("goroutine %d defer order = %q, want %q", result.id, result.order, "pdr") + } + } + + goexitDone := make(chan any, 1) + go func() { + defer func() { + goexitDone <- recover() + }() + runtime.Goexit() + goexitDone <- "Goexit returned" + }() + if recovered := <-goexitDone; recovered != nil { + t.Fatalf("recover during Goexit = %v, want nil", recovered) + } + + var recovered any + func() { + defer func() { + recovered = recover() + }() + panic("main panic") + }() + if recovered != "main panic" { + t.Fatalf("main goroutine recovered %v, want %q", recovered, "main panic") + } +} diff --git a/test/go/runtime_lineinfo_stack_test.go b/test/go/runtime_lineinfo_stack_test.go index 52fffcabef..673276bcd7 100644 --- a/test/go/runtime_lineinfo_stack_test.go +++ b/test/go/runtime_lineinfo_stack_test.go @@ -224,6 +224,7 @@ func TestRuntimeLineInfoAndStack(t *testing.T) { const runtimeFuncInfoConcurrentFirstUseProbe = `package main import ( + "reflect" "runtime" "strconv" "strings" @@ -232,16 +233,33 @@ import ( func main() { const n = 32 + const rounds = 1000 start := make(chan struct{}) errc := make(chan string, n) var wg sync.WaitGroup for i := 0; i < n; i++ { + target := concurrentTargets[i%len(concurrentTargets)] + pc := reflect.ValueOf(target.fn).Pointer() wg.Add(1) - go func() { + go func(target concurrentTarget, pc uintptr) { defer wg.Done() <-start - errc <- checkRuntimeInfo() - }() + for j := 0; j < rounds; j++ { + fn := runtime.FuncForPC(pc) + if fn == nil || fn.Name() != target.name { + name := "" + if fn != nil { + name = fn.Name() + } + errc <- "bad target func: " + name + return + } + if err := checkRuntimeInfo(); err != "" { + errc <- err + return + } + } + }(target, pc) } close(start) wg.Wait() @@ -253,6 +271,30 @@ func main() { } } +type concurrentTarget struct { + fn func() + name string +} + +var concurrentTargets = []concurrentTarget{ + {concurrentTarget0, "main.concurrentTarget0"}, + {concurrentTarget1, "main.concurrentTarget1"}, + {concurrentTarget2, "main.concurrentTarget2"}, + {concurrentTarget3, "main.concurrentTarget3"}, +} + +//go:noinline +func concurrentTarget0() {} + +//go:noinline +func concurrentTarget1() {} + +//go:noinline +func concurrentTarget2() {} + +//go:noinline +func concurrentTarget3() {} + //go:noinline func checkRuntimeInfo() string { pc, file, line, ok := runtime.Caller(0) // CONCURRENT_CALLER_MARK diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index ae5bb230cb..1bdd4d8e51 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -579,12 +579,6 @@ timeouts: case: fixedbugs/issue40367.go timeout: 2m reason: issue40367 run exceeds the default timeout on darwin/arm64 - - version: go1.24 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4066.go - timeout: 2m - reason: issue4066 run exceeds the default timeout on darwin/arm64 - version: go1.24 platform: darwin/arm64 directive: run @@ -951,12 +945,6 @@ timeouts: case: fixedbugs/issue40367.go timeout: 2m reason: issue40367 run exceeds the default timeout on darwin/arm64 - - version: go1.25 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4066.go - timeout: 2m - reason: issue4066 run exceeds the default timeout on darwin/arm64 - version: go1.25 platform: darwin/arm64 directive: run @@ -1323,12 +1311,6 @@ timeouts: case: fixedbugs/issue40367.go timeout: 2m reason: issue40367 run exceeds the default timeout on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4066.go - timeout: 2m - reason: issue4066 run exceeds the default timeout on darwin/arm64 - version: go1.26 platform: darwin/arm64 directive: run @@ -2471,30 +2453,10 @@ xfails: directive: run case: noinit.go reason: current main goroot run failure on darwin/arm64 - - version: go1.24 - platform: darwin/arm64 - directive: run - case: recover2.go - reason: current main goroot run failure on darwin/arm64 - - version: go1.25 - platform: darwin/arm64 - directive: run - case: recover2.go - reason: current main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: runoutput case: rangegen.go reason: current main goroot runoutput failure on darwin/arm64 - - version: go1.24 - platform: darwin/arm64 - directive: run - case: zerodivide.go - reason: current main goroot run failure on darwin/arm64 - - version: go1.25 - platform: darwin/arm64 - directive: run - case: zerodivide.go - reason: current main goroot run failure on darwin/arm64 - version: go1.24 platform: darwin/arm64 directive: run @@ -2505,6 +2467,10 @@ xfails: directive: run case: fixedbugs/issue16130.go reason: current main goroot run failure on darwin/arm64 + - platform: darwin/arm64 + directive: run + case: recover1.go + reason: recover during recursive panics - a plain sub-call inside the deferred function still sees the panic (sub-call recover masking, follow-up on the Defer-node model) on darwin/arm64 - version: go1.24 platform: darwin/arm64 directive: run @@ -2525,16 +2491,6 @@ xfails: directive: run case: fixedbugs/issue37975.go reason: current main goroot run failure on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue73916.go - reason: go1.26 goroot run failure on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue73916b.go - reason: go1.26 goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: typeparam/mdempsky/16.go @@ -2554,11 +2510,6 @@ xfails: directive: run case: noinit.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: recover2.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -2572,7 +2523,7 @@ xfails: - version: go1.24 platform: linux/amd64 directive: run - case: zerodivide.go + case: fixedbugs/issue16130.go reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 @@ -2589,6 +2540,21 @@ xfails: directive: run case: typeparam/mdempsky/16.go reason: nil-interface assertion panic implements runtime.Error but the message lacks the source interface type and prints the command-line-arguments package path on linux/amd64 + - version: go1.24 + platform: linux/amd64 + directive: run + case: recover1.go + reason: recover during recursive panics - a plain sub-call inside the deferred function still sees the panic (sub-call recover masking, follow-up on the Defer-node model) on linux/amd64 + - version: go1.25 + platform: linux/amd64 + directive: run + case: recover1.go + reason: recover during recursive panics - a plain sub-call inside the deferred function still sees the panic (sub-call recover masking, follow-up on the Defer-node model) on linux/amd64 + - version: go1.26 + platform: linux/amd64 + directive: run + case: recover1.go + reason: recover during recursive panics - a plain sub-call inside the deferred function still sees the panic (sub-call recover masking, follow-up on the Defer-node model) on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -2624,11 +2590,6 @@ xfails: directive: run case: noinit.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: recover2.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -2642,7 +2603,7 @@ xfails: - version: go1.25 platform: linux/amd64 directive: run - case: zerodivide.go + case: fixedbugs/issue16130.go reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 @@ -2684,11 +2645,6 @@ xfails: directive: run case: noinit.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: recover2.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -2702,7 +2658,7 @@ xfails: - version: go1.26 platform: linux/amd64 directive: run - case: zerodivide.go + case: fixedbugs/issue16130.go reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 @@ -2729,16 +2685,6 @@ xfails: directive: run case: fixedbugs/issue72844.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue73916.go - reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue73916b.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -2796,14 +2742,6 @@ xfails: directive: run case: recover.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: recover1.go - reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: recover4.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: stackobj.go @@ -2961,16 +2899,6 @@ xfails: directive: run case: fixedbugs/issue33724.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: fixedbugs/issue4066.go - reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4066.go - reason: go1.25 goroot run failure on darwin/arm64 - version: go1.25 platform: linux/amd64 directive: run @@ -3031,16 +2959,6 @@ xfails: directive: run case: recover.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: recover1.go - reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: recover4.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -3132,11 +3050,6 @@ xfails: directive: run case: mallocfin.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: recover1.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3182,16 +3095,6 @@ xfails: directive: run case: fixedbugs/issue33724.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue4066.go - reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4066.go - reason: go1.24 goroot run failure on darwin/arm64 - version: go1.24 platform: linux/amd64 directive: run @@ -3202,11 +3105,6 @@ xfails: directive: run case: fixedbugs/issue4562.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: recover4.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3242,16 +3140,6 @@ xfails: directive: run case: mallocfin.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: recover1.go - reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: recover4.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -3297,16 +3185,6 @@ xfails: directive: run case: fixedbugs/issue33724.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue4066.go - reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4066.go - reason: go1.26 goroot run failure on darwin/arm64 - version: go1.26 platform: linux/amd64 directive: run @@ -3459,16 +3337,6 @@ xfails: directive: run case: fixedbugs/issue38496.go reason: current main goroot run failure on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue73917.go - reason: go1.26 goroot run failure on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue73920.go - reason: go1.26 goroot run failure on darwin/arm64 - version: go1.24 platform: darwin/arm64 directive: run @@ -3539,16 +3407,6 @@ xfails: directive: run case: fixedbugs/issue47928.go reason: current main goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue73917.go - reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue73920.go - reason: go1.26 goroot run failure on linux/amd64 - platform: linux/amd64 directive: run case: fixedbugs/issue8048.go diff --git a/test/llgoext/locality_test.go b/test/llgoext/locality_test.go new file mode 100644 index 0000000000..3f839419ba --- /dev/null +++ b/test/llgoext/locality_test.go @@ -0,0 +1,621 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package llgoext + +import ( + "runtime" + "sync/atomic" + "testing" + + "github.com/goplus/llgo/test/llgoext/testdata/localitybench" + "github.com/goplus/llgo/test/llgoext/testdata/localityfailure" + "github.com/goplus/llgo/test/llgoext/testdata/localityscope" +) + +var initializerSequence int + +func nextLocalValue(base int) int { + initializerSequence++ + return base + initializerSequence +} + +//llgo:tls +var tlsCounter int + +//llgo:gls +var glsCounter int + +//llgo:tls +var initializedTLS = nextLocalValue(100) + +//llgo:gls +var initializedGLS = nextLocalValue(200) + +type localitySnapshot struct { + tlsCounter int + glsCounter int + initializedTLS int + initializedGLS int +} + +func snapshotLocality() localitySnapshot { + return localitySnapshot{ + tlsCounter: tlsCounter, + glsCounter: glsCounter, + initializedTLS: initializedTLS, + initializedGLS: initializedGLS, + } +} + +func TestTLSAndGLSIsolation(t *testing.T) { + parentInitial := snapshotLocality() + if parentInitial.initializedTLS <= 100 || parentInitial.initializedGLS <= 200 { + t.Fatalf("parent initializers did not run: %+v", parentInitial) + } + + tlsCounter = 11 + glsCounter = 22 + parentSet := snapshotLocality() + + type childResult struct { + first localitySnapshot + again localitySnapshot + set localitySnapshot + } + done := make(chan childResult) + go func() { + first := snapshotLocality() + again := snapshotLocality() + tlsCounter = 31 + glsCounter = 32 + done <- childResult{first: first, again: again, set: snapshotLocality()} + }() + child := <-done + + if child.first.tlsCounter != 0 || child.first.glsCounter != 0 { + t.Fatalf("child inherited parent local values: %+v", child.first) + } + if child.first != child.again { + t.Fatalf("child initializer ran more than once: first=%+v again=%+v", child.first, child.again) + } + if child.first.initializedTLS == parentInitial.initializedTLS || child.first.initializedGLS == parentInitial.initializedGLS { + t.Fatalf("child reused parent initialization: parent=%+v child=%+v", parentInitial, child.first) + } + if child.set.tlsCounter != 31 || child.set.glsCounter != 32 { + t.Fatalf("child local writes were lost: %+v", child.set) + } + if got := snapshotLocality(); got != parentSet { + t.Fatalf("child changed parent local values: got=%+v want=%+v", got, parentSet) + } +} + +func TestLocalPackageDirectCaches(t *testing.T) { + type result struct { + local int + imported int + } + done := make(chan result) + go func() { + glsCounter = 41 + localityscope.SetFirst(51) + glsCounter++ + imported := localityscope.IncrementFirst() + done <- result{local: glsCounter, imported: imported} + }() + if got := <-done; got != (result{local: 42, imported: 52}) { + t.Fatalf("local package values through direct caches = %+v", got) + } +} + +func TestPanickingInitializerIsSticky(t *testing.T) { + for attempt := 0; attempt < 2; attempt++ { + var recovered any + func() { + defer func() { recovered = recover() }() + _ = localityfailure.Value() + }() + if recovered == nil { + t.Fatalf("attempt %d did not re-panic", attempt) + } + if recovered != localityfailure.Failure { + t.Fatalf("attempt %d panic = %v, want %v", attempt, recovered, localityfailure.Failure) + } + } + if attempts := localityfailure.Attempts(); attempts != 2 { + t.Fatalf("initializer attempts = %d, want 2", attempts) + } +} + +func TestNilPanickingInitializerIsSticky(t *testing.T) { + for attempt := 0; attempt < 2; attempt++ { + var recovered any + func() { + defer func() { recovered = recover() }() + _ = localityfailure.NilValue() + }() + if recovered == nil { + t.Fatalf("attempt %d did not re-panic", attempt) + } + } + if attempts := localityfailure.NilAttempts(); attempts != 2 { + t.Fatalf("initializer attempts = %d, want 2", attempts) + } +} + +type recursiveInitializer interface { + value() int +} + +type recursiveSourceValue struct{} + +func (recursiveSourceValue) value() int { + return recursiveValue + 1 +} + +var recursiveSource recursiveInitializer = recursiveSourceValue{} + +//llgo:gls +var recursiveValue = recursiveSource.value() + +func TestRecursiveInitializerObservesPartialValue(t *testing.T) { + if recursiveValue != 1 { + t.Fatalf("recursive initializer value = %d, want 1", recursiveValue) + } + if recursiveValue != 1 { + t.Fatal("recursive initializer ran more than once") + } +} + +var lateInitializerAttempts int + +func nextLateValue() int { + lateInitializerAttempts++ + return 100 + lateInitializerAttempts +} + +// zLate sorts after the package init function in SSA member order. +// +//llgo:tls +var zLate = nextLateValue() + +func TestLateSortedInitializer(t *testing.T) { + value, attempts := zLate, lateInitializerAttempts + if value != 100+attempts { + t.Fatalf("late initializer value = %d, attempts = %d", value, attempts) + } + if again := zLate; again != value || lateInitializerAttempts != attempts { + t.Fatalf("late initializer repeated: value=%d/%d attempts=%d/%d", again, value, lateInitializerAttempts, attempts) + } +} + +type rootedValue struct { + value int + pad [256]byte +} + +func newRootedValue() *rootedValue { + return &rootedValue{value: 73} +} + +//llgo:gls +var rootedPointer = newRootedValue() + +//go:noinline +func touchRootedPointer() { + if rootedPointer == nil || rootedPointer.value != 73 { + panic("invalid rooted pointer") + } +} + +//go:noinline +func collectWithoutLocalPointer() { + for i := 0; i < 3; i++ { + _ = make([]byte, 1<<20) + runtime.GC() + } +} + +//go:noinline +func readRootedPointer() int { + return rootedPointer.value +} + +func TestLocalPointerIsGCRoot(t *testing.T) { + touchRootedPointer() + collectWithoutLocalPointer() + if got := readRootedPointer(); got != 73 { + t.Fatalf("rooted pointer value = %d, want 73", got) + } +} + +func TestLocalContextCleanupAfterThreadExit(t *testing.T) { + for i := 0; i < 16; i++ { + exited := make(chan struct{}) + go func() { + defer close(exited) + touchRootedPointer() + }() + <-exited + } + runtime.GC() + runtime.GC() +} + +//llgo:gls +var atomicGLS int64 + +func TestLocalAddressAndAtomicSemantics(t *testing.T) { + first := &atomicGLS + second := &atomicGLS + if first != second { + t.Fatalf("repeated GLS address changed: %p != %p", first, second) + } + atomic.StoreInt64(first, 7) + if got := atomic.AddInt64(&atomicGLS, 5); got != 12 { + t.Fatalf("atomic GLS value = %d, want 12", got) + } +} + +func localClosure() func() int { + glsCounter = 40 + return func() int { + glsCounter++ + return glsCounter + } +} + +func TestClosureUsesInvocationContext(t *testing.T) { + closure := localClosure() + done := make(chan int) + go func() { + done <- closure() + }() + if got := <-done; got != 1 { + t.Fatalf("closure used creator GLS value: got %d, want 1", got) + } + if glsCounter != 40 { + t.Fatalf("child closure changed parent GLS value: %d", glsCounter) + } +} + +type escapedBlockValue struct { + pointer *int + value int +} + +//llgo:gls +var escapedBlock escapedBlockValue + +func TestEscapedPackageBlockAddressSurvivesOwnerExit(t *testing.T) { + addresses := make(chan *escapedBlockValue) + exited := make(chan struct{}) + go func() { + value := 71 + escapedBlock = escapedBlockValue{pointer: &value, value: 71} + addresses <- &escapedBlock + close(exited) + }() + address := <-addresses + <-exited + runtime.GC() + runtime.GC() + if address.value != 71 || address.pointer == nil || *address.pointer != 71 { + t.Fatalf("escaped package block after owner exit = %+v", address) + } + done := make(chan bool) + go func(block *escapedBlockValue) { + block.value = 72 + *block.pointer = 72 + done <- true + }(address) + <-done + if address.value != 72 || *address.pointer != 72 { + t.Fatalf("escaped package block after cross-goroutine write = %+v", address) + } +} + +func TestEscapedPackageBlockAddressSurvivesGoexit(t *testing.T) { + addresses := make(chan *escapedBlockValue) + exited := make(chan struct{}) + go func() { + value := 81 + escapedBlock = escapedBlockValue{pointer: &value, value: 81} + address := &escapedBlock + defer close(exited) + defer func() { + addresses <- address + }() + runtime.Goexit() + }() + address := <-addresses + <-exited + runtime.GC() + if address.value != 81 || address.pointer == nil || *address.pointer != 81 { + t.Fatalf("escaped package block after Goexit = %+v", address) + } +} + +//llgo:gls +var zeroSizedGLS struct{} + +func TestZeroSizedNativeLocalAddressIsStable(t *testing.T) { + first := &zeroSizedGLS + second := &zeroSizedGLS + if first == nil || first != second { + t.Fatalf("zero-sized GLS address changed: %p != %p", first, second) + } +} + +func TestInitializerScopeRunsOncePerPackageKind(t *testing.T) { + firstBefore := localityscope.FirstCalls() + secondBefore := localityscope.SecondCalls() + type result struct { + firstValue int + firstCalls int + secondCalls int + firstCallsAgain int + secondValue int + secondCallsAfter int + } + done := make(chan result) + go func() { + firstValue := localityscope.First() + firstCalls := localityscope.FirstCalls() + secondCalls := localityscope.SecondCalls() + _ = localityscope.First() + firstCallsAgain := localityscope.FirstCalls() + secondValue := localityscope.Second() + done <- result{ + firstValue: firstValue, + firstCalls: firstCalls, + secondCalls: secondCalls, + firstCallsAgain: firstCallsAgain, + secondValue: secondValue, + secondCallsAfter: localityscope.SecondCalls(), + } + }() + got := <-done + if got.firstValue == 0 || got.secondValue == 0 { + t.Fatalf("lazy initializer values = %+v", got) + } + if got.firstCalls != firstBefore+1 || got.firstCallsAgain != got.firstCalls { + t.Fatalf("first initializer calls = %+v, baseline %d", got, firstBefore) + } + if got.secondCalls != secondBefore+1 || got.secondCallsAfter != got.secondCalls { + t.Fatalf("package GLS initializers did not run together once: %+v, baseline %d", got, secondBefore) + } +} + +func TestMultiValueInitializerUsesOneGroup(t *testing.T) { + before := localityscope.PairCalls() + type result struct { + first, second int + afterFirst int + afterSecond int + } + done := make(chan result) + go func() { + first := localityscope.PairFirst() + afterFirst := localityscope.PairCalls() + second := localityscope.PairSecond() + done <- result{first, second, afterFirst, localityscope.PairCalls()} + }() + got := <-done + if got.first == 0 || got.second == 0 || got.afterFirst != before+1 || got.afterSecond != got.afterFirst { + t.Fatalf("multi-value initializer group = %+v, baseline %d", got, before) + } +} + +func TestCrossPackageMixedInitializerGroup(t *testing.T) { + before := localityscope.MixedCalls() + type result struct { + scalar int + pointer *int + addressStable bool + calls int + } + done := make(chan result) + go func() { + scalar := localityscope.MixedScalar() + address := localityscope.MixedScalarAddress() + done <- result{ + scalar: scalar, + pointer: localityscope.MixedPointer(), + addressStable: address == localityscope.MixedScalarAddress(), + calls: localityscope.MixedCalls(), + } + }() + got := <-done + if got.scalar == 0 || got.pointer == nil || !got.addressStable || got.calls != before+1 { + t.Fatalf("cross-package mixed initializer = %+v, baseline %d", got, before) + } +} + +var benchmarkOrdinary int + +//llgo:tls +var benchmarkTLS int + +//llgo:gls +var benchmarkGLS int + +var benchmarkSink int +var benchmarkReadSink uintptr + +//go:noinline +func bumpOrdinaryGlobal() int { + benchmarkOrdinary++ + return benchmarkOrdinary +} + +//go:noinline +func bumpNativeTLS() int { + benchmarkTLS++ + return benchmarkTLS +} + +//go:noinline +func bumpNativeGLS() int { + benchmarkGLS++ + return benchmarkGLS +} + +type benchmarkPackageValue struct { + pointer *int + value int +} + +//llgo:tls +var benchmarkTLSPackage benchmarkPackageValue + +//llgo:gls +var benchmarkGLSPackage benchmarkPackageValue + +//go:noinline +func bumpTLSPackageBlock() int { + benchmarkTLSPackage.value++ + return benchmarkTLSPackage.value +} + +//go:noinline +func bumpGLSPackageBlock() int { + benchmarkGLSPackage.value++ + return benchmarkGLSPackage.value +} + +//go:noinline +func readGLSPackageBlock() uintptr { + return uintptr(benchmarkGLSPackage.value) +} + +func BenchmarkOrdinaryGlobal(b *testing.B) { + value := 0 + for i := 0; i < b.N; i++ { + value += bumpOrdinaryGlobal() + } + benchmarkSink = value +} + +func BenchmarkNativeTLS(b *testing.B) { + value := 0 + for i := 0; i < b.N; i++ { + value += bumpNativeTLS() + } + benchmarkSink = value +} + +func BenchmarkNativeGLS(b *testing.B) { + value := 0 + for i := 0; i < b.N; i++ { + value += bumpNativeGLS() + } + benchmarkSink = value +} + +func BenchmarkTLSPackageBlock(b *testing.B) { + benchmarkTLSPackage.pointer = &benchmarkSink + b.ResetTimer() + value := 0 + for i := 0; i < b.N; i++ { + value += bumpTLSPackageBlock() + } + benchmarkSink = value +} + +func BenchmarkGLSPackageBlock(b *testing.B) { + benchmarkGLSPackage.pointer = &benchmarkSink + b.ResetTimer() + value := 0 + for i := 0; i < b.N; i++ { + value += bumpGLSPackageBlock() + } + benchmarkSink = value +} + +func TestComparableLocalityReads(t *testing.T) { + localitybench.PrepareReads() + ordinary := localitybench.ReadOrdinaryGlobal() + native := localitybench.ReadNativeTLS() + local := localitybench.ReadGLSPackage() + if ordinary == 0 || native != ordinary || local != ordinary { + t.Fatalf("comparable locality reads = ordinary:%#x native:%#x GLS:%#x", ordinary, native, local) + } +} + +func BenchmarkComparableOrdinaryGlobalRead(b *testing.B) { + localitybench.PrepareReads() + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += localitybench.ReadOrdinaryGlobal() + } + benchmarkReadSink = value +} + +func BenchmarkComparableNativeTLSRead(b *testing.B) { + localitybench.PrepareReads() + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += localitybench.ReadNativeTLS() + } + benchmarkReadSink = value +} + +func BenchmarkComparableGLSPackageRead(b *testing.B) { + localitybench.PrepareReads() + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += localitybench.ReadGLSPackage() + } + benchmarkReadSink = value +} + +func BenchmarkAlternatingGLSPackageRead(b *testing.B) { + localitybench.PrepareReads() + benchmarkGLSPackage.pointer = &benchmarkSink + benchmarkGLSPackage.value = 1 + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += readGLSPackageBlock() + value += localitybench.ReadGLSPackage() + } + benchmarkReadSink = value +} + +func BenchmarkGoroutineEntry(b *testing.B) { + for i := 0; i < b.N; i++ { + done := make(chan struct{}) + go func() { close(done) }() + <-done + } +} + +func BenchmarkGoroutinePackageBlockFirstTouch(b *testing.B) { + for i := 0; i < b.N; i++ { + done := make(chan struct{}) + go func() { + localitybench.Touch() + close(done) + }() + <-done + } +} diff --git a/test/llgoext/localitymulti/locality_test.go b/test/llgoext/localitymulti/locality_test.go new file mode 100644 index 0000000000..de1d8afe3e --- /dev/null +++ b/test/llgoext/localitymulti/locality_test.go @@ -0,0 +1,115 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package localitymulti + +import ( + "testing" + + localityblock0 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p0" + localityblock1 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p1" + localityblock2 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p2" + localityblock3 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p3" + localityblock4 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p4" + localityblock5 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p5" + localityblock6 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p6" + localityblock7 "github.com/goplus/llgo/test/llgoext/testdata/localityblocks/p7" +) + +var benchmarkSink uintptr + +func TestGLSPackageWorkingSet(t *testing.T) { + prepare := []func(){ + localityblock0.Prepare, + localityblock1.Prepare, + localityblock2.Prepare, + localityblock3.Prepare, + localityblock4.Prepare, + localityblock5.Prepare, + localityblock6.Prepare, + localityblock7.Prepare, + } + read := []func() uintptr{ + localityblock0.Read, + localityblock1.Read, + localityblock2.Read, + localityblock3.Read, + localityblock4.Read, + localityblock5.Read, + localityblock6.Read, + localityblock7.Read, + } + for i := range prepare { + prepare[i]() + if got := read[i](); got == 0 { + t.Fatalf("package %d GLS pointer is nil", i) + } + } +} + +func BenchmarkGLSPackageWorkingSet2(b *testing.B) { + localityblock0.Prepare() + localityblock1.Prepare() + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += localityblock0.Read() + value += localityblock1.Read() + } + benchmarkSink = value +} + +func BenchmarkGLSPackageWorkingSet4(b *testing.B) { + localityblock0.Prepare() + localityblock1.Prepare() + localityblock2.Prepare() + localityblock3.Prepare() + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += localityblock0.Read() + value += localityblock1.Read() + value += localityblock2.Read() + value += localityblock3.Read() + } + benchmarkSink = value +} + +func BenchmarkGLSPackageWorkingSet8(b *testing.B) { + localityblock0.Prepare() + localityblock1.Prepare() + localityblock2.Prepare() + localityblock3.Prepare() + localityblock4.Prepare() + localityblock5.Prepare() + localityblock6.Prepare() + localityblock7.Prepare() + b.ResetTimer() + var value uintptr + for i := 0; i < b.N; i++ { + value += localityblock0.Read() + value += localityblock1.Read() + value += localityblock2.Read() + value += localityblock3.Read() + value += localityblock4.Read() + value += localityblock5.Read() + value += localityblock6.Read() + value += localityblock7.Read() + } + benchmarkSink = value +} diff --git a/test/llgoext/runtime_g_bench_test.go b/test/llgoext/runtime_g_bench_test.go new file mode 100644 index 0000000000..927a75d3b9 --- /dev/null +++ b/test/llgoext/runtime_g_bench_test.go @@ -0,0 +1,45 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package llgoext + +import ( + "testing" + "unsafe" +) + +var runtimeDeferSink unsafe.Pointer + +//go:linkname runtimeGetThreadDefer github.com/goplus/llgo/runtime/internal/runtime.GetThreadDefer +func runtimeGetThreadDefer() unsafe.Pointer + +func BenchmarkRuntimeGetThreadDefer(b *testing.B) { + for i := 0; i < b.N; i++ { + runtimeDeferSink = runtimeGetThreadDefer() + } +} + +func BenchmarkRuntimeGoroutineEntryWithDefer(b *testing.B) { + for i := 0; i < b.N; i++ { + done := make(chan struct{}) + go func() { + defer close(done) + }() + <-done + } +} diff --git a/test/llgoext/runtime_g_test.go b/test/llgoext/runtime_g_test.go new file mode 100644 index 0000000000..38964b43a9 --- /dev/null +++ b/test/llgoext/runtime_g_test.go @@ -0,0 +1,114 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package llgoext + +import ( + "testing" + "unsafe" +) + +//go:linkname runtimeSetThreadDefer github.com/goplus/llgo/runtime/internal/runtime.SetThreadDefer +func runtimeSetThreadDefer(unsafe.Pointer) + +//go:linkname runtimeClearThreadDefer github.com/goplus/llgo/runtime/internal/runtime.ClearThreadDefer +func runtimeClearThreadDefer() + +type runtimeDeferProbeResult struct { + before unsafe.Pointer + inside unsafe.Pointer + after unsafe.Pointer +} + +func runtimeDeferProbe(ready chan<- unsafe.Pointer, start <-chan struct{}, results chan<- runtimeDeferProbeResult) { + result := runtimeDeferProbeResult{before: runtimeGetThreadDefer()} + func() { + defer func() {}() + result.inside = runtimeGetThreadDefer() + ready <- result.inside + <-start + }() + result.after = runtimeGetThreadDefer() + results <- result +} + +func TestRuntimeDeferHeadIsolation(t *testing.T) { + mainHead := runtimeGetThreadDefer() + ready := make(chan unsafe.Pointer, 2) + start := make(chan struct{}) + results := make(chan runtimeDeferProbeResult, 2) + + go runtimeDeferProbe(ready, start, results) + go runtimeDeferProbe(ready, start, results) + first := <-ready + second := <-ready + close(start) + + if first == nil || second == nil { + t.Fatalf("active defer heads = (%p, %p), want two non-nil heads", first, second) + } + if first == second { + t.Fatalf("concurrent goroutines shared defer head %p", first) + } + for i := 0; i < 2; i++ { + result := <-results + if result.inside == nil { + t.Fatal("active defer head is nil") + } + if result.after != result.before { + t.Fatalf("defer head after return = %p, want previous %p", result.after, result.before) + } + } + if got := runtimeGetThreadDefer(); got != mainHead { + t.Fatalf("main defer head changed to %p, want %p", got, mainHead) + } +} + +type runtimeDeferAccessorResult struct { + want unsafe.Pointer + got unsafe.Pointer + cleared unsafe.Pointer +} + +func runtimeDeferAccessorProbe(token unsafe.Pointer, results chan<- runtimeDeferAccessorResult) { + previous := runtimeGetThreadDefer() + runtimeSetThreadDefer(token) + got := runtimeGetThreadDefer() + runtimeClearThreadDefer() + cleared := runtimeGetThreadDefer() + runtimeSetThreadDefer(previous) + results <- runtimeDeferAccessorResult{want: token, got: got, cleared: cleared} +} + +func TestRuntimeDeferAccessors(t *testing.T) { + results := make(chan runtimeDeferAccessorResult, 2) + first := new(byte) + second := new(byte) + go runtimeDeferAccessorProbe(unsafe.Pointer(first), results) + go runtimeDeferAccessorProbe(unsafe.Pointer(second), results) + + for i := 0; i < 2; i++ { + result := <-results + if result.got != result.want { + t.Fatalf("defer head = %p, want %p", result.got, result.want) + } + if result.cleared != nil { + t.Fatalf("cleared defer head = %p, want nil", result.cleared) + } + } +} diff --git a/test/llgoext/testdata/localitybench/bench.go b/test/llgoext/testdata/localitybench/bench.go new file mode 100644 index 0000000000..2514a55b33 --- /dev/null +++ b/test/llgoext/testdata/localitybench/bench.go @@ -0,0 +1,57 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package localitybench + +import "unsafe" + +var backing int + +var ordinaryPointer *int + +//llgo:tls +var nativePointerBits uintptr + +//llgo:gls +var pointer *int + +//go:noinline +func Touch() { + pointer = &backing +} + +func PrepareReads() { + ordinaryPointer = &backing + nativePointerBits = uintptr(unsafe.Pointer(&backing)) + pointer = &backing +} + +//go:noinline +func ReadOrdinaryGlobal() uintptr { + return uintptr(unsafe.Pointer(ordinaryPointer)) +} + +//go:noinline +func ReadNativeTLS() uintptr { + return nativePointerBits +} + +//go:noinline +func ReadGLSPackage() uintptr { + return uintptr(unsafe.Pointer(pointer)) +} diff --git a/test/llgoext/testdata/localityblocks/p0/block.go b/test/llgoext/testdata/localityblocks/p0/block.go new file mode 100644 index 0000000000..b4978171e8 --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p0/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p0 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p1/block.go b/test/llgoext/testdata/localityblocks/p1/block.go new file mode 100644 index 0000000000..1c83a1dc19 --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p1/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p1 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p2/block.go b/test/llgoext/testdata/localityblocks/p2/block.go new file mode 100644 index 0000000000..440411923c --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p2/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p2 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p3/block.go b/test/llgoext/testdata/localityblocks/p3/block.go new file mode 100644 index 0000000000..2da455b714 --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p3/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p3 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p4/block.go b/test/llgoext/testdata/localityblocks/p4/block.go new file mode 100644 index 0000000000..82fa5ee80d --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p4/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p4 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p5/block.go b/test/llgoext/testdata/localityblocks/p5/block.go new file mode 100644 index 0000000000..8e5151e02d --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p5/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p5 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p6/block.go b/test/llgoext/testdata/localityblocks/p6/block.go new file mode 100644 index 0000000000..e11b82c5d2 --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p6/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p6 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityblocks/p7/block.go b/test/llgoext/testdata/localityblocks/p7/block.go new file mode 100644 index 0000000000..6ebff03a61 --- /dev/null +++ b/test/llgoext/testdata/localityblocks/p7/block.go @@ -0,0 +1,31 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package p7 + +import "unsafe" + +var backing int + +//llgo:gls +var pointer *int + +func Prepare() { pointer = &backing } + +//go:noinline +func Read() uintptr { return uintptr(unsafe.Pointer(pointer)) } diff --git a/test/llgoext/testdata/localityfailure/failure.go b/test/llgoext/testdata/localityfailure/failure.go new file mode 100644 index 0000000000..c42990ebd8 --- /dev/null +++ b/test/llgoext/testdata/localityfailure/failure.go @@ -0,0 +1,57 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package localityfailure + +const Failure = "locality initializer failed" + +var attempts int + +func initialize() int { + attempts++ + if attempts > 1 { + panic(Failure) + } + return attempts +} + +//llgo:tls +var value = initialize() + +//go:noinline +func Value() int { return value } + +func Attempts() int { return attempts } + +var nilAttempts int + +func initializeNil() int { + nilAttempts++ + if nilAttempts > 1 { + panic(nil) + } + return nilAttempts +} + +//llgo:gls +var nilValue = initializeNil() + +//go:noinline +func NilValue() int { return nilValue } + +func NilAttempts() int { return nilAttempts } diff --git a/test/llgoext/testdata/localityscope/scope.go b/test/llgoext/testdata/localityscope/scope.go new file mode 100644 index 0000000000..e20bd0a86a --- /dev/null +++ b/test/llgoext/testdata/localityscope/scope.go @@ -0,0 +1,83 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package localityscope + +var firstCalls int +var secondCalls int + +func initFirst() int { + firstCalls++ + return 100 + firstCalls +} + +func initSecond() int { + secondCalls++ + return 200 + secondCalls +} + +//llgo:gls +var first = initFirst() + +//llgo:gls +var second = initSecond() + +func FirstCalls() int { return firstCalls } +func SecondCalls() int { return secondCalls } +func First() int { return first } +func Second() int { return second } + +func SetFirst(value int) { first = value } + +func IncrementFirst() int { + first++ + return first +} + +var pairCalls int + +func initPair() (int, int) { + pairCalls++ + return 300 + pairCalls, 400 + pairCalls +} + +//llgo:gls +var pairFirst, pairSecond = initPair() + +func PairCalls() int { return pairCalls } + +func PairFirst() int { return pairFirst } +func PairSecond() int { return pairSecond } + +var mixedCalls int +var mixedBacking = 500 + +func initMixed() (int, *int) { + mixedCalls++ + return 600 + mixedCalls, &mixedBacking +} + +//llgo:tls +var mixedScalar, mixedPointer = initMixed() + +func MixedCalls() int { return mixedCalls } + +func MixedScalar() int { return mixedScalar } +func MixedPointer() *int { return mixedPointer } + +func MixedScalarAddress() *int { return &mixedScalar }