From 55d1732732a8a974c80e56f9156d7380eaf0c4e5 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 13 Jul 2026 14:47:36 +0800 Subject: [PATCH 1/2] doc: propose TLS and GLS variable directives --- doc/tls-gls-proposal.md | 195 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 doc/tls-gls-proposal.md diff --git a/doc/tls-gls-proposal.md b/doc/tls-gls-proposal.md new file mode 100644 index 0000000000..4803b792cc --- /dev/null +++ b/doc/tls-gls-proposal.md @@ -0,0 +1,195 @@ +Proposal draft, 2026-07-13 + +# Proposal: LLGo TLS and GLS Variable Directives + +## Summary + +Add two package-variable directives: + +```go +//llgo:tls +var threadState T + +//llgo:gls +var goroutineState U +``` + +`//llgo:tls` gives each operating-system thread an independent instance of the +variable. `//llgo:gls` gives each Go goroutine an independent instance. + +LLGo currently implements one Go goroutine with one pthread. The initial +implementation can therefore lower both directives to LLVM thread-local +storage. Their source semantics and compiler metadata remain distinct so that +`gls` can move into a goroutine object when LLGo gains an M:N scheduler, while +`tls` remains tied to the operating-system thread. + +## Goals + +* Provide low-cost access to LLGo runtime state owned by a thread or goroutine. +* Define initialization, recursion, panic, GC, and thread-exit behavior. +* Preserve the TLS/GLS distinction across package loading and build caches. +* Support references to local variables from other packages. +* Avoid a pthread-specific API in ordinary LLGo runtime code. + +## Non-Goals + +* Implement an M:N scheduler as part of this proposal. +* Define TLS semantics for bare-metal targets that have no thread lifecycle. +* Provide source compatibility with the complete platform-specific TLS APIs. +* Make these directives part of the Go language or portable to other Go + compilers. + +## Terminology and Naming + +TLS means thread-local storage. GLS means goroutine-local storage. The short +names describe the ownership model directly and form a clear pair. Longer +spellings such as `threadlocal` and `goroutinelocal` are not proposed as aliases: +aliases become permanent syntax without adding semantics. + +The spelling follows existing LLGo directives: no space is required between +`//` and `llgo:`. A parser may continue to accept the spaced form consistently +with other LLGo directives, but documentation and generated code use the +canonical form. + +## Semantics + +| Directive | Owner | Initial lowering | Future M:N lowering | +| --- | --- | --- | --- | +| `//llgo:tls` | OS thread | LLVM TLS | LLVM TLS | +| `//llgo:gls` | Go goroutine | LLVM TLS | Field or side storage owned by `g` | + +Taking a variable's address returns the address of the current owner's +instance. Access from another package has the same behavior as access from the +declaring package. + +The LLGo runtime's pointer to the current goroutine is TLS, not GLS. It is the +bootstrap used to find GLS after an M:N scheduler is introduced. + +## Declaration Rules + +The directives apply only to package-level `var` declarations. They do not +apply to constants, types, functions, parameters, or local variables. + +A declaration cannot carry both directives. All non-blank variables in one +multi-value initialization must use the same ownership class because Go +evaluates its right-hand side as one initialization unit. A blank identifier +cannot be local storage. Invalid combinations are compile-time errors. + +The initial implementation does not combine these directives with `go:embed`. +Such a declaration is rejected until its initialization and generated data +ownership are specified. + +## Initialization + +Each owner starts with a zero-valued instance. A variable with an explicit +initializer evaluates that initializer exactly once for each owner before the +owner's first use of any variable in the same initialization unit. + +Package initialization on the initial thread evaluates local initializers in +the same order as ordinary Go package variables. It must not duplicate existing +initializer side effects. A newly created thread or goroutine initializes its +instance lazily on first access. + +Initialization has three states: uninitialized, initializing, and initialized. +Recursive access while the state is initializing observes the partially +initialized instance and does not re-enter the initializer. If initialization +panics, the state remains initializing and the initializer is not retried. This +matches the process-wide rule that package initialization side effects are not +silently repeated. + +Zero-valued, pointer-free local variables need no initialization guard. A +zero-valued variable that can contain Go pointers still performs the root +registration described below. + +## GC Roots and Lifetime + +Native TLS is outside the ordinary Go heap and global-root scan. Before a +pointer-containing TLS allocation can be used, the runtime registers its byte +range as a conservative GC root for the current thread. Registration is once +per range per thread. + +The runtime keeps all registered ranges in one thread-owned list. One pthread +destructor removes those roots and releases the list when the thread exits. +The key is created lazily so runtime-owned TLS can register safely during +runtime package initialization. Steady-state variable access uses a TLS guard +and does not require a pthread key lookup. In `nogc` builds, root registration +is a no-op. + +When GLS is moved into a GC-visible goroutine object, its native TLS root +registration is removed. TLS root handling remains unchanged. + +## Compiler Representation + +Directive scanning is part of the existing package declaration scan, together +with `go:linkname`, LLGo type directives, and exports. It must not add separate +whole-package AST passes. The program records each successfully scanned package +object so the direct compiler entry point can provide a fallback scan without +repeating the preload scan used by the normal build path. + +The program stores one declaration record for each relevant object. The record +contains link name, type background, locality (`none`, `tls`, or `gls`), and +generated initializer/ensure symbols. Lowering consumes this record instead of +rescanning comments. + +TLS and GLS initially emit LLVM `thread_local` globals. Pointer-free zero-value +variables have no access helper. Other local variables use a generated ensure +function for per-owner initialization and/or root registration. Imported +references declare the same thread-local symbol and call the declaring +package's ensure function when required. + +## Build Cache + +Declaration records are serialized into each package cache manifest in a +stable order. On a cache hit, the loader validates and atomically merges the +records before compiling dependent packages. Conflicting records or duplicate +ownership result in a cache miss rather than silently selecting one value. + +This makes cache hits and source builds provide the same cross-package TLS/GLS +metadata. Aggregation happens during the normal package preload phase, before +SSA construction, so no later global scan is needed. + +## Target Support + +The first implementation supports LLGo pthread targets with LLVM TLS. Darwin +and Linux are required test platforms. Unsupported targets, including +bare-metal targets without a defined thread lifecycle, should report a clear +compile-time diagnostic instead of silently using process-global storage. + +## Compatibility + +These directives are new and LLGo-specific. No compatibility alias is required. +The implementation should reject the earlier experimental spellings +`//llgo:threadlocal` and `//llgo:goroutinelocal` so source code does not depend +on syntax that was never released. + +## Testing + +Tests must cover: + +* directive parsing, invalid declarations, and conflicting directives; +* LLVM TLS emission for both ownership classes; +* zero, constant, dynamic, recursive, and panicking initializers; +* independent values on multiple threads and goroutines; +* pointer survival across GC and cleanup at thread exit; +* cross-package access with source builds and cache hits; +* `gc` and `nogc` builds; and +* Darwin and Linux CI targets supported by LLGo. + +The runtime migration of the current goroutine pointer must demonstrate that +the steady-state `getg` path performs native TLS address resolution without a +pthread key lookup or heap-backed TLS slot. + +## Alternatives Considered + +Using only `tls` would describe the current lowering but lose the semantic +boundary required by a future M:N scheduler. A generic `local` directive would +leave ownership ambiguous. The longer `threadlocal` and `goroutinelocal` +spellings are explicit but add verbosity without improving the model defined by +TLS and GLS. + +## Open Questions + +* Whether bare-metal builds should reject both directives or define GLS for a + single execution context in a later proposal. +* Whether a future M:N implementation needs a restricted static-initializer + mode for GLS before it supports arbitrary per-goroutine initializer calls. From 42156848dd19b60a5c6b5ec6401024cd155dc9ac Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 13 Jul 2026 14:47:57 +0800 Subject: [PATCH 2/2] compiler/runtime: implement TLS and GLS variables --- cl/_testgo/tlsgls/expect.txt | 4 + cl/_testgo/tlsgls/main.go | 31 ++ cl/_testgo/tlsgls/state/state.go | 100 +++++ cl/compile.go | 52 ++- cl/directive.go | 72 ++++ cl/import.go | 165 +++++--- cl/import_coverage_test.go | 27 +- cl/locality.go | 394 +++++++++++++++++ cl/locality_test.go | 442 ++++++++++++++++++++ internal/build/build.go | 39 +- internal/build/build_test.go | 12 +- internal/build/collect.go | 130 +++++- internal/build/collect_test.go | 132 +++++- internal/build/fingerprint.go | 17 +- runtime/internal/runtime/g_tls.go | 13 +- runtime/internal/runtime/local_root_gc.go | 97 +++++ runtime/internal/runtime/local_root_nogc.go | 23 + runtime/internal/runtime/local_root_stub.go | 23 + ssa/decl.go | 19 +- ssa/package.go | 145 ++++++- ssa/ssa_test.go | 78 +++- ssa/type.go | 2 +- ssa/type_cvt.go | 13 +- 23 files changed, 1926 insertions(+), 104 deletions(-) create mode 100644 cl/_testgo/tlsgls/expect.txt create mode 100644 cl/_testgo/tlsgls/main.go create mode 100644 cl/_testgo/tlsgls/state/state.go create mode 100644 cl/directive.go create mode 100644 cl/locality.go create mode 100644 cl/locality_test.go create mode 100644 runtime/internal/runtime/local_root_gc.go create mode 100644 runtime/internal/runtime/local_root_nogc.go create mode 100644 runtime/internal/runtime/local_root_stub.go diff --git a/cl/_testgo/tlsgls/expect.txt b/cl/_testgo/tlsgls/expect.txt new file mode 100644 index 0000000000..e348f35655 --- /dev/null +++ b/cl/_testgo/tlsgls/expect.txt @@ -0,0 +1,4 @@ +main 11 22 33 3 41 1 1 101 1 +g1 14 25 36 6 0 2 1 102 2 +g2 17 28 39 9 0 3 1 103 3 +main 11 22 33 9 41 3 1 101 3 diff --git a/cl/_testgo/tlsgls/main.go b/cl/_testgo/tlsgls/main.go new file mode 100644 index 0000000000..ac9a459ea8 --- /dev/null +++ b/cl/_testgo/tlsgls/main.go @@ -0,0 +1,31 @@ +// LITTEST +package main + +import "github.com/goplus/llgo/cl/_testgo/tlsgls/state" + +func printValues(owner string) { + // CHECK-LABEL: define void @"{{.*}}tlsgls.printValues" + // CHECK: call { i64, i64, i64, i64 } @"{{.*}}tlsgls/state.Values"() + tls, gls, pointer, sequence := state.Values() + // CHECK: call { i64, i64 } @"{{.*}}tlsgls/state.PoisonValues"() + poison, attempts := state.PoisonValues() + // CHECK: call { i64, i64 } @"{{.*}}tlsgls/state.LateValues"() + late, lateAttempts := state.LateValues() + // CHECK: call i64 @"{{.*}}tlsgls/state.RecursiveValue"() + println(owner, tls, gls, pointer, sequence, poison, attempts, state.RecursiveValue(), late, lateAttempts) +} + +func run(owner string, done chan bool) { + printValues(owner) + done <- true +} + +func main() { + printValues("main") + done := make(chan bool) + go run("g1", done) + <-done + go run("g2", done) + <-done + printValues("main") +} diff --git a/cl/_testgo/tlsgls/state/state.go b/cl/_testgo/tlsgls/state/state.go new file mode 100644 index 0000000000..a20c220352 --- /dev/null +++ b/cl/_testgo/tlsgls/state/state.go @@ -0,0 +1,100 @@ +package state + +import "runtime" + +var sequence int + +func next(base int) int { + sequence++ + return base + sequence +} + +func nextPointer(base int) *int { + value := next(base) + return &value +} + +//llgo:tls +var TLS = next(10) + +//llgo:gls +var GLS = next(20) + +//llgo:gls +var Pointer = nextPointer(30) + +var poisonAttempts int + +func poisonOtherThreads() int { + poisonAttempts++ + if poisonAttempts > 1 { + panic("poison TLS initializer") + } + return 40 + poisonAttempts +} + +//llgo:tls +var Poison = poisonOtherThreads() + +type recursiveInitializer interface { + value() int +} + +type recursiveValue struct{} + +func (recursiveValue) value() int { + return Recursive + 1 +} + +var recursiveSource recursiveInitializer = recursiveValue{} + +//llgo:gls +var Recursive = recursiveSource.value() + +var zLateAttempts int + +func nextLate() int { + zLateAttempts++ + return 100 + zLateAttempts +} + +// zLate sorts after the synthetic package init function in SSA member order. +// +//llgo:tls +var zLate = nextLate() + +func Values() (tls, gls, pointer, count int) { + tls = TLS + gls = GLS + if Pointer == nil { + panic("nil GLS pointer") + } + runtime.GC() + pointer = *Pointer + count = sequence + return +} + +func readPoison() int { + return Poison +} + +func PoisonValues() (value, attempts int) { + func() { + defer func() { + _ = recover() + }() + value = readPoison() + }() + value = readPoison() + attempts = poisonAttempts + return +} + +func RecursiveValue() int { + return Recursive +} + +func LateValues() (value, attempts int) { + return zLate, zLateAttempts +} diff --git a/cl/compile.go b/cl/compile.go index c38929ba69..f010492794 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -208,6 +208,8 @@ type context struct { staticGlobalInits map[*ssa.Global]llssa.Expr staticInitStores map[*ssa.Store]none + localInitGroups map[string]*localInitGroup + localGlobals map[*ssa.Global]llssa.Global } func (p *context) rewriteValue(name string) (string, bool) { @@ -389,7 +391,14 @@ func (p *context) compileGlobal(pkg llssa.Package, gbl *ssa.Global) { return } dbgInstrln("==> NewVar", name, typ) - g := pkg.NewVar(name, typ, llssa.Background(vtype)) + local, hasDecl := p.prog.DeclInfo(llssa.FullName(gbl.Pkg.Pkg, gbl.Name())) + isLocal := hasDecl && local.Locality != llssa.LocalityNone + var g llssa.Global + if isLocal { + g = p.localGlobal(pkg, local, gbl, name, typ, vtype) + } else { + g = pkg.NewVar(name, typ, llssa.Background(vtype)) + } if p.tryEmbedGlobalInit(pkg, gbl, g, name) { return } @@ -409,6 +418,21 @@ func (p *context) compileGlobal(pkg llssa.Package, gbl *ssa.Global) { } } +func (p *context) localGlobal(pkg llssa.Package, local llssa.DeclInfo, gbl *ssa.Global, name string, typ types.Type, vtype int) llssa.Global { + if global, ok := p.localGlobals[gbl]; ok { + return global + } + global := pkg.NewThreadLocalVar(name, typ, llssa.Background(vtype)) + if p.localGlobals == nil { + p.localGlobals = make(map[*ssa.Global]llssa.Global) + } + p.localGlobals[gbl] = global + if local.EnsureFunc != "" { + p.registerLocalInitializer(pkg, local, global, gbl) + } + return global +} + func makeClosureCtx(pkg *types.Package, vars []*ssa.FreeVar) *types.Var { n := len(vars) flds := make([]*types.Var, n) @@ -835,6 +859,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) } @@ -1930,6 +1955,12 @@ 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 = validateLocalInitializers(prog, pkgTypes); err != nil { + return nil, nil, err + } if pkgPath == llssa.PkgRuntime { prog.SetRuntime(pkgTypes) } @@ -1992,6 +2023,7 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri if !ctx.skipall { processPkg(ctx, ret, pkg) } + ctx.buildLocalInitializers(ret) for len(ctx.inits) > 0 { inits := ctx.inits ctx.inits = nil @@ -2030,6 +2062,24 @@ 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 }) + // Package init can reference any package global, including globals whose + // names sort after "init". Register every local group before compiling + // function bodies so the initial thread can premark all local guards. + for _, m := range members { + global, ok := m.val.(*ssa.Global) + if !ok || isCgoFuncPtrVar(global.Name()) { + continue + } + local, ok := ctx.prog.DeclInfo(llssa.FullName(global.Pkg.Pkg, global.Name())) + if !ok || local.Locality == llssa.LocalityNone { + continue + } + typ := ctx.patchType(global.Type()) + name, vtype, _ := ctx.varName(global.Pkg.Pkg, global) + if vtype != pyVar { + ctx.localGlobal(ret, local, global, name, typ, vtype) + } + } for _, m := range members { member := m.val diff --git a/cl/directive.go b/cl/directive.go new file mode 100644 index 0000000000..5a368800f2 --- /dev/null +++ b/cl/directive.go @@ -0,0 +1,72 @@ +/* + * 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 ( + "go/ast" + "go/token" + "strings" +) + +type sourceDirective struct { + Name string + Args string + Raw string + Pos token.Pos +} + +func parseSourceDirective(comment *ast.Comment) (sourceDirective, bool) { + if comment == nil { + return sourceDirective{}, 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 sourceDirective{Name: "export", Args: strings.TrimSpace(raw[len("//export "):]), Raw: raw, Pos: comment.Pos()}, true + default: + return sourceDirective{}, false + } + body = strings.TrimSpace(body) + if body == "" { + return sourceDirective{}, false + } + name, args := body, "" + if idx := strings.IndexAny(body, " \t"); idx >= 0 { + name, args = body[:idx], strings.TrimSpace(body[idx+1:]) + } + return sourceDirective{Name: namespace + name, Args: args, Raw: raw, Pos: comment.Pos()}, true +} + +func sourceDirectives(doc *ast.CommentGroup) []sourceDirective { + if doc == nil { + return nil + } + ret := make([]sourceDirective, 0, len(doc.List)) + for _, comment := range doc.List { + if directive, ok := parseSourceDirective(comment); ok { + ret = append(ret, directive) + } + } + return ret +} diff --git a/cl/import.go b/cl/import.go index e13437b693..b2c1ef7d51 100644 --- a/cl/import.go +++ b/cl/import.go @@ -180,7 +180,7 @@ func (p *context) initFiles(pkgPath string, files []*ast.File, cPkg bool) { switch decl := decl.(type) { case *ast.FuncDecl: fullName, inPkgName := astFuncName(pkgPath, decl) - if !p.processLinknameByDoc(decl.Doc, fullName, inPkgName, false, true) && cPkg { + if !p.processExportByDoc(decl.Doc, fullName, inPkgName) && cPkg { // package C (https://github.com/goplus/llgo/issues/1165) if decl.Recv == nil && token.IsExported(inPkgName) { exportName := strings.TrimPrefix(inPkgName, "X") @@ -190,13 +190,6 @@ func (p *context) initFiles(pkgPath string, files []*ast.File, cPkg bool) { } case *ast.GenDecl: switch decl.Tok { - case token.VAR: - if len(decl.Specs) == 1 { - if names := decl.Specs[0].(*ast.ValueSpec).Names; len(names) == 1 { - inPkgName := names[0].Name - p.processLinknameByDoc(decl.Doc, pkgPath+"."+inPkgName, inPkgName, true, true) - } - } case token.CONST: fallthrough case token.TYPE: @@ -217,30 +210,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,20 +267,57 @@ func (p *context) collectSkip(line string, prefix int) { } 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-- { - line := doc.List[n].Text - ret := p.initLinkname(line, allowExport, func(name string, isExport bool) (_ string, _, ok bool) { - return fullName, isVar, name == inPkgName || (isExport && enableExportRename) - }) - if ret != unknownDirective { - return ret == hasLinkname - } + directives := sourceDirectives(doc) + for n := len(directives) - 1; n >= 0; n-- { + directive := directives[n] + if directive.Name != "go:linkname" && directive.Name != "llgo:link" && !(allowExport && directive.Name == "export") { + continue + } + ret := p.initLinkname(directive.Raw, allowExport, func(name string, isExport bool) (_ string, _, ok bool) { + return fullName, isVar, name == inPkgName || (isExport && enableExportRename) + }) + if ret == hasLinkname { + return true } } return false } +func collectLinknameByDoc(prog llssa.Program, doc *ast.CommentGroup, fullName, inPkgName string) { + directives := sourceDirectives(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) processExportByDoc(doc *ast.CommentGroup, fullName, inPkgName string) bool { + const export = "//export " + directives := sourceDirectives(doc) + for n := len(directives) - 1; n >= 0; n-- { + directive := directives[n] + if directive.Name != "export" { + continue + } + line := directive.Raw + if !strings.ContainsAny(directive.Args, " \t") { + line += " " + directive.Args + } + p.initLink(line, len(export), true, func(name string, isExport bool) (_ string, _ bool, ok bool) { + return fullName, false, name == inPkgName || (isExport && enableExportRename) + }) + return true + } + return false +} + const ( noDirective = iota hasLinkname @@ -705,9 +711,19 @@ func (p *context) varOf(b llssa.Builder, v *ssa.Global) llssa.Expr { } panic("unreachable") } + local, hasDecl := p.prog.DeclInfo(llssa.FullName(v.Pkg.Pkg, v.Name())) + isLocal := hasDecl && local.Locality != llssa.LocalityNone ret := pkg.VarOf(name) if ret == nil { - ret = pkg.NewVar(name, p.patchType(v.Type()), llssa.Background(vtype)) + if isLocal { + ret = pkg.NewThreadLocalVar(name, p.patchType(v.Type()), llssa.Background(vtype)) + } else { + ret = pkg.NewVar(name, p.patchType(v.Type()), llssa.Background(vtype)) + } + } + if local.EnsureFunc != "" { + ensure := pkg.NewFunc(local.EnsureFunc, llssa.NoArgsNoRet, llssa.InGo) + b.Call(ensure.Expr) } return ret.Expr } @@ -737,19 +753,62 @@ func (p *context) initPyModule() { } } -// ParsePkgSyntax parses AST of a package to check llgo:type in type declaration. -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 prog.PackageSyntaxParsed(pkg) { + return nil + } + pkgPath := llssa.PathOf(pkg) for _, file := range files { for _, decl := range file.Decls { switch decl := decl.(type) { + case *ast.FuncDecl: + if err := rejectNonVarLocality(fset, decl.Doc); err != nil { + return err + } + fullName, inPkgName := astFuncName(pkgPath, decl) + collectLinknameByDoc(prog, decl.Doc, fullName, inPkgName) case *ast.GenDecl: switch decl.Tok { case token.TYPE: + if err := rejectNonVarLocality(fset, decl.Doc); err != nil { + return err + } + for _, spec := range decl.Specs { + if err := rejectNonVarLocality(fset, spec.(*ast.TypeSpec).Doc); err != nil { + return err + } + } handleTypeDecl(prog, pkg, decl) + case 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) + } + } + if err := registerVarLocalities(prog, fset, pkg, decl); err != nil { + return err + } + default: + if err := rejectNonVarLocality(fset, decl.Doc); err != nil { + return err + } + for _, node := range decl.Specs { + if spec, ok := node.(*ast.ValueSpec); ok { + if err := rejectNonVarLocality(fset, spec.Doc); err != nil { + return err + } + } + } } } } } + prog.MarkPackageSyntaxParsed(pkg) + return nil } func handleTypeDecl(prog llssa.Program, pkg *types.Package, decl *ast.GenDecl) { @@ -761,21 +820,11 @@ func handleTypeDecl(prog llssa.Program, pkg *types.Package, decl *ast.GenDecl) { } } -const ( - llgotype = "//llgo:type " - llgotype2 = "// llgo:type " -) - func typeBackground(doc *ast.CommentGroup) (bg string) { - if doc != nil { - if n := len(doc.List); n > 0 { - line := doc.List[n-1].Text - if strings.HasPrefix(line, llgotype) { - return strings.TrimSpace(line[len(llgotype):]) - } - if strings.HasPrefix(line, llgotype2) { - return strings.TrimSpace(line[len(llgotype2):]) - } + directives := sourceDirectives(doc) + for n := len(directives) - 1; n >= 0; n-- { + if directives[n].Name == "llgo:type" { + return directives[n].Args } } return diff --git a/cl/import_coverage_test.go b/cl/import_coverage_test.go index d5710ea5d3..63087c773c 100644 --- a/cl/import_coverage_test.go +++ b/cl/import_coverage_test.go @@ -55,7 +55,19 @@ type ( } 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) + } + 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 TestPkgSymInfoAddSymAndInitLinknamesCoverage(t *testing.T) { @@ -152,13 +164,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"}, } @@ -171,12 +184,20 @@ 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 TestBoolToUint8InvalidArgs(t *testing.T) { diff --git a/cl/locality.go b/cl/locality.go new file mode 100644 index 0000000000..83ea539164 --- /dev/null +++ b/cl/locality.go @@ -0,0 +1,394 @@ +/* + * 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" + "sort" + "strings" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const ( + threadLocalDirective = "//llgo:tls" + goroutineLocalDirective = "//llgo:gls" + legacyThreadLocal = "llgo:threadlocal" + legacyGoroutineLocal = "llgo:goroutinelocal" + localInitPrefix = "__llgo_local_init_" +) + +// PrepareLocalVariables processes package variable locality directives before +// Go SSA is created. Explicit initializers get a synthetic function that can +// replay the initializer once for each local execution context. +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 + } + if len(prog.PackageDecls(llssa.PathOf(pkg))) == 0 { + return nil + } + + nextName := 0 + for _, initializer := range info.InitOrder { + _, found, err := initializerLocality(prog, fset, pkg, initializer) + if err != nil { + return err + } + if !found { + continue + } + if len(files) == 0 { + return fmt.Errorf("cannot prepare local initializer for package %q without syntax files", llssa.PathOf(pkg)) + } + file := files[len(files)-1] + name := "" + for { + name = fmt.Sprintf("%s%d", localInitPrefix, nextName) + nextName++ + if pkg.Scope().Lookup(name) == nil { + break + } + } + fnObj, decl := makeLocalInitializer(pkg, info, name, initializer) + if alt := pkg.Scope().Insert(fnObj); alt != nil { + return fmt.Errorf("local initializer %q conflicts with %v", name, alt) + } + file.Decls = append(file.Decls, decl) + initName := llssa.FullName(pkg, name) + ensureName := localEnsureName(initName) + for _, variable := range initializer.Lhs { + fullName := llssa.FullName(pkg, variable.Name()) + prog.SetLocalInitFunc(fullName, initName) + prog.SetLocalEnsureFunc(fullName, ensureName) + } + } + for _, name := range pkg.Scope().Names() { + variable, ok := pkg.Scope().Lookup(name).(*types.Var) + if !ok { + continue + } + fullName := llssa.FullName(pkg, name) + decl, ok := prog.DeclInfo(fullName) + if !ok || decl.Locality == llssa.LocalityNone || decl.HasInitializer || !typeHasPointers(variable.Type()) { + continue + } + prog.SetLocalEnsureFunc(fullName, fullName+"$ensure") + } + return nil +} + +func validateLocalInitializers(prog llssa.Program, pkg *types.Package) error { + for name, info := range prog.PackageDecls(llssa.PathOf(pkg)) { + if info.Locality != llssa.LocalityNone && info.HasInitializer && (info.InitFunc == "" || info.EnsureFunc == "") { + return fmt.Errorf("local variable %s has not had its initializer prepared before SSA compilation", name) + } + } + return nil +} + +func registerVarLocalities(prog llssa.Program, fset *token.FileSet, pkg *types.Package, decl *ast.GenDecl) error { + declLocality, declPos, err := localityFromDoc(fset, decl.Doc) + if err != nil { + return err + } + for _, node := range decl.Specs { + spec := node.(*ast.ValueSpec) + specLocality, specPos, err := localityFromDoc(fset, spec.Doc) + if err != nil { + return err + } + locality, localityPos, err := mergeLocality(fset, declLocality, declPos, specLocality, specPos) + if err != nil { + return err + } + if locality == llssa.LocalityNone { + continue + } + if target := prog.Target(); target != nil && target.Target != "" { + return localityError(fset, localityPos, "%s is not supported for target %q", localityDirective(locality), target.Target) + } + if hasDirective(decl.Doc, "go:embed") || hasDirective(spec.Doc, "go:embed") { + return localityError(fset, spec.Pos(), "%s and %s cannot apply to the same variable declaration", localityDirective(locality), "//go:embed") + } + for _, ident := range spec.Names { + if ident.Name == "_" { + return localityError(fset, ident.Pos(), "locality directive cannot apply to the blank identifier") + } + prog.SetVarLocality(llssa.FullName(pkg, ident.Name), locality, len(spec.Values) != 0) + } + } + return nil +} + +func localityFromDoc(fset *token.FileSet, doc *ast.CommentGroup) (llssa.Locality, token.Pos, error) { + var locality llssa.Locality + var pos token.Pos + if doc == nil { + return locality, pos, nil + } + for _, directive := range sourceDirectives(doc) { + var next llssa.Locality + switch directive.Name { + case "llgo:tls": + next = llssa.ThreadLocal + case "llgo:gls": + next = llssa.GoroutineLocal + case legacyThreadLocal: + return llssa.LocalityNone, token.NoPos, localityError(fset, directive.Pos, "//%s is not supported; use %s", legacyThreadLocal, threadLocalDirective) + case legacyGoroutineLocal: + return llssa.LocalityNone, token.NoPos, localityError(fset, directive.Pos, "//%s is not supported; use %s", legacyGoroutineLocal, goroutineLocalDirective) + default: + continue + } + if directive.Args != "" { + return llssa.LocalityNone, token.NoPos, localityError(fset, directive.Pos, "//%s does not accept arguments", directive.Name) + } + if locality != llssa.LocalityNone && locality != next { + return llssa.LocalityNone, token.NoPos, localityError(fset, directive.Pos, "%s and %s cannot apply to the same variable declaration", threadLocalDirective, goroutineLocalDirective) + } + locality, pos = next, directive.Pos + } + return locality, pos, nil +} + +func rejectNonVarLocality(fset *token.FileSet, doc *ast.CommentGroup) error { + locality, pos, err := localityFromDoc(fset, doc) + if err != nil { + return err + } + if locality != llssa.LocalityNone { + return localityError(fset, pos, "%s applies only to package-level var declarations", localityDirective(locality)) + } + return nil +} + +func hasDirective(doc *ast.CommentGroup, name string) bool { + for _, directive := range sourceDirectives(doc) { + if directive.Name == name { + return true + } + } + return false +} + +func localityDirective(locality llssa.Locality) string { + if locality == llssa.GoroutineLocal { + return goroutineLocalDirective + } + return threadLocalDirective +} + +func mergeLocality(fset *token.FileSet, a llssa.Locality, apos token.Pos, b llssa.Locality, bpos token.Pos) (llssa.Locality, token.Pos, error) { + if a != llssa.LocalityNone && b != llssa.LocalityNone && a != b { + return llssa.LocalityNone, token.NoPos, localityError(fset, bpos, "%s and %s cannot apply to the same variable declaration", threadLocalDirective, goroutineLocalDirective) + } + if b != llssa.LocalityNone { + return b, bpos, nil + } + return a, apos, nil +} + +func initializerLocality(prog llssa.Program, fset *token.FileSet, pkg *types.Package, initializer *types.Initializer) (llssa.Locality, bool, error) { + var locality llssa.Locality + found := false + for _, variable := range initializer.Lhs { + decl, ok := prog.DeclInfo(llssa.FullName(pkg, variable.Name())) + if !ok || decl.Locality == llssa.LocalityNone { + if found { + return llssa.LocalityNone, false, localityError(fset, initializer.Rhs.Pos(), "one initializer cannot mix local and ordinary package variables") + } + continue + } + if !found { + locality, found = decl.Locality, true + } else if locality != decl.Locality { + return llssa.LocalityNone, false, localityError(fset, initializer.Rhs.Pos(), "one initializer cannot mix thread-local and goroutine-local variables") + } + } + if found && len(initializer.Lhs) != countLocalInitializerVariables(prog, pkg, initializer) { + return llssa.LocalityNone, false, localityError(fset, initializer.Rhs.Pos(), "one initializer cannot mix local and ordinary package variables") + } + return locality, found, nil +} + +func countLocalInitializerVariables(prog llssa.Program, pkg *types.Package, initializer *types.Initializer) int { + n := 0 + for _, variable := range initializer.Lhs { + if info, ok := prog.DeclInfo(llssa.FullName(pkg, variable.Name())); ok && info.Locality != llssa.LocalityNone { + n++ + } + } + return n +} + +func typeHasPointers(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 typeHasPointers(typ.Elem()) + case *types.Struct: + for i := 0; i < typ.NumFields(); i++ { + if typeHasPointers(typ.Field(i).Type()) { + return true + } + } + return false + case *types.Named: + return typeHasPointers(typ.Underlying()) + case *types.TypeParam: + return true + default: + return false + } +} + +func makeLocalInitializer(pkg *types.Package, info *types.Info, name string, initializer *types.Initializer) (*types.Func, *ast.FuncDecl) { + 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 localityError(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) +} + +const ( + localInitInitializing = 1 + localInitDone = 2 +) + +func localEnsureName(initFunc string) string { + return initFunc + "$ensure" +} + +func localGuardName(ensureFunc string) string { + return strings.TrimSuffix(ensureFunc, "$ensure") + "$guard" +} + +type localInitGroup struct { + guard llssa.Global + initFunc string + roots []llssa.Global +} + +func (p *context) registerLocalInitializer(pkg llssa.Package, local llssa.DeclInfo, global llssa.Global, goGlobal *ssa.Global) { + if p.localInitGroups == nil { + p.localInitGroups = make(map[string]*localInitGroup) + } + group := p.localInitGroups[local.EnsureFunc] + if group == nil { + guard := pkg.NewThreadLocalVar(localGuardName(local.EnsureFunc), types.NewPointer(types.Typ[types.Uint8]), llssa.InGo) + guard.InitNil() + group = &localInitGroup{guard: guard, initFunc: local.InitFunc} + p.localInitGroups[local.EnsureFunc] = group + } + if typeHasPointers(goGlobal.Type().(*types.Pointer).Elem()) { + group.roots = append(group.roots, global) + } +} + +func (p *context) buildLocalInitializers(pkg llssa.Package) { + for _, name := range p.localInitGroupNames() { + group := p.localInitGroups[name] + ensure := pkg.NewFunc(name, llssa.NoArgsNoRet, llssa.InGo) + if ensure.HasBody() { + continue + } + b := ensure.MakeBody(3) + zero := p.prog.IntVal(0, p.prog.Byte()) + initialized := b.BinOp(token.NEQ, b.Load(group.guard.Expr), zero) + b.If(initialized, ensure.Block(2), ensure.Block(1)) + b.SetBlock(ensure.Block(1)) + b.Store(group.guard.Expr, p.prog.IntVal(localInitInitializing, p.prog.Byte())) + p.registerLocalRoots(b, group) + if group.initFunc != "" { + helper := pkg.NewFunc(group.initFunc, llssa.NoArgsNoRet, llssa.InGo) + b.Call(helper.Expr) + } + b.Store(group.guard.Expr, p.prog.IntVal(localInitDone, p.prog.Byte())) + b.Jump(ensure.Block(2)) + b.SetBlock(ensure.Block(2)) + b.Return() + b.EndBuild() + } +} + +func (p *context) localInitGroupNames() []string { + names := make([]string, 0, len(p.localInitGroups)) + for name := range p.localInitGroups { + names = append(names, name) + } + sort.Strings(names) + return names +} + +func (p *context) registerLocalRoots(b llssa.Builder, group *localInitGroup) { + if len(group.roots) == 0 { + return + } + register := p.pkg.RuntimeFunc("RegisterLocalRoot") + for _, root := range group.roots { + size := p.prog.SizeOf(p.prog.Elem(root.Expr.Type)) + b.Call(register, root.Expr, p.prog.IntVal(size, p.prog.Uintptr())) + } +} + +func (p *context) initializeLocalGuards(b llssa.Builder) { + if len(p.localInitGroups) == 0 { + return + } + for _, name := range p.localInitGroupNames() { + group := p.localInitGroups[name] + // Root-only groups are initialized lazily. This is required by the + // runtime package itself: the root registry's pthread key is an + // ordinary package variable and must be initialized before currentG + // registers its TLS range. + if group.initFunc == "" { + continue + } + p.registerLocalRoots(b, group) + b.Store(group.guard.Expr, p.prog.IntVal(localInitDone, p.prog.Byte())) + } +} diff --git a/cl/locality_test.go b/cl/locality_test.go new file mode 100644 index 0000000000..9c77b7062d --- /dev/null +++ b/cl/locality_test.go @@ -0,0 +1,442 @@ +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 := &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), + } + 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)) + runtimePkg := types.NewPackage(llssa.PkgRuntime, "runtime") + params := types.NewTuple( + types.NewParam(token.NoPos, runtimePkg, "start", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, runtimePkg, "size", types.Typ[types.Uintptr]), + ) + runtimePkg.Scope().Insert(types.NewFunc(token.NoPos, runtimePkg, "RegisterLocalRoot", types.NewSignatureType(nil, nil, nil, params, nil, false))) + prog.SetRuntime(runtimePkg) + 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 prepareLocalitySource(t *testing.T, src string) error { + t.Helper() + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "locality.go", src, parser.ParseComments) + if err != nil { + return err + } + files := []*ast.File{file} + info := &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), + } + pkg, err := (&types.Config{Importer: importer.Default()}).Check("example.com/locality", fset, files, info) + if err != nil { + return err + } + prog := ssatest.NewProgram(t, nil) + if err := ParsePkgSyntax(prog, fset, pkg, files); err != nil { + return err + } + return PrepareLocalVariables(prog, fset, pkg, info, files) +} + +func TestLocalityDirectivesLowerToTLS(t *testing.T) { + prog, ir := compileLocalitySource(t, `package locality + +func makeValue() int { return 42 } + +var ordinaryValue int + +//llgo:tls +var threadValue = makeValue() + +//llgo:gls +var goroutineValue *int + +// llgo:tls +var zeroValue int + +func values() (int, *int, int) { return threadValue, goroutineValue, zeroValue } +`) + thread, ok := prog.DeclInfo("example.com/locality.threadValue") + if !ok || thread.Locality != llssa.ThreadLocal || thread.InitFunc == "" { + t.Fatalf("threadValue metadata = %+v, %v", thread, ok) + } + goroutine, ok := prog.DeclInfo("example.com/locality.goroutineValue") + if !ok || goroutine.Locality != llssa.GoroutineLocal || goroutine.InitFunc != "" || goroutine.EnsureFunc == "" { + t.Fatalf("goroutineValue metadata = %+v, %v", goroutine, ok) + } + zero, ok := prog.DeclInfo("example.com/locality.zeroValue") + if !ok || zero.Locality != llssa.ThreadLocal || zero.InitFunc != "" || zero.EnsureFunc != "" { + t.Fatalf("zeroValue metadata = %+v, %v", zero, ok) + } + for _, want := range []string{ + `@"example.com/locality.threadValue" = thread_local global i64`, + `@"example.com/locality.goroutineValue" = thread_local global ptr`, + `@"example.com/locality.zeroValue" = thread_local global i64`, + `@"example.com/locality.__llgo_local_init_0$guard" = thread_local global i8 0`, + `@"example.com/locality.goroutineValue$guard" = thread_local global i8 0`, + `define void @"example.com/locality.__llgo_local_init_0$ensure"()`, + `define void @"example.com/locality.goroutineValue$ensure"()`, + `call void @"example.com/locality.__llgo_local_init_0$ensure"()`, + `call void @"github.com/goplus/llgo/runtime/internal/runtime.RegisterLocalRoot"`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("IR missing %q:\n%s", want, ir) + } + } + guardDone := `store i8 2, ptr @"example.com/locality.__llgo_local_init_0$guard"` + if got := strings.Count(ir, guardDone); got != 2 { + t.Fatalf("initializer guard done stores = %d, want 2 (ensure and package init):\n%s", got, ir) + } +} + +func TestLocalityDirectiveConflict(t *testing.T) { + err := prepareLocalitySource(t, `package locality + +//llgo:tls +//llgo:gls +var value int +`) + if err == nil || !strings.Contains(err.Error(), "cannot apply to the same variable declaration") { + t.Fatalf("PrepareLocalVariables error = %v", err) + } +} + +func TestLocalityDirectiveRejectsBlankIdentifier(t *testing.T) { + err := prepareLocalitySource(t, `package locality + +//llgo:tls +var _ int +`) + if err == nil || !strings.Contains(err.Error(), "blank identifier") { + t.Fatalf("PrepareLocalVariables error = %v", err) + } +} + +func TestLocalityDirectiveDiagnostics(t *testing.T) { + tests := []struct { + name string + src string + want string + }{ + { + name: "legacy tls spelling", + src: `package locality + +//llgo:threadlocal +var value int +`, + want: "use //llgo:tls", + }, + { + name: "legacy gls spelling", + src: `package locality + +//llgo:goroutinelocal +var value int +`, + want: "use //llgo:gls", + }, + { + name: "arguments", + src: `package locality + +//llgo:tls extra +var value int +`, + want: "does not accept arguments", + }, + { + name: "function", + src: `package locality + +//llgo:gls +func value() {} +`, + want: "applies only to package-level var declarations", + }, + { + name: "type", + src: `package locality + +//llgo:tls +type value int +`, + want: "applies only to package-level var declarations", + }, + { + name: "constant", + src: `package locality + +//llgo:tls +const value = 1 +`, + want: "applies only to package-level var declarations", + }, + { + name: "embed", + src: `package locality + +import _ "embed" + +//go:embed locality.go +//llgo:tls +var value string +`, + want: "cannot apply to the same variable declaration", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := prepareLocalitySource(t, test.src) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("PrepareLocalVariables error = %v, want %q", err, test.want) + } + }) + } +} + +func TestTypeHasPointers(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 { + name string + typ types.Type + want bool + }{ + {name: "int", typ: types.Typ[types.Int]}, + {name: "string", typ: types.Typ[types.String], want: true}, + {name: "unsafe pointer", typ: types.Typ[types.UnsafePointer], want: true}, + {name: "pointer", typ: types.NewPointer(types.Typ[types.Int]), want: true}, + {name: "slice", typ: types.NewSlice(types.Typ[types.Int]), want: true}, + {name: "map", typ: types.NewMap(types.Typ[types.Int], types.Typ[types.Int]), want: true}, + {name: "channel", typ: types.NewChan(types.SendRecv, types.Typ[types.Int]), want: true}, + {name: "signature", typ: types.NewSignatureType(nil, nil, nil, nil, nil, false), want: true}, + {name: "interface", typ: types.NewInterfaceType(nil, nil).Complete(), want: true}, + {name: "array", typ: types.NewArray(types.Typ[types.Int], 2)}, + {name: "pointer array", typ: types.NewArray(types.NewPointer(types.Typ[types.Int]), 2), want: true}, + {name: "struct", typ: types.NewStruct([]*types.Var{types.NewVar(token.NoPos, pkg, "n", types.Typ[types.Int])}, nil)}, + {name: "pointer struct", typ: types.NewStruct([]*types.Var{ + types.NewVar(token.NoPos, pkg, "n", types.Typ[types.Int]), + types.NewVar(token.NoPos, pkg, "p", types.NewPointer(types.Typ[types.Int])), + }, nil), want: true}, + {name: "named", typ: namedInt}, + {name: "type parameter", typ: typeParam, want: true}, + {name: "tuple", typ: types.NewTuple(types.NewVar(token.NoPos, pkg, "n", types.Typ[types.Int]))}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := typeHasPointers(test.typ); got != test.want { + t.Fatalf("typeHasPointers(%v) = %v, want %v", test.typ, got, test.want) + } + }) + } +} + +func TestInitializerLocalityDiagnostics(t *testing.T) { + pkg := types.NewPackage("example.com/locality", "locality") + 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]) + prog := ssatest.NewProgram(t, nil) + prog.SetVarLocality(llssa.FullName(pkg, thread.Name()), llssa.ThreadLocal, true) + prog.SetVarLocality(llssa.FullName(pkg, goroutine.Name()), llssa.GoroutineLocal, true) + rhs := ast.NewIdent("rhs") + tests := []struct { + name string + lhs []*types.Var + want string + }{ + {name: "local then ordinary", lhs: []*types.Var{thread, ordinary}, want: "mix local and ordinary"}, + {name: "ordinary then local", lhs: []*types.Var{ordinary, thread}, want: "mix local and ordinary"}, + {name: "tls then gls", lhs: []*types.Var{thread, goroutine}, want: "mix thread-local and goroutine-local"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, _, err := initializerLocality(prog, nil, pkg, &types.Initializer{Lhs: test.lhs, Rhs: rhs}) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("initializerLocality error = %v, want %q", err, test.want) + } + }) + } + if locality, found, err := initializerLocality(prog, nil, pkg, &types.Initializer{Lhs: []*types.Var{ordinary}, Rhs: rhs}); err != nil || found || locality != llssa.LocalityNone { + t.Fatalf("ordinary initializer = %v, %v, %v", locality, found, err) + } +} + +func TestValidateLocalInitializers(t *testing.T) { + pkg := types.NewPackage("example.com/locality", "locality") + prog := ssatest.NewProgram(t, nil) + name := llssa.FullName(pkg, "value") + prog.SetVarLocality(name, llssa.ThreadLocal, true) + if err := validateLocalInitializers(prog, pkg); err == nil || !strings.Contains(err.Error(), "initializer prepared") { + t.Fatalf("validateLocalInitializers error = %v", err) + } + prog.SetLocalInitFunc(name, "example.com/locality.init") + prog.SetLocalEnsureFunc(name, "example.com/locality.init$ensure") + if err := validateLocalInitializers(prog, pkg); err != nil { + t.Fatal(err) + } +} + +func TestParseSourceDirective(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 := parseSourceDirective(nil); ok { + t.Fatal("nil comment parsed as a directive") + } + for _, test := range tests { + t.Run(test.text, func(t *testing.T) { + got, ok := parseSourceDirective(&ast.Comment{Text: test.text}) + if ok != test.ok || got.Name != test.name || got.Args != test.args { + t.Fatalf("parseSourceDirective(%q) = %+v, %v", test.text, got, ok) + } + }) + } +} + +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.SetVarLocality(llssa.FullName(pkg, value.Name()), llssa.ThreadLocal, 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) + } + if err := rejectNonVarLocality(nil, nil); err != nil { + t.Fatal(err) + } + (&context{}).initializeLocalGuards(nil) +} + +func TestLocalInitializerNameCollision(t *testing.T) { + prog, _ := compileLocalitySource(t, `package locality + +func __llgo_local_init_0() {} + +//llgo:tls +var value = 1 +`) + info, ok := prog.DeclInfo("example.com/locality.value") + if !ok || !strings.HasSuffix(info.InitFunc, ".__llgo_local_init_1") { + t.Fatalf("value metadata = %+v, %v", info, ok) + } +} + +func TestMergeLocality(t *testing.T) { + tests := []struct { + a, b llssa.Locality + want llssa.Locality + err bool + }{ + {want: llssa.LocalityNone}, + {a: llssa.ThreadLocal, want: llssa.ThreadLocal}, + {b: llssa.GoroutineLocal, want: llssa.GoroutineLocal}, + {a: llssa.ThreadLocal, b: llssa.ThreadLocal, want: llssa.ThreadLocal}, + {a: llssa.ThreadLocal, b: llssa.GoroutineLocal, err: true}, + } + for _, test := range tests { + got, _, err := mergeLocality(nil, test.a, token.NoPos, test.b, token.NoPos) + if (err != nil) != test.err || got != test.want { + t.Fatalf("mergeLocality(%v, %v) = %v, %v", test.a, test.b, got, err) + } + } +} + +func TestLocalityDirectiveRejectsEmbeddedTarget(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) + } + pkg, err := (&types.Config{}).Check("example.com/locality", fset, []*ast.File{file}, nil) + if err != nil { + t.Fatal(err) + } + prog := ssatest.NewProgram(t, nil) + prog.Target().Target = "rp2040" + err = ParsePkgSyntax(prog, fset, pkg, []*ast.File{file}) + if err == nil || !strings.Contains(err.Error(), `not supported for target "rp2040"`) { + t.Fatalf("ParsePkgSyntax error = %v", err) + } +} diff --git a/internal/build/build.go b/internal/build/build.go index e561ee1c2e..c8da7e7ad3 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -353,11 +353,19 @@ func Do(args []string, conf *Config) ([]Package, error) { return prog.TypeSizes(sizes) } dedup := packages.NewDeduper() + var syntaxErr error + var syntaxErrMu sync.Mutex 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 { + syntaxErrMu.Lock() + if syntaxErr == nil { + syntaxErr = err + } + syntaxErrMu.Unlock() + } }) if patterns == nil { @@ -380,6 +388,9 @@ func Do(args []string, conf *Config) ([]Package, error) { if err != nil { return nil, err } + if syntaxErr != nil { + return nil, syntaxErr + } mode := conf.Mode if mode == ModeTest { initial, err = filterTestPackages(initial, conf.OutFile) @@ -410,6 +421,9 @@ func Do(args []string, conf *Config) ([]Package, error) { if err != nil { return nil, err } + if syntaxErr != nil { + return nil, syntaxErr + } prog.SetRuntime(func() *types.Package { return altPkgs[0].Types @@ -417,7 +431,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 @@ -1646,13 +1662,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 == 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 c8ce08e9e6..ba86dda902 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -9,6 +9,7 @@ import ( "go/ast" "go/parser" "go/token" + "go/types" "io" "os" "os/exec" @@ -17,6 +18,7 @@ import ( "strings" "testing" + "github.com/goplus/llgo/cl" "github.com/goplus/llgo/internal/buildenv" "github.com/goplus/llgo/internal/lto" "github.com/goplus/llgo/internal/mockable" @@ -409,7 +411,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 @@ -420,10 +422,10 @@ 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) } diff --git a/internal/build/collect.go b/internal/build/collect.go index dd30daf72b..d005d9d4f0 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -28,6 +28,7 @@ import ( "github.com/goplus/llgo/internal/env" "github.com/goplus/llgo/internal/packages" + llssa "github.com/goplus/llgo/ssa" gopackages "golang.org/x/tools/go/packages" ) @@ -354,6 +355,11 @@ func (c *context) tryLoadFromCache(pkg *aPackage) bool { if err != nil { return false } + if len(meta.Declarations) != 0 { + if c.prog == nil || !mergeCachedDeclarations(c.prog, pkg.PkgPath, meta.Declarations) { + return false + } + } // Use the .a archive directly for linking (no extraction needed) pkg.ArchiveFile = paths.Archive @@ -375,6 +381,7 @@ func parseManifestMetadata(content string) (*cacheArchiveMetadata, error) { meta.LinkArgs = append([]string(nil), data.Metadata.LinkArgs...) meta.NeedRt = data.Metadata.NeedRt meta.NeedPyInit = data.Metadata.NeedPyInit + meta.Declarations = append([]declarationMetadata(nil), data.Metadata.Declarations...) } return meta, nil } @@ -425,9 +432,121 @@ func parseManifestMetadataLegacy(content string, meta *cacheArchiveMetadata) (*c // cacheArchiveMetadata holds metadata about a cached archive. type cacheArchiveMetadata struct { - LinkArgs []string - NeedRt bool - NeedPyInit bool + LinkArgs []string + NeedRt bool + NeedPyInit bool + Declarations []declarationMetadata +} + +func declarationMetadataFor(decls map[string]llssa.DeclInfo) []declarationMetadata { + if len(decls) == 0 { + return nil + } + names := make([]string, 0, len(decls)) + for name := range decls { + names = append(names, name) + } + sort.Strings(names) + ret := make([]declarationMetadata, 0, len(names)) + for _, name := range names { + info := decls[name] + ret = append(ret, declarationMetadata{ + Name: name, + Linkname: info.Linkname, + Background: declarationBackgroundName(info.Background), + Locality: declarationLocalityName(info.Locality), + HasInitializer: info.HasInitializer, + InitFunc: info.InitFunc, + EnsureFunc: info.EnsureFunc, + }) + } + return ret +} + +func mergeCachedDeclarations(prog llssa.Program, pkgPath string, decls []declarationMetadata) bool { + prefix := pkgPath + "." + entries := make(map[string]llssa.DeclInfo, len(decls)) + for _, decl := range decls { + if !strings.HasPrefix(decl.Name, prefix) { + return false + } + if _, exists := entries[decl.Name]; exists { + return false + } + background, ok := parseDeclarationBackground(decl.Background) + if !ok { + return false + } + locality, ok := parseDeclarationLocality(decl.Locality) + if !ok { + return false + } + entries[decl.Name] = llssa.DeclInfo{ + Linkname: decl.Linkname, + Background: background, + Locality: locality, + HasInitializer: decl.HasInitializer, + InitFunc: decl.InitFunc, + EnsureFunc: decl.EnsureFunc, + } + } + return prog.MergeDeclInfos(entries) +} + +func declarationBackgroundName(background llssa.Background) string { + switch background { + case 0: + return "" + case llssa.InGo: + return "go" + case llssa.InC: + return "c" + case llssa.InPython: + return "python" + default: + return fmt.Sprintf("invalid:%d", background) + } +} + +func parseDeclarationBackground(name string) (llssa.Background, bool) { + switch name { + case "": + return 0, true + case "go": + return llssa.InGo, true + case "c": + return llssa.InC, true + case "python": + return llssa.InPython, true + default: + return 0, false + } +} + +func declarationLocalityName(locality llssa.Locality) string { + switch locality { + case llssa.LocalityNone: + return "" + case llssa.ThreadLocal: + return "tls" + case llssa.GoroutineLocal: + return "gls" + default: + return fmt.Sprintf("invalid:%d", locality) + } +} + +func parseDeclarationLocality(name string) (llssa.Locality, bool) { + switch name { + case "": + return llssa.LocalityNone, true + case "tls": + return llssa.ThreadLocal, true + case "gls": + return llssa.GoroutineLocal, true + default: + return llssa.LocalityNone, false + } } // saveToCache saves a built package to cache. @@ -483,7 +602,10 @@ func (c *context) saveToCache(pkg *aPackage) error { NeedRt: pkg.NeedRt, NeedPyInit: pkg.NeedPyInit, } - if len(meta.LinkArgs) == 0 && !meta.NeedRt && !meta.NeedPyInit { + if c.prog != nil { + meta.Declarations = declarationMetadataFor(c.prog.PackageDecls(pkg.PkgPath)) + } + if len(meta.LinkArgs) == 0 && !meta.NeedRt && !meta.NeedPyInit && len(meta.Declarations) == 0 { data.Metadata = nil } else { data.Metadata = meta diff --git a/internal/build/collect_test.go b/internal/build/collect_test.go index bfa52ead8c..8a29646d34 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" ) @@ -436,6 +437,96 @@ func TestTryLoadFromCache_NoFingerprint(t *testing.T) { } } +func TestDeclarationMetadataRoundTrip(t *testing.T) { + prog := llssa.NewProgram(nil) + prog.SetLinkname("example.com/p.F", "C.f") + prog.SetTypeBackground("example.com/p.T", llssa.InC) + prog.SetVarLocality("example.com/p.value", llssa.GoroutineLocal, true) + prog.SetLocalInitFunc("example.com/p.value", "example.com/p.__llgo_local_init_0") + prog.SetLocalEnsureFunc("example.com/p.value", "example.com/p.__llgo_local_init_0$ensure") + + encoded := declarationMetadataFor(prog.PackageDecls("example.com/p")) + if len(encoded) != 3 { + t.Fatalf("encoded declarations = %d, want 3", len(encoded)) + } + for i := 1; i < len(encoded); i++ { + if encoded[i-1].Name >= encoded[i].Name { + t.Fatalf("declarations are not sorted: %+v", encoded) + } + } + if got := declarationMetadataFor(nil); got != nil { + t.Fatalf("declarationMetadataFor(nil) = %+v", got) + } + for _, declaration := range encoded { + if declaration.Name == "example.com/p.value" && declaration.Locality != "gls" { + t.Fatalf("encoded locality = %q, want gls", declaration.Locality) + } + if declaration.Name == "example.com/p.T" && declaration.Background != "c" { + t.Fatalf("encoded background = %q, want c", declaration.Background) + } + } + restored := llssa.NewProgram(nil) + if !mergeCachedDeclarations(restored, "example.com/p", encoded) { + t.Fatal("mergeCachedDeclarations rejected matching metadata") + } + if got, ok := restored.DeclInfo("example.com/p.value"); !ok || got.Locality != llssa.GoroutineLocal || got.InitFunc == "" || got.EnsureFunc == "" { + t.Fatalf("restored locality = %+v, %v", got, ok) + } + restored.SetVarLocality("example.com/p.value", llssa.ThreadLocal, true) + if mergeCachedDeclarations(restored, "example.com/p", encoded) { + t.Fatal("mergeCachedDeclarations accepted conflicting source metadata") + } +} + +func TestDeclarationMetadataValidation(t *testing.T) { + valid := declarationMetadata{Name: "example.com/p.value", Background: "go", Locality: "tls"} + tests := []struct { + name string + decls []declarationMetadata + }{ + {name: "wrong package", decls: []declarationMetadata{{Name: "example.com/q.value"}}}, + {name: "duplicate", decls: []declarationMetadata{valid, valid}}, + {name: "background", decls: []declarationMetadata{{Name: valid.Name, Background: "invalid"}}}, + {name: "locality", decls: []declarationMetadata{{Name: valid.Name, Locality: "invalid"}}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + prog := llssa.NewProgram(nil) + if mergeCachedDeclarations(prog, "example.com/p", test.decls) { + t.Fatalf("mergeCachedDeclarations accepted %+v", test.decls) + } + }) + } + + backgrounds := []llssa.Background{0, llssa.InGo, llssa.InC, llssa.InPython} + for _, background := range backgrounds { + name := declarationBackgroundName(background) + if got, ok := parseDeclarationBackground(name); !ok || got != background { + t.Fatalf("background %v round trip = %v, %v", background, got, ok) + } + } + if name := declarationBackgroundName(llssa.Background(99)); name != "invalid:99" { + t.Fatalf("invalid background name = %q", name) + } + if _, ok := parseDeclarationBackground("invalid:99"); ok { + t.Fatal("invalid background parsed successfully") + } + + localities := []llssa.Locality{llssa.LocalityNone, llssa.ThreadLocal, llssa.GoroutineLocal} + for _, locality := range localities { + name := declarationLocalityName(locality) + if got, ok := parseDeclarationLocality(name); !ok || got != locality { + t.Fatalf("locality %v round trip = %v, %v", locality, got, ok) + } + } + if name := declarationLocalityName(llssa.Locality(99)); name != "invalid:99" { + t.Fatalf("invalid locality name = %q", name) + } + if _, ok := parseDeclarationLocality("invalid:99"); ok { + t.Fatal("invalid locality parsed successfully") + } +} + func TestTryLoadFromCache_ForceRebuild(t *testing.T) { td := t.TempDir() oldFunc := cacheRootFunc @@ -606,8 +697,13 @@ func TestSaveToCache_Success(t *testing.T) { cacheRootFunc = func() string { return td } defer func() { cacheRootFunc = oldFunc }() + prog := llssa.NewProgram(nil) + prog.SetVarLocality("example.com/lib.value", llssa.ThreadLocal, true) + prog.SetLocalInitFunc("example.com/lib.value", "example.com/lib.__llgo_local_init_0") + prog.SetLocalEnsureFunc("example.com/lib.value", "example.com/lib.__llgo_local_init_0$ensure") ctx := &context{ conf: &packages.Config{}, + prog: prog, buildConf: &Config{ Goos: "darwin", Goarch: "arm64", @@ -661,14 +757,46 @@ func TestSaveToCache_Success(t *testing.T) { if data.Env.Goos != "darwin" { t.Errorf("manifest should contain original env content") } - if data.Metadata != nil { - t.Errorf("metadata should be empty when no link args/runtime flags") + if data.Metadata == nil || len(data.Metadata.Declarations) != 1 { + t.Fatalf("cached declaration metadata = %+v", data.Metadata) } // Check archive exists if _, err := os.Stat(paths.Archive); err != nil { t.Errorf("archive should exist: %v", err) } + + newCachedPackage := func() *aPackage { + return &aPackage{ + Package: &packages.Package{PkgPath: "example.com/lib", Name: "lib"}, + Fingerprint: "def456", + } + } + nilProgCtx := &context{ + conf: &packages.Config{}, + buildConf: ctx.buildConf, + crossCompile: ctx.crossCompile, + cacheManager: cm, + } + if nilProgCtx.tryLoadFromCache(newCachedPackage()) { + t.Fatal("cache metadata loaded without a compiler program") + } + + restored := llssa.NewProgram(nil) + loadCtx := &context{ + conf: &packages.Config{}, + prog: restored, + buildConf: ctx.buildConf, + crossCompile: ctx.crossCompile, + cacheManager: cm, + } + loaded := newCachedPackage() + if !loadCtx.tryLoadFromCache(loaded) || !loaded.CacheHit { + t.Fatal("cache entry with declaration metadata was not loaded") + } + if got, ok := restored.DeclInfo("example.com/lib.value"); !ok || got.Locality != llssa.ThreadLocal || got.InitFunc == "" || got.EnsureFunc == "" { + t.Fatalf("restored declaration metadata = %+v, %v", got, ok) + } } func TestGetLLVMVersion(t *testing.T) { diff --git a/internal/build/fingerprint.go b/internal/build/fingerprint.go index f99469d682..eef52bea67 100644 --- a/internal/build/fingerprint.go +++ b/internal/build/fingerprint.go @@ -37,9 +37,20 @@ type depEntry struct { // manifestMetadata stores metadata produced during build but not part of the fingerprint. type manifestMetadata struct { - LinkArgs []string `yaml:"link_args,omitempty"` - NeedRt bool `yaml:"need_rt,omitempty"` - NeedPyInit bool `yaml:"need_py_init,omitempty"` + LinkArgs []string `yaml:"link_args,omitempty"` + NeedRt bool `yaml:"need_rt,omitempty"` + NeedPyInit bool `yaml:"need_py_init,omitempty"` + Declarations []declarationMetadata `yaml:"declarations,omitempty"` +} + +type declarationMetadata struct { + Name string `yaml:"name"` + Linkname string `yaml:"linkname,omitempty"` + Background string `yaml:"background,omitempty"` + Locality string `yaml:"locality,omitempty"` + HasInitializer bool `yaml:"has_initializer,omitempty"` + InitFunc string `yaml:"init_func,omitempty"` + EnsureFunc string `yaml:"ensure_func,omitempty"` } // manifestData is the structured representation of manifest content. diff --git a/runtime/internal/runtime/g_tls.go b/runtime/internal/runtime/g_tls.go index c2aa52921c..289941c7ba 100644 --- a/runtime/internal/runtime/g_tls.go +++ b/runtime/internal/runtime/g_tls.go @@ -20,17 +20,18 @@ package runtime import ( "unsafe" - - "github.com/goplus/llgo/runtime/internal/clite/tls" ) -var gTLS = tls.Alloc[g](nil) +// currentG locates the goroutine state for the current OS thread. It is +// thread-local rather than goroutine-local because getg bootstraps access to +// the goroutine object itself. +// +//llgo:tls +var currentG g // getg returns the state for the goroutine pinned to the current OS thread. -// The TLS slot is created lazily so threads that never enter Go code pay no -// per-goroutine allocation cost. func getg() *g { - return gTLS.Local() + return ¤tG } func getPanic(gp *g) unsafe.Pointer { diff --git a/runtime/internal/runtime/local_root_gc.go b/runtime/internal/runtime/local_root_gc.go new file mode 100644 index 0000000000..0de494530a --- /dev/null +++ b/runtime/internal/runtime/local_root_gc.go @@ -0,0 +1,97 @@ +//go:build llgo && !baremetal && !nogc + +/* + * 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" + + c "github.com/goplus/llgo/runtime/internal/clite" + "github.com/goplus/llgo/runtime/internal/clite/bdwgc" + "github.com/goplus/llgo/runtime/internal/clite/pthread" +) + +type localRoot struct { + start c.Pointer + end c.Pointer + next *localRoot +} + +var ( + localRootKey pthread.Key + localRootKeyReady bool +) + +func init() { + ensureLocalRootKey() +} + +func ensureLocalRootKey() { + if localRootKeyReady { + return + } + localRootKey = newLocalRootKey() + localRootKeyReady = true +} + +func newLocalRootKey() pthread.Key { + var key pthread.Key + ret := key.Create(pthread.KeyDestructor(destroyLocalRoots)) + if ret != 0 { + c.Fprintf(c.Stderr, c.Str("runtime: pthread_key_create for local roots failed (errno=%d)\n"), ret) + c.Exit(2) + } + return key +} + +// RegisterLocalRoot keeps pointers stored in a native TLS range visible to +// BDWGC until the current thread exits. +func RegisterLocalRoot(start unsafe.Pointer, size uintptr) { + if size == 0 { + return + } + ensureLocalRootKey() + addr := uintptr(start) + if addr > ^uintptr(0)-size { + panic("runtime: local root range overflow") + } + root := (*localRoot)(c.Calloc(1, unsafe.Sizeof(localRoot{}))) + if root == nil { + panic("runtime: failed to allocate local root") + } + root.start = c.Pointer(start) + root.end = c.Pointer(unsafe.Pointer(addr + size)) + root.next = (*localRoot)(unsafe.Pointer(localRootKey.Get())) + bdwgc.AddRoots(root.start, root.end) + if ret := localRootKey.Set(c.Pointer(unsafe.Pointer(root))); ret != 0 { + bdwgc.RemoveRoots(root.start, root.end) + c.Free(unsafe.Pointer(root)) + c.Fprintf(c.Stderr, c.Str("runtime: pthread_setspecific for local roots failed (errno=%d)\n"), ret) + c.Exit(2) + } +} + +func destroyLocalRoots(ptr c.Pointer) { + for root := (*localRoot)(unsafe.Pointer(ptr)); root != nil; { + next := root.next + bdwgc.RemoveRoots(root.start, root.end) + *root = localRoot{} + c.Free(unsafe.Pointer(root)) + root = next + } +} diff --git a/runtime/internal/runtime/local_root_nogc.go b/runtime/internal/runtime/local_root_nogc.go new file mode 100644 index 0000000000..ff853af6d2 --- /dev/null +++ b/runtime/internal/runtime/local_root_nogc.go @@ -0,0 +1,23 @@ +//go:build llgo && !baremetal && nogc + +/* + * 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" + +func RegisterLocalRoot(start unsafe.Pointer, size uintptr) {} diff --git a/runtime/internal/runtime/local_root_stub.go b/runtime/internal/runtime/local_root_stub.go new file mode 100644 index 0000000000..48995abc6e --- /dev/null +++ b/runtime/internal/runtime/local_root_stub.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 + +import "unsafe" + +func RegisterLocalRoot(start unsafe.Pointer, size uintptr) {} 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/package.go b/ssa/package.go index 5b0e9dd411..f29af83c59 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -23,6 +23,8 @@ import ( "log" "runtime" "strconv" + "strings" + "sync" "unsafe" "github.com/goplus/llgo/internal/env" @@ -223,7 +225,7 @@ type aProgram struct { printfTy *types.Signature paramObjPtr_ *types.Var - linkname map[string]string // pkgPath.nameInPkg => linkname + decls *declarationInfos abiSymbol map[string]*AbiSymbol // abi symbol name => AbiSymbol ptrSize int @@ -306,11 +308,12 @@ func NewProgram(target *Target) Program { ctx.Finalize() */ is32Bits := (td.PointerSize() == 4 || is32Bits(target.GOARCH)) + decls := newDeclarationInfos() prog := &aProgram{ - ctx: ctx, gocvt: newGoTypes(), + ctx: ctx, gocvt: newGoTypes(decls), decls: decls, 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), abiSymbol: make(map[string]*AbiSymbol), + abiSymbol: make(map[string]*AbiSymbol), } prog.abi.Init(uintptr(prog.ptrSize), (*goProgram)(unsafe.Pointer(prog))) return prog @@ -371,16 +374,139 @@ func (p Program) SetRuntime(runtime any) { } func (p Program) SetTypeBackground(fullName string, bg Background) { - p.gocvt.typbg.Store(fullName, bg) + p.decls.update(fullName, func(info *DeclInfo) { + info.Background = bg + }) } func (p Program) SetLinkname(name, link string) { - p.linkname[name] = link + p.decls.update(name, func(info *DeclInfo) { + info.Linkname = link + }) } func (p Program) Linkname(name string) (link string, ok bool) { - link, ok = p.linkname[name] - return + info, ok := p.DeclInfo(name) + return info.Linkname, ok && info.Linkname != "" +} + +// Locality describes which execution context owns a package variable. +type Locality uint8 + +const ( + LocalityNone Locality = iota + ThreadLocal + GoroutineLocal +) + +// DeclInfo contains the compiler directives attached to one Go declaration. +// ThreadLocal and GoroutineLocal remain distinct even while both lower to +// native TLS in the current one-thread-per-goroutine runtime. +type DeclInfo struct { + Linkname string + Background Background + Locality Locality + HasInitializer bool + InitFunc string + EnsureFunc string +} + +type declarationInfos struct { + mu sync.RWMutex + entries map[string]DeclInfo + parsedPackages map[*types.Package]struct{} +} + +func newDeclarationInfos() *declarationInfos { + return &declarationInfos{ + entries: make(map[string]DeclInfo), + parsedPackages: make(map[*types.Package]struct{}), + } +} + +func (p *declarationInfos) update(name string, update func(*DeclInfo)) { + p.mu.Lock() + info := p.entries[name] + update(&info) + p.entries[name] = info + p.mu.Unlock() +} + +func (p *declarationInfos) get(name string) (DeclInfo, bool) { + p.mu.RLock() + info, ok := p.entries[name] + p.mu.RUnlock() + return info, ok +} + +func (p Program) SetVarLocality(name string, locality Locality, hasInitializer bool) { + p.decls.update(name, func(info *DeclInfo) { + info.Locality = locality + info.HasInitializer = hasInitializer + }) +} + +func (p Program) SetLocalInitFunc(name, initFunc string) { + p.decls.update(name, func(info *DeclInfo) { + info.InitFunc = initFunc + }) +} + +func (p Program) SetLocalEnsureFunc(name, ensureFunc string) { + p.decls.update(name, func(info *DeclInfo) { + info.EnsureFunc = ensureFunc + }) +} + +func (p Program) DeclInfo(name string) (DeclInfo, bool) { + return p.decls.get(name) +} + +// PackageSyntaxParsed reports whether declaration directives have already +// been collected for this package object. +func (p Program) PackageSyntaxParsed(pkg *types.Package) bool { + p.decls.mu.RLock() + _, ok := p.decls.parsedPackages[pkg] + p.decls.mu.RUnlock() + return ok +} + +// MarkPackageSyntaxParsed records that declaration directives were collected +// successfully for this package object. +func (p Program) MarkPackageSyntaxParsed(pkg *types.Package) { + p.decls.mu.Lock() + p.decls.parsedPackages[pkg] = struct{}{} + p.decls.mu.Unlock() +} + +// PackageDecls returns a copy of the declaration metadata for pkgPath. +func (p Program) PackageDecls(pkgPath string) map[string]DeclInfo { + prefix := pkgPath + "." + ret := make(map[string]DeclInfo) + p.decls.mu.RLock() + for name, info := range p.decls.entries { + if strings.HasPrefix(name, prefix) { + ret[name] = info + } + } + p.decls.mu.RUnlock() + return ret +} + +// MergeDeclInfos atomically restores declaration metadata from a package cache +// manifest. Existing source metadata must agree with every cached entry. +func (p Program) MergeDeclInfos(cached map[string]DeclInfo) bool { + p.decls.mu.Lock() + defer p.decls.mu.Unlock() + for name, info := range cached { + if current, ok := p.decls.entries[name]; ok && current != info { + return false + } + } + for name, info := range cached { + p.decls.entries[name] = info + } + return true } func (p Program) runtime() *types.Package { @@ -836,6 +962,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 6f1365b057..aaae8c0083 100644 --- a/ssa/ssa_test.go +++ b/ssa/ssa_test.go @@ -1897,6 +1897,82 @@ 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 TestDeclarationInfos(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.SetLinkname(name, "C.value") + prog.SetTypeBackground(name, InC) + prog.SetVarLocality(name, GoroutineLocal, true) + prog.SetLocalInitFunc(name, "example.com/p.initLocal") + prog.SetLocalEnsureFunc(name, "example.com/p.initLocal$ensure") + + if link, ok := prog.Linkname(name); !ok || link != "C.value" { + t.Fatalf("Linkname(%q) = %q, %v", name, link, ok) + } + if link, ok := prog.Linkname("example.com/p.missing"); ok || link != "" { + t.Fatalf("missing Linkname = %q, %v", link, ok) + } + want, ok := prog.DeclInfo(name) + if !ok || want.Background != InC || want.Locality != GoroutineLocal || !want.HasInitializer || want.InitFunc == "" || want.EnsureFunc == "" { + t.Fatalf("DeclInfo(%q) = %+v, %v", name, want, ok) + } + + prog.SetLinkname("example.com/peer.value", "C.peer") + decls := prog.PackageDecls("example.com/p") + if len(decls) != 1 || decls[name] != want { + t.Fatalf("PackageDecls = %+v", decls) + } + delete(decls, name) + if _, ok := prog.DeclInfo(name); !ok { + t.Fatal("mutating PackageDecls changed program metadata") + } + + if !prog.MergeDeclInfos(map[string]DeclInfo{name: want}) { + t.Fatal("MergeDeclInfos rejected matching metadata") + } + newName := "example.com/p.zero" + if !prog.MergeDeclInfos(map[string]DeclInfo{newName: {Locality: ThreadLocal}}) { + t.Fatal("MergeDeclInfos rejected new metadata") + } + if prog.MergeDeclInfos(map[string]DeclInfo{name: {Locality: ThreadLocal}}) { + t.Fatal("MergeDeclInfos accepted conflicting metadata") + } + if got, _ := prog.DeclInfo(name); got != want { + t.Fatalf("conflicting merge changed metadata: %+v", got) + } +} + func TestConst(t *testing.T) { prog := NewProgram(nil) pkg := prog.NewPackage("bar", "foo/bar") @@ -2634,7 +2710,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..befb103846 100644 --- a/ssa/type.go +++ b/ssa/type.go @@ -129,7 +129,7 @@ func (p *goProgram) extraSize(typ types.Type, ptrSize int64) (ret int64) { retry: switch t := typ.(type) { case *types.Named: - if v, ok := p.gocvt.typbg.Load(namedLinkname(t)); ok && v.(Background) == InC { + if v, ok := p.gocvt.decls.get(namedLinkname(t)); ok && v.Background == InC { return 0 } typ = t.Underlying() diff --git a/ssa/type_cvt.go b/ssa/type_cvt.go index bd8ab36ed4..74d32acbf2 100644 --- a/ssa/type_cvt.go +++ b/ssa/type_cvt.go @@ -21,7 +21,6 @@ import ( "go/token" "go/types" "reflect" - "sync" "unsafe" ) @@ -29,12 +28,16 @@ import ( type goTypes struct { typs map[unsafe.Pointer]unsafe.Pointer - typbg sync.Map + decls *declarationInfos } -func newGoTypes() goTypes { +func newGoTypes(withDecls ...*declarationInfos) goTypes { typs := make(map[unsafe.Pointer]unsafe.Pointer) - return goTypes{typs: typs} + decls := newDeclarationInfos() + if len(withDecls) != 0 { + decls = withDecls[0] + } + return goTypes{typs: typs, decls: decls} } type Background int @@ -101,7 +104,7 @@ func (p goTypes) cvtType(typ types.Type) (raw types.Type, cvt bool) { } return p.cvtStruct(t) case *types.Named: - if v, ok := p.typbg.Load(namedLinkname(t)); ok && v.(Background) == InC { + if v, ok := p.decls.get(namedLinkname(t)); ok && v.Background == InC { break } return p.cvtNamed(t)