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/_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/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/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/compile.go b/cl/compile.go index a824d69fdf..c0ee5d2279 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -208,6 +208,7 @@ type context struct { staticGlobalInits map[*ssa.Global]llssa.Expr staticInitStores map[*ssa.Store]none + locality localityLowering } func (p *context) rewriteValue(name string) (string, bool) { @@ -389,7 +390,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 } @@ -599,12 +603,15 @@ 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 p.fn = fn p.goFn = f p.callerFrameMark = llssa.Nil + p.locality.function = localityFunction{} p.state = state // restore pkgState when compiling funcBody defer func() { p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark = oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark + p.locality.function = oldLocalityFunction }() p.phis = nil if dbgSymsEnabled { @@ -620,6 +627,7 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun bodyPos := p.getFuncBodyPos(f) b.DebugFunction(fn, pos, bodyPos) } + p.prepareExportedLocalContext(f) p.bvals = make(map[ssa.Value]llssa.Expr) p.methodNilDerefChecks = collectMethodNilDerefChecks(f) off := make([]int, len(f.Blocks)) @@ -815,6 +823,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 @@ -823,6 +834,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()) } @@ -835,6 +849,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) } @@ -1664,11 +1679,13 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { } if p.returnNeedsImplicitRunDefers(v) { p.recordPanicLocation(b, v.Pos()) + p.emitPCLineLabel(b, p.deferRunPos(v.Pos())) b.RunDefers() } if p.shouldTrackCallerFrames() { p.popCallerLocationFrame(b) } + p.leaveExportedLocalContext(b) b.Return(results...) case *ssa.If: fn := p.fn @@ -1693,10 +1710,16 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { p.call(b, llssa.Go, &v.Call) case *ssa.RunDefers: p.recordPanicLocation(b, v.Pos()) + p.emitPCLineLabel(b, p.deferRunPos(v.Pos())) b.RunDefers() case *ssa.Panic: arg := p.compileValue(b, v.X) p.recordPanicLocation(b, v.Pos()) + // panic is not a Call instruction, so callEx's statement anchor + // does not cover it; the panic snapshot attributes the panicking + // frame to this pc (issue5856 wants the panic line, not the + // nearest call's). + p.emitPCLineLabel(b, v.Pos()) b.Panic(arg) case *ssa.Send: ch := p.compileValue(b, v.Chan) @@ -1763,7 +1786,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) } @@ -1852,6 +1875,25 @@ func (p *context) functionHasExplicitStackDeferSeen(fn *ssa.Function, seen map[* return false } +// deferRunPos is where gc attributes a deferred function's caller frame: +// the function's closing brace — defers run at function exit, not at the +// defer statement (goroot issue14646, issue5856). +func (p *context) deferRunPos(fallback token.Pos) token.Pos { + if p.goFn != nil { + switch syntax := p.goFn.Syntax().(type) { + case *ast.FuncDecl: + if syntax.Body != nil && syntax.Body.Rbrace.IsValid() { + return syntax.Body.Rbrace + } + case *ast.FuncLit: + if syntax.Body != nil && syntax.Body.Rbrace.IsValid() { + return syntax.Body.Rbrace + } + } + } + return fallback +} + func (p *context) returnNeedsImplicitRunDefers(ret *ssa.Return) bool { fn := ret.Parent() if fn == nil || fn.Synthetic != "" || ret.Block() == fn.Recover { @@ -1999,6 +2041,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) } @@ -2099,6 +2150,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/defer_panic_pc_test.go b/cl/defer_panic_pc_test.go new file mode 100644 index 0000000000..0deb3d0aef --- /dev/null +++ b/cl/defer_panic_pc_test.go @@ -0,0 +1,145 @@ +//go:build !llgo +// +build !llgo + +package cl + +import ( + "fmt" + "go/ast" + "go/token" + "strings" + "testing" + + "golang.org/x/tools/go/ssa" +) + +func TestDeferRunPos(t *testing.T) { + ssapkg, _ := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo + +func declared() {} + +func outer() { + func() {}() +} +`) + fallback := token.Pos(1) + + declFn := ssapkg.Func("declared") + decl, ok := declFn.Syntax().(*ast.FuncDecl) + if !ok { + t.Fatalf("declared syntax = %T, want *ast.FuncDecl", declFn.Syntax()) + } + ctx := &context{goFn: declFn} + if got := ctx.deferRunPos(fallback); got != decl.Body.Rbrace { + t.Fatalf("declaration defer position = %v, want closing brace %v", got, decl.Body.Rbrace) + } + + outer := ssapkg.Func("outer") + if len(outer.AnonFuncs) != 1 { + t.Fatalf("outer anonymous functions = %d, want 1", len(outer.AnonFuncs)) + } + litFn := outer.AnonFuncs[0] + lit, ok := litFn.Syntax().(*ast.FuncLit) + if !ok { + t.Fatalf("literal syntax = %T, want *ast.FuncLit", litFn.Syntax()) + } + ctx.goFn = litFn + if got := ctx.deferRunPos(fallback); got != lit.Body.Rbrace { + t.Fatalf("literal defer position = %v, want closing brace %v", got, lit.Body.Rbrace) + } + + declBody := decl.Body + decl.Body = nil + ctx.goFn = declFn + if got := ctx.deferRunPos(fallback); got != fallback { + t.Fatalf("bodyless declaration position = %v, want fallback %v", got, fallback) + } + decl.Body = declBody + declRbrace := decl.Body.Rbrace + decl.Body.Rbrace = token.NoPos + if got := ctx.deferRunPos(fallback); got != fallback { + t.Fatalf("declaration with invalid closing brace position = %v, want fallback %v", got, fallback) + } + decl.Body.Rbrace = declRbrace + + litBody := lit.Body + lit.Body = nil + ctx.goFn = litFn + if got := ctx.deferRunPos(fallback); got != fallback { + t.Fatalf("bodyless literal position = %v, want fallback %v", got, fallback) + } + lit.Body = litBody + litRbrace := lit.Body.Rbrace + lit.Body.Rbrace = token.NoPos + if got := ctx.deferRunPos(fallback); got != fallback { + t.Fatalf("literal with invalid closing brace position = %v, want fallback %v", got, fallback) + } + lit.Body.Rbrace = litRbrace + + ctx.goFn = &ssa.Function{} + if got := ctx.deferRunPos(fallback); got != fallback { + t.Fatalf("function without syntax position = %v, want fallback %v", got, fallback) + } + ctx.goFn = nil + if got := ctx.deferRunPos(fallback); got != fallback { + t.Fatalf("nil function position = %v, want fallback %v", got, fallback) + } +} + +func TestCompileDeferAndPanicPCLineAnchors(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func withDefer() { + defer func() {}() + runtime.Caller(0) +} + +func withLiteralDefer() { + func() { + defer func() {}() + runtime.Caller(0) + }() +} + +func withPanic() { + runtime.Caller(0) + panic("boom") +} +`) + prog := newLLSSAProg(t) + prog.Target().GOOS = "linux" + prog.Target().GOARCH = "amd64" + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + + tests := []struct { + symbol string + line int + column int + }{ + {symbol: "example.com/foo.withDefer", line: 7, column: 1}, + {symbol: "example.com/foo.withLiteralDefer$1", line: 13, column: 2}, + {symbol: "example.com/foo.withPanic", line: 18, column: 7}, + } + for _, tt := range tests { + if !hasPCLineMetadataPosition(ir, tt.symbol, tt.line, tt.column) { + t.Fatalf("missing pc-line anchor for %s at caller_frame_compile.go:%d:%d:\n%s", tt.symbol, tt.line, tt.column, ir) + } + } +} + +func hasPCLineMetadataPosition(ir, symbol string, line, column int) bool { + want := fmt.Sprintf(`!%q, !"caller_frame_compile.go", i32 %d, i32 %d}`, symbol, line, column) + for _, row := range strings.Split(ir, "\n") { + if strings.Contains(row, "= !{i32 1, i64 ") && strings.Contains(row, want) { + return true + } + } + return false +} 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/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..b06dc2e432 --- /dev/null +++ b/cl/locality_lower.go @@ -0,0 +1,415 @@ +/* + * 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" +) + +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/static_init.go b/cl/static_init.go index 4e72444a17..d69af46591 100644 --- a/cl/static_init.go +++ b/cl/static_init.go @@ -75,6 +75,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..8e2bd6d0b3 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 } @@ -134,12 +134,16 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g // 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) { +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 +229,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 +254,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/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index e41c3e811f..c017c7e5f4 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -277,6 +277,11 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Le "-target", targetTriple, "-Qunused-arguments", "-Wno-unused-command-line-argument", + // Keep frame pointers in C code too: the runtime's physical + // unwinder walks fault-site chains through C frames (Go keeps + // them via the "frame-pointer"="non-leaf" attribute; x86-64 C + // would omit them at -O by default). + "-fno-omit-frame-pointer", } if ltoMode.Enabled() { export.CCFLAGS = append(export.CCFLAGS, ltoMode.ClangFlag()) 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/_wrap/runtime.c b/runtime/internal/lib/runtime/_wrap/runtime.c index 7b03d95b65..a0e504f350 100644 --- a/runtime/internal/lib/runtime/_wrap/runtime.c +++ b/runtime/internal/lib/runtime/_wrap/runtime.c @@ -23,12 +23,3 @@ int llgo_maxprocs() return 1; #endif } - -__attribute__((noinline)) void *llgo_framepointer(void) -{ -#if defined(__GNUC__) || defined(__clang__) - return __builtin_frame_address(0); -#else - return 0; -#endif -} diff --git a/runtime/internal/lib/runtime/extern.go b/runtime/internal/lib/runtime/extern.go index aa3d7dbc55..ca61558d24 100644 --- a/runtime/internal/lib/runtime/extern.go +++ b/runtime/internal/lib/runtime/extern.go @@ -26,7 +26,7 @@ func Caller(skip int) (pc uintptr, file string, line int, ok bool) { ensureRuntimePCLN() if fpUnwindAvailable() { var pcs [1]uintptr - if fpCallers(skip+1, pcs[:]) >= 1 { + if callersWithPanicSplice(skip+1, pcs[:]) >= 1 { // Caller returns the call-instruction PC (matching Go), whereas // Callers returns return PCs. Keeping the adjusted value matters // when the return address equals the next function's entry. @@ -53,7 +53,7 @@ func Caller(skip int) (pc uintptr, file string, line int, ok bool) { func Callers(skip int, pc []uintptr) int { ensureRuntimePCLN() if fpUnwindAvailable() { - if n := fpCallers(skip, pc); n > 0 { + if n := callersWithPanicSplice(skip, pc); n > 0 { return n } } diff --git a/runtime/internal/lib/runtime/fault_unwind_llgo.go b/runtime/internal/lib/runtime/fault_unwind_llgo.go index a975eba713..4e3622da32 100644 --- a/runtime/internal/lib/runtime/fault_unwind_llgo.go +++ b/runtime/internal/lib/runtime/fault_unwind_llgo.go @@ -107,6 +107,7 @@ func onFault(pc, fp uintptr, sig int32) { n += m } faultN = int32(n) + rtdebug.StoreFaultPCs(faultPCs[:n]) } // Capture done: re-arm the recursion guard before this fault turns // into an ordinary (recoverable) panic. 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..ca868bc4f2 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) { @@ -1698,6 +1698,17 @@ func refinePCSymbolLine(sym pcSymbol, pc uintptr) pcSymbol { lineSym.pc = pc return mergePCLineSymbol(sym, lineSym) } + // No same-function statement anchor covers pc. Mid-function pcs of a + // record with no evidence can actually belong to a foreign (C) + // function linked between Go functions — nearest-below cannot see the + // hole and would misattribute the frame to the preceding Go function. + // One dladdr cross-check on this cold, cache-backed path: a closer + // symbol wins. Exact entries and anchored functions never get here. + if pc != sym.entry { + if ai := addrInfoSymbol(pc); ai.ok && ai.entry > sym.entry { + return ai + } + } return sym } diff --git a/runtime/internal/lib/runtime/unwind_llgo.go b/runtime/internal/lib/runtime/unwind_llgo.go index 68a7313b9d..4efe885232 100644 --- a/runtime/internal/lib/runtime/unwind_llgo.go +++ b/runtime/internal/lib/runtime/unwind_llgo.go @@ -14,6 +14,213 @@ func c_framepointer() unsafe.Pointer func init() { rtdebug.PanicTraceback = panicTraceback rtdebug.PanicRecovered = clearFaultTraceback + rtdebug.PanicPCSnapshot = capturePanicPCs + rtdebug.RecoverMark = recoverMark +} + +// recoverMark records the recovering deferred frame (and one above, for +// wrapper-reached recover) so the panic snapshot stays spliceable while +// that frame is live. After siglongjmp the frame-pointer chain two levels +// up can point into a stale/reused stack region that is sometimes +// unmapped; probe each slot before dereferencing — an unguarded read here +// self-faults ~7% of the time, converting to a nil-deref panic that +// corrupts the value the recover was extracting (goroot reflectmake flake). +func recoverMark() { + // Record this function's frame address: it sits below the recovering + // deferred frame, and the liveness gate tests interval containment, so + // the exact level does not matter. + fp := callerFramePointer() + if fp == 0 { + return + } + rtdebug.MarkPanicRecoverFPs(fp, 0) +} + +// capturePanicPCs runs at panic time, before any longjmp unwinding, and +// stores the physical pc chain for later splicing (see spliceCallers). +func capturePanicPCs() { + if !fpUnwindAvailable() { + return + } + var pcs [64]uintptr + n := fpCallers(0, pcs[:]) + rtdebug.StorePanicPCs(pcs[:n]) +} + +// panicSplicePCs returns the snapshot when it is observable: either the +// panic is still in flight, or the deferred frame that recovered it is +// still live on the physical chain (gc keeps panic frames on the stack +// exactly that long). +func panicSplicePCs() []uintptr { + pcs := rtdebug.PanicPCs() + if len(pcs) == 0 { + return nil + } + if rtdebug.PanicActive() { + return pcs + } + mark, _ := rtdebug.PanicRecoverFPs() + if mark == 0 { + return nil + } + // The mark is a frame address recorded inside Recover's call chain; + // the recovering deferred frame is live iff the mark still lies + // within the current chain's span. Interval containment instead of + // exact equality: the hook's own frame depth differs across + // platforms (stub/wrapper layers), but any frame's [fp, parent fp) + // range straddling the mark proves the region is still stack, not + // reused heap or a dead extent. + fp := callerFramePointer() + for i := 0; fp != 0 && i < maxPanicSpliceFrames; i++ { + if !memReadable(fp) { + break + } + prev := *(*uintptr)(unsafe.Pointer(fp)) + if fp <= mark && (prev > mark || prev == 0) { + return pcs + } + if prev <= fp || prev-fp > maxFPStride || prev&(unsafe.Sizeof(uintptr(0))-1) != 0 { + break + } + fp = prev + } + return nil +} + +const maxPanicSpliceFrames = 4096 + +// callerFramePointer returns the frame of its Go caller. llgo_framepointer +// returns the C helper's own frame; consume that saved link and this helper's +// live saved link immediately, before another call can reuse either slot. +// +//go:noinline +func callerFramePointer() uintptr { + fp := uintptr(c_framepointer()) + if fp == 0 { + return 0 + } + fp = *(*uintptr)(unsafe.Pointer(fp)) + if fp == 0 { + return 0 + } + return *(*uintptr)(unsafe.Pointer(fp)) +} + +// trimPlumbingPCs drops leading pcs attributed to the LLGo runtime core +// (panic machinery, the capture path) and cuts the tail at the first pc +// outside the program text — fault snapshots are captured without the +// text bound (see fpWalkFrom). +func trimPlumbingPCs(pcs []uintptr) []uintptr { + initRuntimeFuncPCFrames() + head := 0 + for head < len(pcs) { + sym := frameSymbol(pcs[head] - 1) + if sym.function != "" && (hasPrefix(sym.function, "github.com/goplus/llgo/runtime/internal/") || + sym.function == "runtime.capturePanicPCs" || sym.function == "runtime.onFault" || + sym.function == "runtime.fpWalkFrom") { + head++ + continue + } + break + } + // Keep the innermost panic-machinery frame: gc's logical stack has + // runtime.gopanic between the deferred function and the panic site, + // and fixed Caller depths (issue5856's Caller(2)) count it. Fault + // snapshots start at the fault pc and trim nothing — gc's walkers + // skip runtime frames by name there, not by depth. + if head > 0 { + head-- + } + pcs = pcs[head:] + if rtdebug.PanicPCsAreFault() { + // Fault snapshots come from a genuine interrupted context and the + // chain-discipline guards already bounded the walk; keep unnamed + // frames (linux dladdr cannot name non-dynamic C symbols — they + // display as raw pcs, like gc does for unknown frames). + return pcs + } + for i := 0; i < len(pcs); i++ { + if prebuiltTextContains(pcs[i]) { + continue + } + // Outside the Go text range: C frames in this binary (and its + // libraries) still resolve to a symbol via dladdr — keep those; + // cut at the first pc nothing can name (wild slots past the last + // FP-disciplined frame). + if frameSymbol(pcs[i]-1).function == "" { + return pcs[:i] + } + } + return pcs +} + +// spliceCallers rebuilds the caller view a deferred function should see +// during (or right after recovering) a panic: its own live frames, then the +// panic-site chain from the snapshot. The junction is the first live frame +// whose function also appears in the snapshot — the defer owner; the +// snapshot side wins there because the live copy\'s pc points at the +// longjmp resume site, not at the call that panicked. +func spliceCallers(cur []uintptr) []uintptr { + snap := panicSplicePCs() + if len(snap) == 0 { + return cur + } + snap = trimPlumbingPCs(snap) + if len(snap) == 0 { + return cur + } + // The junction is the first live frame whose function also appears in + // the snapshot — the defer owner (or the panicking function itself when + // defer and panic share a frame). Everything from there down is + // replaced by the whole snapshot: it already contains the owner and its + // callers, with the owner's pc on the panic path instead of the longjmp + // resume site. + for i := 0; i < len(cur); i++ { + entry := frameSymbol(cur[i] - 1).entry + if entry == 0 { + continue + } + for j := 0; j < len(snap); j++ { + if frameSymbol(snap[j]-1).entry == entry { + out := make([]uintptr, 0, i+len(snap)) + out = append(out, cur[:i]...) + out = append(out, snap...) + return out + } + } + } + return cur +} + +// callersWithPanicSplice is Callers with panic-frame splicing. With no +// snapshot stored (the overwhelmingly common case) it degrades to the +// plain walk at the cost of one TLS load. Otherwise the raw walk runs +// unskipped so splicing sees the junction frame, then the requested skip +// applies to the spliced view (matching gc, whose skip counts the logical +// panic-inclusive stack). +// +//go:noinline +func callersWithPanicSplice(skip int, pc []uintptr) int { + if len(pc) == 0 { + return 0 + } + if len(rtdebug.PanicPCs()) == 0 { + // One frame deeper than the extern.go call sites used to be. + return fpCallers(skip+1, pc) + } + var raw [128]uintptr + n := fpCallers(1, raw[:]) + if n <= 0 { + return 0 + } + view := spliceCallers(raw[:n]) + if skip < 0 { + skip = 0 + } + if skip >= len(view) { + return 0 + } + return copy(pc, view[skip:]) } func hasPrefix(s, prefix string) bool { @@ -49,8 +256,12 @@ func panicTraceback(skip int) bool { if n <= 0 { return false } + // A stored panic snapshot (Go panic or hardware fault, including + // faults inside C code) carries the frames the longjmp unwinding + // already removed; splice them in like Callers does. + view := spliceCallers(pcs[:n]) print("goroutine 1 [running]:\n") - frames := CallersFrames(pcs[:n]) + frames := CallersFrames(view) skippingPlumbing := true for { frame, more := frames.Next() diff --git a/runtime/internal/runtime/_wrap/fp.c b/runtime/internal/runtime/_wrap/fp.c new file mode 100644 index 0000000000..ac8cff69ce --- /dev/null +++ b/runtime/internal/runtime/_wrap/fp.c @@ -0,0 +1,11 @@ +/* llgo_framepointer lives in the runtime core (not the public runtime + * package): Recover() records the recovering frame through it, and + * programs that never import "runtime" still link the core. */ +__attribute__((noinline)) void *llgo_framepointer(void) +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_frame_address(0); +#else + return 0; +#endif +} diff --git a/runtime/internal/runtime/caller.go b/runtime/internal/runtime/caller.go index 341158e825..e5a9ae0155 100644 --- a/runtime/internal/runtime/caller.go +++ b/runtime/internal/runtime/caller.go @@ -226,7 +226,93 @@ func Callers(skip int, pcs []uintptr) int { return n } +// PanicPCSnapshot, set by the public runtime package at init, captures the +// physical pc chain at panic time into the per-thread snapshot below. gc +// runs deferred functions on top of the panicked stack, so runtime.Caller, +// CallersFrames and debug.Stack invoked from a deferred function (before or +// after recover) see the panic-site frames; LLGo's longjmp unwinding +// removes them physically, and this snapshot is what caller-info APIs +// splice back in. +var PanicPCSnapshot func() + func SavePanicCallerFrames() { + // A fault handler stores the fault-site snapshot right before it + // panics; the regular capture here must not overwrite it. + if panicPCs.armed != 0 { + panicPCs.armed = 0 + return + } + if PanicPCSnapshot != nil { + PanicPCSnapshot() + } +} + +type panicPCStore struct { + n int32 + armed int32 + fault int32 + recFP1 uintptr + recFP2 uintptr + pcs [64]uintptr +} + +// StorePanicPCs replaces the thread's panic snapshot (a new panic +// supersedes the previous one) and resets the recover marks. +func StorePanicPCs(pcs []uintptr) { + storePanicPCs(pcs, 0) +} + +// StoreFaultPCs is StorePanicPCs for fault handlers: the imminent +// panic's own capture is suppressed so the fault-site chain survives. +func StoreFaultPCs(pcs []uintptr) { + storePanicPCs(pcs, 1) +} + +func storePanicPCs(pcs []uintptr, armed int32) { + p := &panicPCs + n := len(pcs) + if n > len(p.pcs) { + n = len(p.pcs) + } + copy(p.pcs[:n], pcs) + p.n = int32(n) + p.armed = armed + p.fault = armed + p.recFP1 = 0 + p.recFP2 = 0 +} + +// PanicPCsAreFault reports whether the stored snapshot came from a +// hardware-fault context (captured without the program-text bound). +func PanicPCsAreFault() bool { + return panicPCs.fault != 0 +} + +// PanicPCs returns the thread's captured panic pcs (nil when none). +func PanicPCs() []uintptr { + p := &panicPCs + if p.n == 0 { + return nil + } + return p.pcs[:p.n] +} + +// MarkPanicRecoverFPs records the frames observing the panic at recover +// time; the snapshot stays spliceable exactly while one of them is live on +// the physical chain (the deferred function has not returned yet). +func MarkPanicRecoverFPs(fp1, fp2 uintptr) { + panicPCs.recFP1 = fp1 + panicPCs.recFP2 = fp2 +} + +// PanicRecoverFPs returns the recover-time frame marks. +func PanicRecoverFPs() (uintptr, uintptr) { + return panicPCs.recFP1, panicPCs.recFP2 +} + +// PanicActive reports whether a panic is in flight (not yet recovered). +func PanicActive() bool { + return getg().panic_ != nil } func BindCallerLocation(pc uintptr, rawName string) { 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/defer_tls.go b/runtime/internal/runtime/g.go similarity index 56% rename from runtime/internal/runtime/defer_tls.go rename to runtime/internal/runtime/g.go index 6a430e9924..ffd180d06b 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,31 @@ 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 + 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..3f181a4d03 --- /dev/null +++ b/runtime/internal/runtime/local_context.go @@ -0,0 +1,143 @@ +//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) +} + +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 +} + +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..71fa122eda --- /dev/null +++ b/runtime/internal/runtime/local_initializer.go @@ -0,0 +1,62 @@ +/* + * 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" + +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/panic_pcs_global.go b/runtime/internal/runtime/panic_pcs_global.go new file mode 100644 index 0000000000..1c4501d232 --- /dev/null +++ b/runtime/internal/runtime/panic_pcs_global.go @@ -0,0 +1,21 @@ +//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 + +var panicPCs panicPCStore diff --git a/runtime/internal/runtime/panic_pcs_tls.go b/runtime/internal/runtime/panic_pcs_tls.go new file mode 100644 index 0000000000..af286797f2 --- /dev/null +++ b/runtime/internal/runtime/panic_pcs_tls.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:tls +var panicPCs panicPCStore diff --git a/runtime/internal/runtime/z_default.go b/runtime/internal/runtime/z_default.go index d0757f9cb1..4d33eced58 100644 --- a/runtime/internal/runtime/z_default.go +++ b/runtime/internal/runtime/z_default.go @@ -16,7 +16,8 @@ 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)) if PanicTraceback == nil || !PanicTraceback(2) { @@ -27,7 +28,7 @@ func Rethrow(link *Defer) { } else { 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 +37,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..090e3e4cbd 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" ) @@ -38,18 +37,43 @@ type Defer struct { // Recover recovers a panic. func Recover() (ret any) { - ptr := excepKey.Get() + gp := getg() + ptr := gp.panic_ if ptr != nil { - excepKey.Set(nil) + gp.panic_ = nil ret = *(*any)(ptr) c.Free(ptr) if PanicRecovered != nil { PanicRecovered() } + // The deferred function that recovers keeps observing the panic + // stack until it returns (gc runs defers on top of it). The public + // runtime marks its frame so the pc snapshot stays spliceable that + // long; the mark reads the frame-pointer chain, which after + // siglongjmp can reach a stale/unmapped slot, so the guarded read + // lives in the package that has a page probe (RecoverMark). Nil + // when lib/runtime is not linked — no snapshot machinery, nothing + // to mark. + if RecoverMark != nil { + RecoverMark() + } } return } +// RecoverMark, set by the public runtime package, records the recovering +// frame for panic-snapshot splicing. +var RecoverMark func() + +const ( + // LLGoFiles: the frame-pointer helper must live in the runtime core — + // programs that never import "runtime" still link Recover. + LLGoFiles = "_wrap/fp.c" +) + +//go:linkname c_framepointer C.llgo_framepointer +func c_framepointer() unsafe.Pointer + // Panic panics with a value. func Panic(v any) { if v == nil { @@ -58,26 +82,20 @@ func Panic(v any) { SavePanicCallerFrames() ptr := c.Malloc(unsafe.Sizeof(v)) *(*any)(ptr) = v - excepKey.Set(ptr) + gp := getg() + gp.panic_ = ptr - Rethrow((*Defer)(c.GoDeferData())) + 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/ssa/decl.go b/ssa/decl.go index 82ba5026a7..e82933c66f 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/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/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..a8e9a62a7c --- /dev/null +++ b/ssa/locality.go @@ -0,0 +1,220 @@ +/* + * 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{} + } + seen := make(map[string]bool) + current := name + for { + if seen[current] { + return "", VariableLocality{}, false, fmt.Errorf("declaration linkname cycle involving %s", current) + } + seen[current] = true + target, hasLink := linkname(current) + target = strings.TrimPrefix(target, "go:") + if !hasLink || target == "" { + return current, result, ok, nil + } + 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() + 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() + 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 a13ccae93b..0010d218cf 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" @@ -223,7 +224,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 @@ -311,7 +314,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), } prog.abi.Init(uintptr(prog.ptrSize), (*goProgram)(unsafe.Pointer(prog))) return prog @@ -392,11 +396,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 } @@ -853,6 +861,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..33d63e2ffd 100644 --- a/ssa/ssa_test.go +++ b/ssa/ssa_test.go @@ -1896,6 +1896,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 +2694,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/_manualtest/README.md b/test/_manualtest/README.md index ddb96abd29..69ce1f1300 100644 --- a/test/_manualtest/README.md +++ b/test/_manualtest/README.md @@ -44,18 +44,20 @@ runtime-internal, patched-stdlib and startup frames may differ. # SIGFPE Verified (darwin/arm64 + linux/arm64 + linux/amd64): -- SIGSEGV in a C frame converts to a Go panic; recover observes gc's exact - error text. -- Known gaps (recorded for the follow-up PRs): - 1. The fault-site stack (cexc_leaf_segv -> cexc_mid_segv x3 -> cexc_segv - -> Go frames) is not visible yet — recover/tracebacks show the - post-longjmp stack. The panic-snapshot follow-up extends to signal - handlers: walk the FP chain from the ucontext pc/fp; C frames get - dladdr names, Go frames funcinfo names — same machinery as the - unwinder. - 2. Only SIGSEGV is installed; SIGFPE (amd64 division) core-dumps, SIGBUS - is not handled. - 3. No sigaltstack: on stack overflow the handler cannot run and the +- SIGSEGV/SIGBUS/SIGFPE in a C frame convert to Go panics (gc's exact + error texts); recover works, and the fault-site stack — C frames down + through the Go callers — appears in recovered `debug.Stack()` and in + unrecovered tracebacks (panic pc snapshot captured from the signal + ucontext, FP chain walked from the interrupted frame; C is compiled + with -fno-omit-frame-pointer so x86-64 chains hold). +- Known limitations: + 1. On linux, C frames may display under a neighboring Go function's + name: dladdr only sees dynamic symbols there, so the nearest-below + table attribution cannot be cross-checked (darwin names C frames + correctly via dladdr). Pre-existing for any C pc between Go + functions; the link-phase hole-sentinel follow-up fixes naming and + misattribution together. + 2. No sigaltstack: on stack overflow the handler cannot run and the process dies (gc prints "stack overflow"). Note that C-side UB gets propagated by clang (this test was once optimized into infinite recursion); wrap/fault.c uses a volatile pointer to prevent that. diff --git a/test/go/caller_acceptance_test.go b/test/go/caller_acceptance_test.go index d078765025..0fb84b824d 100644 --- a/test/go/caller_acceptance_test.go +++ b/test/go/caller_acceptance_test.go @@ -25,6 +25,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strconv" "strings" "sync" @@ -422,6 +423,7 @@ func TestCallerAcceptanceCFaultRecover(t *testing.T) { import ( "fmt" "os" + "runtime/debug" _ "unsafe" ) @@ -437,7 +439,7 @@ func viaGo() { cexcSegv(2) } -func main() { +func one(last bool) { defer func() { r := recover() if r == nil { @@ -448,10 +450,22 @@ func main() { if !ok || err.Error() != "runtime error: invalid memory address or nil pointer dereference" { panic(r) } - os.Stdout.WriteString("CFAULT_OK\n") + if last { + os.Stdout.WriteString("CFAULT_OK\n") + os.Stdout.Write(debug.Stack()) + } }() viaGo() } + +func main() { + // Three sequential faults: a handler that leaves the signal blocked + // after the longjmp escape (savemask=0 jmpbufs) survives the first + // fault and core-dumps on the second — the exact CI failure mode. + one(false) + one(false) + one(true) +} ` const csrc = `#include @@ -460,9 +474,11 @@ func main() { static int32_t *volatile cexc_null; volatile int32_t cexc_marks; -static void cexc_leaf(void) { *cexc_null = 42; } +/* Non-static: the fault-chain tail cut keeps frames dladdr can name; + * static helpers have no symbol and would end the visible chain. */ +void cexc_leaf(void) { *cexc_null = 42; } -static void cexc_mid(int32_t depth) { +void cexc_mid(int32_t depth) { if (depth > 0) { cexc_mid(depth - 1); cexc_marks++; @@ -489,6 +505,20 @@ void cexc_segv(int32_t depth) { if !strings.Contains(out, "CFAULT_OK") { t.Fatalf("C fault probe missing marker:\n%s", out) } + // The fault-site chain must be visible from the recovered deferred + // function. C frame names come from dladdr, which on linux only sees + // dynamic symbols — there the C frames show as raw pcs and only the + // Go side of the chain is asserted (foreign-symbol naming is the + // planned link-phase hole-sentinel follow-up). + wants := []string{"main.viaGo", "main.main"} + if runtime.GOOS == "darwin" { + wants = append(wants, "cexc_segv") + } + for _, want := range wants { + if !strings.Contains(out, want) { + t.Fatalf("recovered stack missing fault frame %q:\n%s", want, out) + } + } } func writeCallerAcceptanceModule(t *testing.T, dir string, files map[string]string) { 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 2f30a6997f..7d72ff78dd 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2819,15 +2819,11 @@ xfails: - platform: darwin/arm64 directive: run case: fixedbugs/bug347.go - reason: latest main goroot run failure on darwin/arm64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - platform: darwin/arm64 directive: run case: fixedbugs/bug348.go - reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue14646.go - reason: latest main goroot run failure on darwin/arm64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - platform: darwin/arm64 directive: run case: fixedbugs/issue15281.go @@ -2839,7 +2835,7 @@ xfails: - platform: darwin/arm64 directive: run case: fixedbugs/issue27201.go - reason: latest main goroot run failure on darwin/arm64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - platform: darwin/arm64 directive: run case: fixedbugs/issue27518b.go @@ -2847,15 +2843,11 @@ xfails: - platform: darwin/arm64 directive: run case: fixedbugs/issue29504.go - reason: latest main goroot run failure on darwin/arm64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - platform: darwin/arm64 directive: run case: fixedbugs/issue32477.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue33724.go - reason: latest main goroot run failure on darwin/arm64 - version: go1.24 platform: darwin/arm64 directive: run @@ -2868,8 +2860,12 @@ xfails: reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run - case: fixedbugs/issue4562.go + case: fixedbugs/issue45045.go reason: latest main goroot run failure on darwin/arm64 + - platform: darwin/arm64 + directive: run + case: fixedbugs/issue4562.go + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - platform: darwin/arm64 directive: run case: fixedbugs/issue46725.go @@ -2892,10 +2888,6 @@ xfails: directive: run case: fixedbugs/issue57823.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue5856.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue5963.go @@ -2909,17 +2901,12 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/bug347.go - reason: go1.25 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.25 platform: linux/amd64 directive: run case: fixedbugs/bug348.go - reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: fixedbugs/issue14646.go - reason: go1.25 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.25 platform: linux/amd64 directive: run @@ -2944,7 +2931,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue27201.go - reason: go1.25 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.25 platform: linux/amd64 directive: run @@ -2954,17 +2941,12 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue29504.go - reason: go1.25 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.25 platform: linux/amd64 directive: run case: fixedbugs/issue32477.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: fixedbugs/issue33724.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -2984,7 +2966,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue4562.go - reason: go1.25 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.25 platform: linux/amd64 directive: run @@ -3010,11 +2992,6 @@ xfails: directive: run case: fixedbugs/issue57823.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: fixedbugs/issue5856.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -3080,12 +3057,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/bug347.go - reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue14646.go - reason: go1.24 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.24 platform: linux/amd64 directive: run @@ -3100,7 +3072,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue29504.go - reason: go1.24 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.24 platform: linux/amd64 directive: run @@ -3121,11 +3093,6 @@ xfails: directive: run case: fixedbugs/issue57823.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue5856.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3160,7 +3127,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/bug348.go - reason: go1.24 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.24 platform: linux/amd64 directive: run @@ -3175,17 +3142,12 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue27201.go - reason: go1.24 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.24 platform: linux/amd64 directive: run case: fixedbugs/issue27518b.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue33724.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3205,7 +3167,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue4562.go - reason: go1.24 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.24 platform: linux/amd64 directive: run @@ -3275,7 +3237,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/bug348.go - reason: go1.26 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.26 platform: linux/amd64 directive: run @@ -3290,17 +3252,12 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue27201.go - reason: go1.26 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.26 platform: linux/amd64 directive: run case: fixedbugs/issue27518b.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue33724.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -3320,7 +3277,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue4562.go - reason: go1.26 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.26 platform: linux/amd64 directive: run @@ -3350,12 +3307,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/bug347.go - reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue14646.go - reason: go1.26 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.26 platform: linux/amd64 directive: run @@ -3370,7 +3322,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue29504.go - reason: go1.26 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.26 platform: linux/amd64 directive: run @@ -3391,11 +3343,6 @@ xfails: directive: run case: fixedbugs/issue57823.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue5856.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run 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 }