diff --git a/cl/caller_tracking_precompute_test.go b/cl/caller_tracking_precompute_test.go new file mode 100644 index 0000000000..921d0a1af9 --- /dev/null +++ b/cl/caller_tracking_precompute_test.go @@ -0,0 +1,79 @@ +//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 cl + +import ( + "sync" + "testing" + + gossa "golang.org/x/tools/go/ssa" +) + +func TestCallerTrackingPrecomputeFreezesConcurrentReads(t *testing.T) { + var nilTracking *CallerTracking + nilTracking.Precompute(nil) + dep, root := buildCallerFrameSSAProgram(t, + "example.com/dep", `package dep +import "runtime" +func Where() { runtime.Caller(0) } +`, + "example.com/root", `package root +import "example.com/dep" +func Logs() { dep.Where() } +`) + tracking := NewCallerTracking() + tracking.Precompute([]*gossa.Package{root}) + tracking.Precompute(nil) + if !tracking.frozen { + t.Fatal("CallerTracking was not frozen after precomputation") + } + if !runtimeCallerBaseSet(tracking, dep)[dep.Func("Where")] { + t.Fatal("precomputed base set lost runtime caller function") + } + if !runtimeCallerFuncSet(tracking, root)[root.Func("Logs")] { + t.Fatal("precomputed extended set lost cross-package caller") + } + + var wg sync.WaitGroup + errs := make(chan struct{}, 32) + for range 32 { + wg.Add(1) + go func() { + defer wg.Done() + if !runtimeCallerBaseSet(tracking, dep)[dep.Func("Where")] || + !runtimeCallerFuncSet(tracking, root)[root.Func("Logs")] { + errs <- struct{}{} + } + }() + } + wg.Wait() + close(errs) + if len(errs) != 0 { + t.Fatal("concurrent read lost precomputed caller tracking data") + } + + delete(tracking.base, dep) + if got := runtimeCallerBaseSet(tracking, dep); got != nil { + t.Fatalf("frozen base lookup for unknown package = %v, want nil", got) + } + delete(tracking.extended, root) + if got := runtimeCallerFuncSet(tracking, root); got != nil { + t.Fatalf("frozen extended lookup for unknown package = %v, want nil", got) + } +} diff --git a/cl/import.go b/cl/import.go index 728d162049..fad661b0dd 100644 --- a/cl/import.go +++ b/cl/import.go @@ -176,20 +176,14 @@ start: syms.initLinknames(p) } -func (p *context) initFiles(pkgPath string, files []*ast.File, cPkg bool) { +func (p *context) initFiles(pkgPath string, files []*ast.File, _ bool) { for _, file := range files { for _, decl := range file.Decls { switch decl := decl.(type) { case *ast.FuncDecl: - fullName, inPkgName := astFuncName(pkgPath, decl) - p.processNoInterfaceByDoc(decl.Doc, fullName) - if !p.processLinknameByDoc(decl.Doc, fullName, inPkgName, false, true) && cPkg { - // package C (https://github.com/goplus/llgo/issues/1165) - if decl.Recv == nil && token.IsExported(inPkgName) { - exportName := strings.TrimPrefix(inPkgName, "X") - p.prog.SetLinkname(fullName, exportName) - p.pkg.SetExport(fullName, exportName) - } + fullName, _ := astFuncName(pkgPath, decl) + if exportName, ok := p.prog.PackageExport(fullName); ok { + p.pkg.SetExport(fullName, exportName) } case *ast.GenDecl: switch decl.Tok { @@ -197,7 +191,10 @@ func (p *context) initFiles(pkgPath string, files []*ast.File, cPkg bool) { 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) + fullName := pkgPath + "." + inPkgName + if exportName, ok := p.prog.PackageExport(fullName); ok { + p.pkg.SetExport(fullName, exportName) + } } } case token.CONST: @@ -278,7 +275,7 @@ func (p *context) collectSkip(line string, prefix int) { // collectDeclarationDirectives caches source metadata needed after the syntax // pass. funcPos is token.NoPos for non-function declarations. -func collectDeclarationDirectives(prog llssa.Program, fset *token.FileSet, doc *ast.CommentGroup, fullName, inPkgName string, funcPos token.Pos) { +func collectDeclarationDirectives(prog llssa.Program, fset *token.FileSet, doc *ast.CommentGroup, fullName, inPkgName string, funcPos token.Pos, options Options) (bool, error) { directives := directive.ParseGroup(doc) linkCollected := false hasClosureEnv := false @@ -294,6 +291,16 @@ func collectDeclarationDirectives(prog llssa.Program, fset *token.FileSet, doc * prog.SetLinkname(fullName, strings.Join(fields[1:], " ")) linkCollected = true } + case "export": + if linkCollected || item.Args == "" { + continue + } + if item.Args != inPkgName && !options.ExportRename { + return false, fmt.Errorf("export comment has wrong name %q", item.Args) + } + prog.SetLinkname(fullName, item.Args) + prog.SetPackageExport(fullName, item.Args) + linkCollected = true case "llgo:env": if funcPos.IsValid() { hasClosureEnv = true @@ -303,6 +310,7 @@ func collectDeclarationDirectives(prog llssa.Program, fset *token.FileSet, doc * if hasClosureEnv { prog.SetClosureEnvDirective(fset, fullName, funcPos) } + return linkCollected, nil } func (p *context) processLinknameByDoc(doc *ast.CommentGroup, fullName, inPkgName string, isVar, allowExport bool) bool { @@ -766,16 +774,21 @@ func (p *context) initPyModule() { } // 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. +// creation using the legacy frontend options. func ParsePkgSyntax(prog llssa.Program, fset *token.FileSet, pkg *types.Package, files []*ast.File) error { + return ParsePkgSyntaxWithOptions(prog, fset, pkg, files, legacyOptions()) +} + +// ParsePkgSyntaxWithOptions collects all Program-side declaration metadata. +// LLVM Package effects such as preserving //export symbols are applied later. +func ParsePkgSyntaxWithOptions(prog llssa.Program, fset *token.FileSet, pkg *types.Package, files []*ast.File, options Options) error { if pkg == nil { return nil } if prog.PackageSyntaxParsed(pkg) { return nil } - ctx := &context{prog: prog} + ctx := &context{prog: prog, options: options, optionsSet: true} pkgPath := llssa.PathOf(pkg) for _, file := range files { for _, decl := range file.Decls { @@ -788,14 +801,24 @@ func ParsePkgSyntax(prog llssa.Program, fset *token.FileSet, pkg *types.Package, return err } fullName, inPkgName := astFuncName(pkgPath, decl) - collectDeclarationDirectives(prog, fset, decl.Doc, fullName, inPkgName, decl.Pos()) + hasLinkname, err := collectDeclarationDirectives(prog, fset, decl.Doc, fullName, inPkgName, decl.Pos(), options) + if err != nil { + return err + } + if !hasLinkname && pkg.Name() == "C" && decl.Recv == nil && token.IsExported(inPkgName) { + exportName := strings.TrimPrefix(inPkgName, "X") + prog.SetLinkname(fullName, exportName) + prog.SetPackageExport(fullName, exportName) + } ctx.processNoInterfaceByDoc(decl.Doc, fullName) case *ast.GenDecl: 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 - collectDeclarationDirectives(prog, fset, decl.Doc, pkgPath+"."+inPkgName, inPkgName, token.NoPos) + if _, err := collectDeclarationDirectives(prog, fset, decl.Doc, pkgPath+"."+inPkgName, inPkgName, token.NoPos, options); err != nil { + return err + } } } vars, err := locality.ScanPackageVar(fset, decl) diff --git a/cl/import_coverage_test.go b/cl/import_coverage_test.go index ce11dbf1b8..46a95d1932 100644 --- a/cl/import_coverage_test.go +++ b/cl/import_coverage_test.go @@ -251,12 +251,60 @@ func TestParsePkgSyntaxCollectsLinknames(t *testing.T) { }) } prog := llssa.NewProgram(nil) - collectDeclarationDirectives(prog, nil, &ast.CommentGroup{List: []*ast.Comment{{Text: "//go:linkname Other C.other"}}}, llssa.PkgRuntime+".Sigsetjmp", "Sigsetjmp", token.NoPos) + collectDeclarationDirectives(prog, nil, &ast.CommentGroup{List: []*ast.Comment{{Text: "//go:linkname Other C.other"}}}, llssa.PkgRuntime+".Sigsetjmp", "Sigsetjmp", token.NoPos, Options{}) if _, ok := prog.Linkname(llssa.PkgRuntime + ".Sigsetjmp"); ok { t.Fatal("mismatched linkname was collected") } } +func TestParsePkgSyntaxCollectsExportsBeforeLowering(t *testing.T) { + tests := []struct { + name string + pkgName string + declaration string + exportRename bool + want string + wantErr string + }{ + {name: "same name", pkgName: "p", declaration: "//export Entry\nfunc Entry() {}", want: "Entry"}, + {name: "target rename", pkgName: "p", declaration: "//export irq_handler\nfunc Entry() {}", exportRename: true, want: "irq_handler"}, + {name: "invalid rename", pkgName: "p", declaration: "//export irq_handler\nfunc Entry() {}", wantErr: "wrong name"}, + {name: "C default", pkgName: "C", declaration: "func Xmalloc() {}", want: "malloc"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "p.go", "package "+test.pkgName+"\n"+test.declaration+"\n", parser.ParseComments) + if err != nil { + t.Fatal(err) + } + prog := llssa.NewProgram(nil) + defer prog.Dispose() + pkg := types.NewPackage("example.com/"+test.pkgName, test.pkgName) + err = ParsePkgSyntaxWithOptions(prog, fset, pkg, []*ast.File{file}, Options{ExportRename: test.exportRename}) + if test.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), test.wantErr) { + t.Fatalf("ParsePkgSyntaxWithOptions error = %v, want %q", err, test.wantErr) + } + return + } + if err != nil { + t.Fatal(err) + } + fullName := pkg.Path() + ".Entry" + if test.pkgName == "C" { + fullName = pkg.Path() + ".Xmalloc" + } + if link, ok := prog.Linkname(fullName); !ok || link != test.want { + t.Fatalf("Linkname(%q) = (%q, %v), want (%q, true)", fullName, link, ok, test.want) + } + if export, ok := prog.PackageExport(fullName); !ok || export != test.want { + t.Fatalf("PackageExport(%q) = (%q, %v), want (%q, true)", fullName, export, ok, test.want) + } + }) + } +} + func TestParsePkgSyntaxCollectsClosureEnvDirectives(t *testing.T) { const src = `package p //go:linkname env C.old @@ -303,7 +351,7 @@ func TestCollectDeclarationDirectivesIgnoresOtherDirectives(t *testing.T) { {Text: "//llgo:tls"}, }} const fullName = "example.com/p.Value" - collectDeclarationDirectives(prog, nil, doc, fullName, "Value", token.NoPos) + collectDeclarationDirectives(prog, nil, doc, fullName, "Value", token.NoPos, Options{}) if _, ok := prog.Linkname(fullName); ok { t.Fatal("non-link directives installed a linkname") } diff --git a/cl/instr.go b/cl/instr.go index 4b52624f1b..0f5a04a5a5 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -25,6 +25,7 @@ import ( "log" "os" "regexp" + "sort" "strings" "golang.org/x/tools/go/ssa" @@ -927,6 +928,9 @@ func runtimeCallerFuncSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function if set, ok := c.extended[pkg]; ok { return set } + if c.frozen { + return nil + } base := runtimeCallerBaseSet(c, pkg) out := make(map[*ssa.Function]bool, len(base)) for fn := range base { @@ -980,12 +984,55 @@ func runtimeCallerFuncSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function // queries (criterion 2 below) hit the memoization. It must not outlive // the compilation — the maps are keyed by *ssa.Package with // *ssa.Function values, so anything longer-lived would pin every -// compiled package's go/types and go/ssa graphs. Plain maps are enough: -// packages of one compilation are compiled sequentially (the LLVM -// context is not thread-safe). +// compiled package's go/types and go/ssa graphs. Concurrent drivers call +// Precompute and share only the resulting frozen, read-only maps. type CallerTracking struct { base map[*ssa.Package]map[*ssa.Function]bool extended map[*ssa.Package]map[*ssa.Function]bool + frozen bool +} + +// Precompute resolves every caller-tracking query before backend workers +// start, then freezes the maps for concurrent read-only access. +func (c *CallerTracking) Precompute(pkgs []*ssa.Package) { + if c == nil || c.frozen { + return + } + all := make(map[*ssa.Package]bool) + for _, pkg := range pkgs { + if pkg == nil { + continue + } + all[pkg] = true + if pkg.Prog != nil { + for _, programPkg := range pkg.Prog.AllPackages() { + if programPkg != nil { + all[programPkg] = true + } + } + } + } + ordered := make([]*ssa.Package, 0, len(all)) + for pkg := range all { + ordered = append(ordered, pkg) + } + sort.Slice(ordered, func(i, j int) bool { + left, right := "", "" + if ordered[i].Pkg != nil { + left = ordered[i].Pkg.Path() + } + if ordered[j].Pkg != nil { + right = ordered[j].Pkg.Path() + } + return left < right + }) + for _, pkg := range ordered { + runtimeCallerBaseSet(c, pkg) + } + for _, pkg := range ordered { + runtimeCallerFuncSet(c, pkg) + } + c.frozen = true } // NewCallerTracking creates the caller-tracking memoization for one @@ -1015,6 +1062,9 @@ func runtimeCallerBaseSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function if set, ok := c.base[pkg]; ok { return set } + if c.frozen { + return nil + } set := computeRuntimeCallerBaseSet(pkg) c.base[pkg] = set return set diff --git a/internal/build/backend_program_test.go b/internal/build/backend_program_test.go new file mode 100644 index 0000000000..bdc066123c --- /dev/null +++ b/internal/build/backend_program_test.go @@ -0,0 +1,97 @@ +/* + * 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 build + +import ( + "go/token" + "go/types" + "testing" + + llssa "github.com/goplus/llgo/ssa" +) + +func TestBackendProgramTemplateCreatesIsolatedSessions(t *testing.T) { + conf := &Config{Goos: "linux", Goarch: "amd64"} + template := newBackendProgramTemplate( + &llssa.Target{GOOS: conf.Goos, GOARCH: conf.Goarch}, + conf, + true, + true, + ) + first, err := template.newSession() + if err != nil { + t.Fatal(err) + } + defer first.prog.Dispose() + second, err := template.newSession() + if err != nil { + t.Fatal(err) + } + defer second.prog.Dispose() + if first.transformer == nil || second.transformer == nil { + t.Fatal("backend session missing C ABI transformer") + } + if !first.prog.FuncInfoMetadataEnabled() || !first.prog.FuncInfoSitesEnabled() { + t.Fatal("backend template did not preserve funcinfo configuration") + } + firstModule := first.prog.NewPackage("first", "example.com/first").Module() + secondModule := second.prog.NewPackage("second", "example.com/second").Module() + if firstModule.Context().C == secondModule.Context().C { + t.Fatal("backend sessions share an LLVM context") + } +} + +func TestBackendProgramTemplateOptionalState(t *testing.T) { + conf := &Config{Goos: "linux", Goarch: "amd64", DisableBoundsChecks: true, PthreadStackSize: 4096} + template := newBackendProgramTemplate(nil, conf, false, false) + template.typeSizes = &types.StdSizes{WordSize: 8, MaxAlign: 8} + template.runtimePackage = types.NewPackage(llssa.PkgRuntime, "runtime") + template.pythonPackage = types.NewPackage(llssa.PkgPython, "python") + prog := template.newProgram() + defer prog.Dispose() + if prog.Target() == nil { + t.Fatal("backend program created without a default target") + } + + coordinator := llssa.NewProgram(nil) + defer coordinator.Dispose() + pkg := types.NewPackage("example.com/shared", "shared") + fset := token.NewFileSet() + coordinator.SetLinkname("example.com/shared.Entry", "shared_entry") + coordinator.SetPackageExport("example.com/shared.Entry", "shared_entry") + coordinator.SetClosureEnvDirective(fset, "example.com/shared.Entry", token.Pos(7)) + coordinator.MarkPackageSyntaxParsed(pkg) + template.packageSyntax = coordinator.FreezePackageSyntaxState() + sharedSession, err := template.newSession() + if err != nil { + t.Fatal(err) + } + shared := sharedSession.prog + defer shared.Dispose() + if !shared.PackageSyntaxParsed(pkg) { + t.Fatal("backend Program lost shared parsed-package state") + } + if link, ok := shared.Linkname("example.com/shared.Entry"); !ok || link != "shared_entry" { + t.Fatalf("shared linkname = (%q, %v), want (shared_entry, true)", link, ok) + } + if export, ok := shared.PackageExport("example.com/shared.Entry"); !ok || export != "shared_entry" { + t.Fatalf("shared export = (%q, %v), want (shared_entry, true)", export, ok) + } + if !shared.HasClosureEnvDirective(fset, "example.com/shared.Entry", token.Pos(7)) { + t.Fatal("backend Program lost shared closure environment directive") + } +} diff --git a/internal/build/build.go b/internal/build/build.go index 4e3ae2a499..5e04ff77d0 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -452,8 +452,14 @@ func Build(inv Invocation) ([]Package, error) { llssa.Initialize(llssa.InitAll) }) - prog := llssa.NewProgram(target) - prog.DisableBoundsChecks(conf.DisableBoundsChecks) + funcInfo := conf.Mode != ModeGen && conf.PCLNMode != PCLNNone + backendTemplate := newBackendProgramTemplate( + target, + conf, + funcInfo, + shouldEnablePCLNSites(conf, funcInfo, emitDebugInfo), + ) + prog := backendTemplate.newProgram() if conf.Mode != ModeGen { // ModeGen callers (llgen and the golden suites) read LPkg.String() // after Do returns and dispose the program themselves; every other @@ -463,24 +469,17 @@ func Build(inv Invocation) ([]Package, error) { // harness) otherwise accumulate every compile's C++-side memory. defer prog.Dispose() } - prog.EnableGoGlobalDCE(conf.goGlobalDCEEnabled()) - prog.EnableDeadcodeDrop(conf.deadcodeDropEnabled()) - if conf.PthreadStackSize > 0 { - prog.SetPthreadStackSize(uint64(conf.PthreadStackSize)) - } - prog.EnableLTOPluginMarkers(conf.LTOPlugin.Enabled()) - funcInfo := conf.Mode != ModeGen && conf.PCLNMode != PCLNNone - prog.EnableFuncInfoMetadata(funcInfo) - // Site records are inline-asm fragments inside function bodies. Darwin - // DWARF builds avoid them because they disturb LLDB lexical scopes; Linux - // still needs them because its restricted dynamic symbol table cannot - // reconstruct every Go entry PC through dlsym. External mode always needs - // final-PC sites for sidecar construction. - prog.EnableFuncInfoSites(shouldEnablePCLNSites(conf, funcInfo, emitDebugInfo)) + var backendTypeSizes types.Sizes + var backendTypeSizesMu sync.Mutex sizes := func(sizes types.Sizes, compiler, arch string) types.Sizes { if arch == "wasm" { sizes = &types.StdSizes{WordSize: 4, MaxAlign: 4} } + backendTypeSizesMu.Lock() + if backendTypeSizes == nil { + backendTypeSizes = sizes + } + backendTypeSizesMu.Unlock() return prog.TypeSizes(sizes) } dedup := packages.NewDeduper() @@ -502,7 +501,7 @@ func Build(inv Invocation) ([]Package, error) { if llruntime.SkipToBuild(pkg.Path()) { return } - if err := cl.ParsePkgSyntax(prog, cfg.Fset, pkg, files); err != nil { + if err := cl.ParsePkgSyntaxWithOptions(prog, cfg.Fset, pkg, files, frontendOptions); err != nil { recordSyntaxErr(err) } }) @@ -583,12 +582,11 @@ func Build(inv Invocation) ([]Package, error) { return altPkgs[0].Types }) prog.SetPython(func() *types.Package { - return dedup.Check(llssa.PkgPython).Types + if pkg := dedup.Check(llssa.PkgPython); pkg != nil { + return pkg.Types + } + return nil }) - if err := prepareLocalVariables(prog, initial, altPkgs); err != nil { - return nil, err - } - buildMode := ssaBuildMode cabiOptimize := true passOpt := true @@ -602,9 +600,25 @@ func Build(inv Invocation) ([]Package, error) { if !IsOptimizeEnabled() { buildMode |= ssa.NaiveForm } + backendTemplate.cabiOptimize = cabiOptimize progSSA := ssa.NewProgram(initial[0].Fset, buildMode) patches := make(cl.Patches, len(altPkgPaths)) altEntries := registerAltSSAPkgs(progSSA, patches, altPkgs[1:], conf, verbose) + if err := preloadPatchedPackageSyntax(prog, patches, dedup, frontendOptions); err != nil { + return nil, err + } + if err := prepareLocalVariables(prog, initial, altPkgs); err != nil { + return nil, err + } + backendTemplate.typeSizes = backendTypeSizes + backendTemplate.runtimePackage = altPkgs[0].Types + if pkg := dedup.Check(llssa.PkgPython); pkg != nil { + backendTemplate.pythonPackage = pkg.Types + } + backendTemplate.packageSyntax = prog.FreezePackageSyntaxState() + backendTemplate.localities = prog.FreezeLocalityState() + backendTemplate.llvmTarget = export.LLVMTarget + backendTemplate.targetABI = export.TargetABI output := conf.OutFile != "" ctx := &context{conf: cfg, progSSA: progSSA, prog: prog, dedup: dedup, @@ -620,6 +634,7 @@ func Build(inv Invocation) ([]Package, error) { commands: commands, frontendOptions: frontendOptions, cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize), + backend: backendTemplate, } defer ctx.closePackageMetas() defer ctx.closePackageArchiveBuffers() @@ -636,6 +651,7 @@ func Build(inv Invocation) ([]Package, error) { return nil, err } buildSSAPkgs(ctx, append(append(altEntries, pkgEntries...), depEntries...)) + ctx.callerTracking.Precompute(ctx.progSSA.AllPackages()) allPkgs := append([]*aPackage{}, pkgs...) allPkgs = append(allPkgs, depPkgs...) @@ -883,6 +899,7 @@ type context struct { frontendOptions cl.Options cTransformer *cabi.Transformer + backend backendProgramTemplate testFail bool @@ -915,6 +932,116 @@ func (c *context) closePackageMetas() { } } +// backendProgramTemplate contains immutable build-local inputs. Creating a +// session allocates a new llssa.Program, LLVM context, TargetMachine, and C ABI +// transformer; no LLVM-owned state is shared between sessions. +type backendProgramTemplate struct { + target *llssa.Target + disableBoundsChecks bool + typeSizes types.Sizes + goGlobalDCE bool + deadcodeDrop bool + pthreadStackSize int64 + ltoPluginMarkers bool + funcInfoMetadata bool + funcInfoSites bool + runtimePackage *types.Package + pythonPackage *types.Package + packageSyntax llssa.PackageSyntaxState + localities llssa.LocalityState + llvmTarget string + targetABI string + abiMode cabi.Mode + cabiOptimize bool +} + +type backendSession struct { + prog llssa.Program + transformer *cabi.Transformer +} + +func newBackendProgramTemplate(target *llssa.Target, conf *Config, funcInfoMetadata, funcInfoSites bool) backendProgramTemplate { + var targetCopy *llssa.Target + if target != nil { + copy := *target + targetCopy = © + } + return backendProgramTemplate{ + target: targetCopy, + disableBoundsChecks: conf.DisableBoundsChecks, + goGlobalDCE: conf.goGlobalDCEEnabled(), + deadcodeDrop: conf.deadcodeDropEnabled(), + pthreadStackSize: conf.PthreadStackSize, + ltoPluginMarkers: conf.LTOPlugin.Enabled(), + funcInfoMetadata: funcInfoMetadata, + funcInfoSites: funcInfoSites, + abiMode: conf.AbiMode, + } +} + +func (t backendProgramTemplate) newProgram() llssa.Program { + var target *llssa.Target + if t.target != nil { + copy := *t.target + target = © + } + prog := llssa.NewProgram(target) + prog.DisableBoundsChecks(t.disableBoundsChecks) + if t.typeSizes != nil { + prog.TypeSizes(t.typeSizes) + } + prog.EnableGoGlobalDCE(t.goGlobalDCE) + prog.EnableDeadcodeDrop(t.deadcodeDrop) + if t.pthreadStackSize > 0 { + prog.SetPthreadStackSize(uint64(t.pthreadStackSize)) + } + prog.EnableLTOPluginMarkers(t.ltoPluginMarkers) + prog.EnableFuncInfoMetadata(t.funcInfoMetadata) + prog.EnableFuncInfoSites(t.funcInfoSites) + if t.runtimePackage != nil { + prog.SetRuntime(t.runtimePackage) + } + if t.pythonPackage != nil { + prog.SetPython(t.pythonPackage) + } + return prog +} + +func (t backendProgramTemplate) newSession() (backendSession, error) { + prog := t.newProgram() + prog.UsePackageSyntaxState(t.packageSyntax) + prog.UseLocalityState(t.localities) + return backendSession{ + prog: prog, + transformer: cabi.NewTransformer(prog, t.llvmTarget, t.targetABI, t.abiMode, t.cabiOptimize), + }, nil +} + +func preloadPatchedPackageSyntax(prog llssa.Program, patches cl.Patches, dedup packages.Deduper, options cl.Options) error { + paths := make([]string, 0, len(patches)) + for pkgPath := range patches { + paths = append(paths, pkgPath) + } + slices.Sort(paths) + for _, pkgPath := range paths { + patch := patches[pkgPath] + alt := dedup.Check(altPkgPathPrefix + pkgPath) + if alt == nil || len(alt.Syntax) == 0 || patch.Types == nil { + continue + } + fset := alt.Fset + files := slices.Clone(alt.Syntax) + if original := dedup.Check(pkgPath); original != nil { + fset = original.Fset + files = append(slices.Clone(original.Syntax), files...) + } + if err := cl.ParsePkgSyntaxWithOptions(prog, fset, patch.Types, files, options); err != nil { + return err + } + } + return nil +} + func (c *context) compiler() *clang.Cmd { config := clang.NewConfig( c.crossCompile.CC, diff --git a/ssa/locality.go b/ssa/locality.go index 0e9b41e6cd..deb6f6452f 100644 --- a/ssa/locality.go +++ b/ssa/locality.go @@ -51,14 +51,20 @@ type VariableLocality struct { } type localityInfos struct { - mu sync.RWMutex + mu sync.RWMutex + frozen bool // entries and ownerlessEntries retain the canonical-only compatibility // view. Production declaration handling uses declarationEntries instead. entries map[string]VariableLocality ownerlessEntries map[string]VariableLocality declarationEntries map[string]map[string]VariableLocality activePackages map[string]struct{} - parsedPackages map[*types.Package]struct{} +} + +// LocalityState is an immutable handle to Go-side locality metadata. It +// contains no LLVM objects and can be shared by independent backend Programs. +type LocalityState struct { + infos *localityInfos } func newLocalityInfos() *localityInfos { @@ -67,23 +73,43 @@ func newLocalityInfos() *localityInfos { ownerlessEntries: make(map[string]VariableLocality), declarationEntries: make(map[string]map[string]VariableLocality), activePackages: make(map[string]struct{}), - parsedPackages: make(map[*types.Package]struct{}), } } +// FreezeLocalityState freezes p's locality metadata and returns it without +// copying. Package syntax parse markers remain local to each Program. +func (p Program) FreezeLocalityState() LocalityState { + p.localities.mu.Lock() + p.localities.frozen = true + p.localities.mu.Unlock() + return LocalityState{infos: p.localities} +} + +// UseLocalityState replaces p's locality metadata with shared immutable state. +func (p Program) UseLocalityState(state LocalityState) { + if state.infos == nil { + state = LocalityState{infos: newLocalityInfos()} + state.infos.frozen = true + } + p.localities = state.infos +} + func (p *localityInfos) update(name string, update func(*VariableLocality)) { p.mu.Lock() + defer p.mu.Unlock() + p.assertMutable() info := p.entries[name] update(&info) p.entries[name] = info ownerless := p.ownerlessEntries[name] update(&ownerless) p.ownerlessEntries[name] = ownerless - p.mu.Unlock() } func (p *localityInfos) updateFor(pkg *types.Package, name string, update func(*VariableLocality)) { p.mu.Lock() + defer p.mu.Unlock() + p.assertMutable() entries := p.declarationEntries[name] if entries == nil { entries = make(map[string]VariableLocality) @@ -94,7 +120,12 @@ func (p *localityInfos) updateFor(pkg *types.Package, name string, update func(* update(&info) entries[owner] = info p.entries[name] = info - p.mu.Unlock() +} + +func (p *localityInfos) assertMutable() { + if p.frozen { + panic("cannot modify frozen locality state") + } } func (p Program) SetLocalityInfo(name string, info LocalityInfo) { @@ -116,7 +147,14 @@ func (p Program) DeclareLocality(pkg *types.Package, name string, info LocalityI fullName := FullName(pkg, name) owner := pkg.Path() p.localities.mu.Lock() + defer p.localities.mu.Unlock() entries := p.localities.declarationEntries[fullName] + if entries != nil { + if _, exists := entries[owner]; exists { + return + } + } + p.localities.assertMutable() if entries == nil { entries = make(map[string]VariableLocality) p.localities.declarationEntries[fullName] = entries @@ -126,7 +164,6 @@ func (p Program) DeclareLocality(pkg *types.Package, name string, info LocalityI entries[owner] = current p.localities.entries[fullName] = current } - p.localities.mu.Unlock() } func (p Program) SetLocalStorage(name string, storage LocalStorage) { @@ -148,8 +185,9 @@ func (p Program) ActivateLocalitiesFor(pkg *types.Package) { return } p.localities.mu.Lock() + defer p.localities.mu.Unlock() + p.localities.assertMutable() p.localities.activePackages[pkg.Path()] = struct{}{} - p.localities.mu.Unlock() } // VariableLocality returns the legacy canonical-only metadata view. Its result @@ -293,12 +331,11 @@ func (p Program) validateLocalities(pkgPath string, packageEntries map[string]Va if len(localNames) == 0 { return nil } - p.linknameMu.RLock() - links := make(map[string]string, len(p.linkname)) - for name, target := range p.linkname { + rawLinks := p.packageSyntax.linknamesSnapshot() + links := make(map[string]string, len(rawLinks)) + for name, target := range rawLinks { 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 @@ -330,16 +367,11 @@ func linknameReachesLocal(name string, links map[string]string, localNames map[s } func (p Program) PackageSyntaxParsed(pkg *types.Package) bool { - p.localities.mu.RLock() - _, ok := p.localities.parsedPackages[pkg] - p.localities.mu.RUnlock() - return ok + return p.packageSyntax.packageParsed(pkg) } func (p Program) MarkPackageSyntaxParsed(pkg *types.Package) { - p.localities.mu.Lock() - p.localities.parsedPackages[pkg] = struct{}{} - p.localities.mu.Unlock() + p.packageSyntax.markPackageParsed(pkg) } // PackageLocalities returns the legacy canonical-only metadata view. Its diff --git a/ssa/locality_test.go b/ssa/locality_test.go index 1d6ad63cb5..49ac203c6e 100644 --- a/ssa/locality_test.go +++ b/ssa/locality_test.go @@ -63,6 +63,64 @@ func TestLocalityInfos(t *testing.T) { } } +func TestFrozenLocalityStateIsShared(t *testing.T) { + empty := NewProgram(nil) + defer empty.Dispose() + empty.UseLocalityState(LocalityState{}) + if empty.NeedsLocalContext() { + t.Fatal("zero locality state unexpectedly requires a context") + } + + source := NewProgram(nil) + defer source.Dispose() + pkg := types.NewPackage("example.com/p", "p") + name := "example.com/p.state" + source.DeclareLocality(pkg, "state", LocalityInfo{Locality: GoroutineLocal, HasInitializer: true}) + source.SetLocalStorageFor(pkg, name, LocalStoragePackage) + source.ActivateLocalitiesFor(pkg) + source.MarkPackageSyntaxParsed(pkg) + + state := source.FreezeLocalityState() + dest := NewProgram(nil) + defer dest.Dispose() + dest.UseLocalityState(state) + peer := NewProgram(nil) + defer peer.Dispose() + peer.UseLocalityState(state) + if dest.localities != peer.localities { + t.Fatal("backend Programs did not share locality state") + } + got, ok := dest.VariableLocalityFor(pkg, name) + if !ok || got.Locality != GoroutineLocal || got.LocalStorage != LocalStoragePackage { + t.Fatalf("shared locality = %+v, %v", got, ok) + } + if !dest.NeedsLocalContext() { + t.Fatal("shared locality state lost active metadata") + } + if dest.PackageSyntaxParsed(pkg) { + t.Fatal("package syntax state was shared with locality metadata") + } + + // Re-parsing the same package owner is idempotent and must not overwrite + // prepared metadata in the frozen shared state. + dest.DeclareLocality(pkg, "state", LocalityInfo{Locality: ThreadLocal}) + got, ok = dest.VariableLocalityFor(pkg, name) + if !ok || got.Locality != GoroutineLocal || got.LocalStorage != LocalStoragePackage { + t.Fatalf("idempotent declaration changed shared locality: %+v, %v", got, ok) + } + dest.MarkPackageSyntaxParsed(pkg) + if !dest.PackageSyntaxParsed(pkg) || peer.PackageSyntaxParsed(pkg) { + t.Fatal("package syntax state was not Program-local") + } + + defer func() { + if recovered := recover(); recovered == nil { + t.Fatal("modifying shared locality state did not panic") + } + }() + source.SetLocalStorageFor(pkg, name, LocalStorageNativeTLS) +} + func TestPackageLocalitiesRetainDeclarationOwners(t *testing.T) { prog := NewProgram(nil) std := types.NewPackage("runtime", "runtime") diff --git a/ssa/package.go b/ssa/package.go index 498bbaf942..7d44d216d0 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -23,7 +23,6 @@ import ( "log" "runtime" "strconv" - "sync" "unsafe" "github.com/goplus/llgo/internal/env" @@ -221,13 +220,10 @@ type aProgram struct { printfTy *types.Signature - paramObjPtr_ *types.Var - linknameMu sync.RWMutex - linkname map[string]string // pkgPath.nameInPkg => linkname - closureEnvDirectives sync.Map // closureEnvDirectiveKey => none - localities *localityInfos - noInterface map[string]none // pkgPath.T.method or pkgPath.(*T).method - abiSymbol map[string]*AbiSymbol // abi symbol name => AbiSymbol + paramObjPtr_ *types.Var + packageSyntax *packageSyntaxData + localities *localityInfos + abiSymbol map[string]*AbiSymbol // abi symbol name => AbiSymbol ptrSize int @@ -312,12 +308,13 @@ func NewProgram(target *Target) Program { ctx.Finalize() */ is32Bits := (td.PointerSize() == 4 || is32Bits(target.GOARCH)) + gocvt := newGoTypes() prog := &aProgram{ - ctx: ctx, gocvt: newGoTypes(), + ctx: ctx, gocvt: gocvt, 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), localities: newLocalityInfos(), - noInterface: make(map[string]none), abiSymbol: make(map[string]*AbiSymbol), + packageSyntax: gocvt.packageSyntax, localities: newLocalityInfos(), + abiSymbol: make(map[string]*AbiSymbol), debugInfoOptimized: target.effectiveOptLevel() != optlevel.O0, } prog.abi.Init(uintptr(prog.ptrSize), (*goProgram)(unsafe.Pointer(prog))) @@ -383,7 +380,7 @@ func (p Program) EnableLTOPluginMarkers(enable bool) { } func (p Program) SetNoInterfaceMethod(fullName string) { - p.noInterface[fullName] = none{} + p.packageSyntax.setNoInterface(fullName) } func (p Program) isNoInterfaceMethod(fn *types.Func) bool { @@ -394,7 +391,7 @@ func (p Program) isNoInterfaceMethod(fn *types.Func) bool { if !ok || sig.Recv() == nil { return false } - _, ok = p.noInterface[FuncName(fn.Pkg(), fn.Name(), sig.Recv(), true)] + _, ok = p.packageSyntax.noInterfaceMethod(FuncName(fn.Pkg(), fn.Name(), sig.Recv(), true)) return ok } @@ -410,20 +407,15 @@ func (p Program) SetRuntime(runtime any) { } func (p Program) SetTypeBackground(fullName string, bg Background) { - p.gocvt.typbg.Store(fullName, bg) + p.packageSyntax.setTypeBackground(fullName, bg) } func (p Program) SetLinkname(name, link string) { - p.linknameMu.Lock() - p.linkname[name] = link - p.linknameMu.Unlock() + p.packageSyntax.setLinkname(name, link) } func (p Program) Linkname(name string) (link string, ok bool) { - p.linknameMu.RLock() - link, ok = p.linkname[name] - p.linknameMu.RUnlock() - return + return p.packageSyntax.linknameOf(name) } type closureEnvDirectiveKey struct { @@ -437,15 +429,14 @@ type closureEnvDirectiveKey struct { // than its resolved linker symbol, so aliases retain independent ABI metadata. func (p Program) SetClosureEnvDirective(fset *token.FileSet, name string, pos token.Pos) { key := closureEnvDirectiveKey{fset: fset, name: name, pos: pos} - p.closureEnvDirectives.Store(key, none{}) + p.packageSyntax.setClosureEnvDirective(key) } // HasClosureEnvDirective reports whether a source function declaration has the // cached llgo:env directive. func (p Program) HasClosureEnvDirective(fset *token.FileSet, name string, pos token.Pos) bool { key := closureEnvDirectiveKey{fset: fset, name: name, pos: pos} - _, ok := p.closureEnvDirectives.Load(key) - return ok + return p.packageSyntax.hasClosureEnvDirective(key) } func (p Program) runtime() *types.Package { diff --git a/ssa/package_syntax.go b/ssa/package_syntax.go new file mode 100644 index 0000000000..dcbbb39586 --- /dev/null +++ b/ssa/package_syntax.go @@ -0,0 +1,195 @@ +/* + * 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" + "sync" +) + +// PackageSyntaxState is immutable Go-side metadata collected while package +// syntax is preloaded. It contains no LLVM objects and can be shared directly +// by Programs with independent LLVM contexts. +type PackageSyntaxState struct { + data *packageSyntaxData +} + +type packageSyntaxData struct { + mu sync.RWMutex + frozen bool + linknames map[string]string + exports map[string]string + closureEnvDirectives map[closureEnvDirectiveKey]none + parsedPackages map[*types.Package]struct{} + noInterface map[string]none + typeBackgrounds map[string]Background +} + +func newPackageSyntaxData() *packageSyntaxData { + return &packageSyntaxData{ + linknames: make(map[string]string), + exports: make(map[string]string), + closureEnvDirectives: make(map[closureEnvDirectiveKey]none), + parsedPackages: make(map[*types.Package]struct{}), + noInterface: make(map[string]none), + typeBackgrounds: make(map[string]Background), + } +} + +// FreezePackageSyntaxState freezes p's preloaded package metadata and returns +// the same immutable state for workers to share without copying. +func (p Program) FreezePackageSyntaxState() PackageSyntaxState { + p.packageSyntax.mu.Lock() + p.packageSyntax.frozen = true + p.packageSyntax.mu.Unlock() + return PackageSyntaxState{data: p.packageSyntax} +} + +// UsePackageSyntaxState replaces p's package metadata with shared immutable +// state. A worker must not discover syntax that the coordinator did not preload. +func (p Program) UsePackageSyntaxState(state PackageSyntaxState) { + if state.data == nil { + state.data = newPackageSyntaxData() + state.data.frozen = true + } + p.packageSyntax = state.data + p.gocvt.packageSyntax = state.data +} + +// SetPackageExport records an export directive independently of the LLVM +// Package on which it will later be applied. +func (p Program) SetPackageExport(name, export string) { + p.packageSyntax.setString(p.packageSyntax.exports, name, export, "package export") +} + +// PackageExport returns the export directive collected for name. +func (p Program) PackageExport(name string) (string, bool) { + p.packageSyntax.mu.RLock() + export, ok := p.packageSyntax.exports[name] + p.packageSyntax.mu.RUnlock() + return export, ok +} + +func (p *packageSyntaxData) setLinkname(name, link string) { + p.setString(p.linknames, name, link, "linkname") +} + +func (p *packageSyntaxData) linknameOf(name string) (string, bool) { + p.mu.RLock() + link, ok := p.linknames[name] + p.mu.RUnlock() + return link, ok +} + +func (p *packageSyntaxData) linknamesSnapshot() map[string]string { + p.mu.RLock() + links := make(map[string]string, len(p.linknames)) + copyMap(links, p.linknames) + p.mu.RUnlock() + return links +} + +func (p *packageSyntaxData) setNoInterface(name string) { + p.mu.Lock() + defer p.mu.Unlock() + if _, ok := p.noInterface[name]; ok { + return + } + p.assertMutable("nointerface", name) + p.noInterface[name] = none{} +} + +func (p *packageSyntaxData) noInterfaceMethod(name string) (none, bool) { + p.mu.RLock() + value, ok := p.noInterface[name] + p.mu.RUnlock() + return value, ok +} + +func (p *packageSyntaxData) setClosureEnvDirective(key closureEnvDirectiveKey) { + p.mu.Lock() + defer p.mu.Unlock() + if _, ok := p.closureEnvDirectives[key]; ok { + return + } + p.assertMutable("closure environment directive", key.name) + p.closureEnvDirectives[key] = none{} +} + +func (p *packageSyntaxData) hasClosureEnvDirective(key closureEnvDirectiveKey) bool { + p.mu.RLock() + _, ok := p.closureEnvDirectives[key] + p.mu.RUnlock() + return ok +} + +func (p *packageSyntaxData) markPackageParsed(pkg *types.Package) { + p.mu.Lock() + defer p.mu.Unlock() + if _, ok := p.parsedPackages[pkg]; ok { + return + } + p.assertMutable("parsed package", pkg.Path()) + p.parsedPackages[pkg] = struct{}{} +} + +func (p *packageSyntaxData) packageParsed(pkg *types.Package) bool { + p.mu.RLock() + _, ok := p.parsedPackages[pkg] + p.mu.RUnlock() + return ok +} + +func (p *packageSyntaxData) setTypeBackground(name string, bg Background) { + p.mu.Lock() + defer p.mu.Unlock() + if current, ok := p.typeBackgrounds[name]; ok && current == bg { + return + } + p.assertMutable("type background", name) + p.typeBackgrounds[name] = bg +} + +func (p *packageSyntaxData) typeBackground(name string) (Background, bool) { + p.mu.RLock() + bg, ok := p.typeBackgrounds[name] + p.mu.RUnlock() + return bg, ok +} + +func (p *packageSyntaxData) setString(dst map[string]string, key, value, kind string) { + p.mu.Lock() + defer p.mu.Unlock() + if current, ok := dst[key]; ok && current == value { + return + } + p.assertMutable(kind, key) + dst[key] = value +} + +func (p *packageSyntaxData) assertMutable(kind, name string) { + if p.frozen { + panic(fmt.Sprintf("cannot add %s %q to frozen package syntax state", kind, name)) + } +} + +func copyMap[K comparable, V any](dst, src map[K]V) { + for key, value := range src { + dst[key] = value + } +} diff --git a/ssa/package_syntax_test.go b/ssa/package_syntax_test.go new file mode 100644 index 0000000000..17b652a68c --- /dev/null +++ b/ssa/package_syntax_test.go @@ -0,0 +1,116 @@ +/* + * 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" + "sync" + "testing" +) + +func TestFrozenPackageSyntaxStateIsSharedWithoutWorkerOverlay(t *testing.T) { + source := NewProgram(nil) + defer source.Dispose() + pkg := types.NewPackage("example.com/p", "p") + fset := token.NewFileSet() + const name = "example.com/p.Entry" + source.SetLinkname(name, "entry") + source.SetPackageExport(name, "entry") + source.SetClosureEnvDirective(fset, name, token.Pos(7)) + source.SetNoInterfaceMethod(name) + source.SetTypeBackground("example.com/p.CType", InC) + source.SetClosureEnvDirective(fset, name, token.Pos(7)) + source.SetNoInterfaceMethod(name) + source.SetTypeBackground("example.com/p.CType", InC) + source.MarkPackageSyntaxParsed(pkg) + + state := source.FreezePackageSyntaxState() + first := NewProgram(nil) + defer first.Dispose() + first.UsePackageSyntaxState(state) + second := NewProgram(nil) + defer second.Dispose() + second.UsePackageSyntaxState(state) + if first.packageSyntax != source.packageSyntax || first.packageSyntax != second.packageSyntax { + t.Fatal("backend Programs did not share package syntax state") + } + if !first.PackageSyntaxParsed(pkg) { + t.Fatal("shared state lost parsed package") + } + if link, ok := first.Linkname(name); !ok || link != "entry" { + t.Fatalf("shared linkname = (%q, %v), want (entry, true)", link, ok) + } + if links := first.packageSyntax.linknamesSnapshot(); links[name] != "entry" { + t.Fatalf("shared linkname snapshot = %#v", links) + } + if export, ok := first.PackageExport(name); !ok || export != "entry" { + t.Fatalf("shared export = (%q, %v), want (entry, true)", export, ok) + } + if !first.HasClosureEnvDirective(fset, name, token.Pos(7)) { + t.Fatal("shared state lost closure environment directive") + } + if _, ok := first.packageSyntax.noInterfaceMethod(name); !ok { + t.Fatal("shared state lost nointerface directive") + } + if bg, ok := first.packageSyntax.typeBackground("example.com/p.CType"); !ok || bg != InC { + t.Fatalf("shared type background = (%v, %v), want (InC, true)", bg, ok) + } + + // Repeated lowering-time registration is allowed only when it exactly + // matches metadata that the coordinator already collected. + first.SetLinkname(name, "entry") + first.SetPackageExport(name, "entry") + first.MarkPackageSyntaxParsed(pkg) + var wg sync.WaitGroup + for i := 0; i < 8; i++ { + prog := first + if i%2 != 0 { + prog = second + } + wg.Add(1) + go func(prog Program) { + defer wg.Done() + for range 100 { + prog.SetLinkname(name, "entry") + if link, ok := prog.Linkname(name); !ok || link != "entry" { + t.Errorf("concurrent shared linkname = (%q, %v)", link, ok) + } + } + }(prog) + } + wg.Wait() + + defer func() { + if recovered := recover(); recovered == nil { + t.Fatal("worker added metadata to frozen package syntax state") + } + }() + first.SetLinkname("example.com/p.Missing", "missing") +} + +func TestZeroPackageSyntaxStateIsFrozen(t *testing.T) { + prog := NewProgram(nil) + defer prog.Dispose() + prog.UsePackageSyntaxState(PackageSyntaxState{}) + defer func() { + if recovered := recover(); recovered == nil { + t.Fatal("zero package syntax state remained mutable") + } + }() + prog.MarkPackageSyntaxParsed(types.NewPackage("example.com/missing", "missing")) +} diff --git a/ssa/type.go b/ssa/type.go index c635cf16e9..3217e6649f 100644 --- a/ssa/type.go +++ b/ssa/type.go @@ -130,7 +130,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 bg, ok := p.packageSyntax.typeBackground(namedLinkname(t)); ok && bg == InC { return 0 } typ = t.Underlying() diff --git a/ssa/type_cvt.go b/ssa/type_cvt.go index bd8ab36ed4..01793fb5fb 100644 --- a/ssa/type_cvt.go +++ b/ssa/type_cvt.go @@ -21,20 +21,20 @@ import ( "go/token" "go/types" "reflect" - "sync" "unsafe" ) // ----------------------------------------------------------------------------- type goTypes struct { - typs map[unsafe.Pointer]unsafe.Pointer - typbg sync.Map + typs map[unsafe.Pointer]unsafe.Pointer + packageSyntax *packageSyntaxData } func newGoTypes() goTypes { + packageSyntax := newPackageSyntaxData() typs := make(map[unsafe.Pointer]unsafe.Pointer) - return goTypes{typs: typs} + return goTypes{typs: typs, packageSyntax: packageSyntax} } type Background int @@ -101,7 +101,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 bg, ok := p.packageSyntax.typeBackground(namedLinkname(t)); ok && bg == InC { break } return p.cvtNamed(t)