From 0efd3fa8e2eca43e6b091f10aa2a89d69eb319d6 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Thu, 30 Jul 2026 08:12:34 +0800 Subject: [PATCH 1/4] build: isolate package backend sessions --- cl/caller_tracking_precompute_test.go | 79 ++++++++++ cl/instr.go | 56 ++++++- internal/build/backend_program_test.go | 89 +++++++++++ internal/build/build.go | 204 ++++++++++++++++++++++--- 4 files changed, 408 insertions(+), 20 deletions(-) create mode 100644 cl/caller_tracking_precompute_test.go create mode 100644 internal/build/backend_program_test.go 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/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..dfa9bb125f --- /dev/null +++ b/internal/build/backend_program_test.go @@ -0,0 +1,89 @@ +/* + * 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/types" + "testing" + + "github.com/goplus/llgo/cl" + "github.com/goplus/llgo/internal/packages" + 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") + } + + validTypes := types.NewPackage("example.com/valid", "valid") + valid := &packages.Package{PkgPath: "example.com/valid", Types: validTypes} + duplicate := &packages.Package{PkgPath: "example.com/duplicate", Types: validTypes} + missingTypes := &packages.Package{PkgPath: "example.com/missing"} + illTyped := &packages.Package{PkgPath: "example.com/ill", Types: types.NewPackage("example.com/ill", "ill"), IllTyped: true} + inputs := collectBackendProgramInputs([]*packages.Package{missingTypes, illTyped, valid, duplicate}) + if len(inputs) != 1 || inputs[0].pkg != validTypes { + t.Fatalf("backend inputs = %#v, want one deduplicated package", inputs) + } + + patched := backendProgramTemplate{} + appendPatchedBackendInputs(&patched, cl.Patches{ + "example.com/missing": {}, + "example.com/other": {}, + }, packages.NewDeduper()) + if len(patched.inputs) != 0 { + t.Fatalf("missing patched packages produced inputs: %#v", patched.inputs) + } +} diff --git a/internal/build/build.go b/internal/build/build.go index 4e3ae2a499..58525e85a4 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() @@ -583,11 +582,22 @@ 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 } + backendTemplate.typeSizes = backendTypeSizes + backendTemplate.runtimePackage = altPkgs[0].Types + if pkg := dedup.Check(llssa.PkgPython); pkg != nil { + backendTemplate.pythonPackage = pkg.Types + } + backendTemplate.inputs = collectBackendProgramInputs(initial, altPkgs) + backendTemplate.llvmTarget = export.LLVMTarget + backendTemplate.targetABI = export.TargetABI buildMode := ssaBuildMode cabiOptimize := true @@ -602,9 +612,11 @@ 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) + appendPatchedBackendInputs(&backendTemplate, patches, dedup) output := conf.OutFile != "" ctx := &context{conf: cfg, progSSA: progSSA, prog: prog, dedup: dedup, @@ -620,6 +632,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 +649,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 +897,7 @@ type context struct { frontendOptions cl.Options cTransformer *cabi.Transformer + backend backendProgramTemplate testFail bool @@ -915,6 +930,161 @@ func (c *context) closePackageMetas() { } } +type backendProgramInput struct { + fset *token.FileSet + pkg *types.Package + info *types.Info + files []*ast.File + parseSyntax bool + prepareLocal bool +} + +// backendProgramTemplate contains only 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 + inputs []backendProgramInput + 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() + if err := t.replayProgramState(prog); err != nil { + prog.Dispose() + return backendSession{}, err + } + return backendSession{ + prog: prog, + transformer: cabi.NewTransformer(prog, t.llvmTarget, t.targetABI, t.abiMode, t.cabiOptimize), + }, nil +} + +func (t backendProgramTemplate) replayProgramState(prog llssa.Program) error { + for _, input := range t.inputs { + if input.parseSyntax { + if err := cl.ParsePkgSyntax(prog, input.fset, input.pkg, input.files); err != nil { + return err + } + } + } + for _, input := range t.inputs { + if input.prepareLocal { + if err := cl.PrepareLocalVariables(prog, input.fset, input.pkg, input.info, input.files); err != nil { + return err + } + } + } + return nil +} + +func collectBackendProgramInputs(groups ...[]*packages.Package) []backendProgramInput { + seen := make(map[*types.Package]bool) + var inputs []backendProgramInput + for _, roots := range groups { + packages.Visit(roots, nil, func(pkg *packages.Package) { + if pkg == nil || pkg.Types == nil || pkg.IllTyped || seen[pkg.Types] { + return + } + seen[pkg.Types] = true + inputs = append(inputs, backendProgramInput{ + fset: pkg.Fset, + pkg: pkg.Types, + info: pkg.TypesInfo, + files: slices.Clone(pkg.Syntax), + parseSyntax: !llruntime.SkipToBuild(pkg.PkgPath), + prepareLocal: true, + }) + }) + } + return inputs +} + +func appendPatchedBackendInputs(template *backendProgramTemplate, patches cl.Patches, dedup packages.Deduper) { + paths := make([]string, 0, len(patches)) + for pkgPath := range patches { + paths = append(paths, pkgPath) + } + slices.Sort(paths) + for _, pkgPath := range paths { + alt := dedup.Check(altPkgPathPrefix + pkgPath) + if alt == nil || len(alt.Syntax) == 0 { + continue + } + template.inputs = append(template.inputs, backendProgramInput{ + fset: alt.Fset, + pkg: types.NewPackage(pkgPath, ""), + files: slices.Clone(alt.Syntax), + parseSyntax: true, + }) + } +} + func (c *context) compiler() *clang.Cmd { config := clang.NewConfig( c.crossCompile.CC, From ee1c9bec9981c7e64b70cd90f1245e321a0c551b Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Fri, 31 Jul 2026 08:39:28 +0800 Subject: [PATCH 2/4] build: replay locality state in backend sessions --- internal/build/backend_program_test.go | 2 +- internal/build/build.go | 37 ++++++-------- ssa/locality.go | 69 ++++++++++++++++++++++++++ ssa/locality_test.go | 28 +++++++++++ 4 files changed, 114 insertions(+), 22 deletions(-) diff --git a/internal/build/backend_program_test.go b/internal/build/backend_program_test.go index dfa9bb125f..161d2aa2ae 100644 --- a/internal/build/backend_program_test.go +++ b/internal/build/backend_program_test.go @@ -73,7 +73,7 @@ func TestBackendProgramTemplateOptionalState(t *testing.T) { duplicate := &packages.Package{PkgPath: "example.com/duplicate", Types: validTypes} missingTypes := &packages.Package{PkgPath: "example.com/missing"} illTyped := &packages.Package{PkgPath: "example.com/ill", Types: types.NewPackage("example.com/ill", "ill"), IllTyped: true} - inputs := collectBackendProgramInputs([]*packages.Package{missingTypes, illTyped, valid, duplicate}) + inputs := collectBackendProgramInputs(prog, []*packages.Package{missingTypes, illTyped, valid, duplicate}) if len(inputs) != 1 || inputs[0].pkg != validTypes { t.Fatalf("backend inputs = %#v, want one deduplicated package", inputs) } diff --git a/internal/build/build.go b/internal/build/build.go index 58525e85a4..9bb73fbcee 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -595,7 +595,8 @@ func Build(inv Invocation) ([]Package, error) { if pkg := dedup.Check(llssa.PkgPython); pkg != nil { backendTemplate.pythonPackage = pkg.Types } - backendTemplate.inputs = collectBackendProgramInputs(initial, altPkgs) + backendTemplate.inputs = collectBackendProgramInputs(prog, initial, altPkgs) + backendTemplate.localities = prog.SnapshotLocalityState() backendTemplate.llvmTarget = export.LLVMTarget backendTemplate.targetABI = export.TargetABI @@ -931,12 +932,11 @@ func (c *context) closePackageMetas() { } type backendProgramInput struct { - fset *token.FileSet - pkg *types.Package - info *types.Info - files []*ast.File - parseSyntax bool - prepareLocal bool + fset *token.FileSet + pkg *types.Package + info *types.Info + files []*ast.File + parseSyntax bool } // backendProgramTemplate contains only immutable build-local inputs. Creating @@ -955,6 +955,7 @@ type backendProgramTemplate struct { runtimePackage *types.Package pythonPackage *types.Package inputs []backendProgramInput + localities llssa.LocalityState llvmTarget string targetABI string abiMode cabi.Mode @@ -1033,17 +1034,11 @@ func (t backendProgramTemplate) replayProgramState(prog llssa.Program) error { } } } - for _, input := range t.inputs { - if input.prepareLocal { - if err := cl.PrepareLocalVariables(prog, input.fset, input.pkg, input.info, input.files); err != nil { - return err - } - } - } + prog.RestoreLocalityState(t.localities) return nil } -func collectBackendProgramInputs(groups ...[]*packages.Package) []backendProgramInput { +func collectBackendProgramInputs(prog llssa.Program, groups ...[]*packages.Package) []backendProgramInput { seen := make(map[*types.Package]bool) var inputs []backendProgramInput for _, roots := range groups { @@ -1052,13 +1047,13 @@ func collectBackendProgramInputs(groups ...[]*packages.Package) []backendProgram return } seen[pkg.Types] = true + parsed := prog.PackageSyntaxParsed(pkg.Types) inputs = append(inputs, backendProgramInput{ - fset: pkg.Fset, - pkg: pkg.Types, - info: pkg.TypesInfo, - files: slices.Clone(pkg.Syntax), - parseSyntax: !llruntime.SkipToBuild(pkg.PkgPath), - prepareLocal: true, + fset: pkg.Fset, + pkg: pkg.Types, + info: pkg.TypesInfo, + files: slices.Clone(pkg.Syntax), + parseSyntax: parsed && !llruntime.SkipToBuild(pkg.PkgPath), }) }) } diff --git a/ssa/locality.go b/ssa/locality.go index 0e9b41e6cd..cd3402b773 100644 --- a/ssa/locality.go +++ b/ssa/locality.go @@ -61,6 +61,17 @@ type localityInfos struct { parsedPackages map[*types.Package]struct{} } +// LocalityState is a copyable snapshot of the Go-side locality metadata owned +// by one Program. It contains no LLVM objects and can therefore be replayed +// safely into an independent backend Program. +type LocalityState struct { + entries map[string]VariableLocality + ownerlessEntries map[string]VariableLocality + declarationEntries map[string]map[string]VariableLocality + activePackages map[string]struct{} + parsedPackages map[*types.Package]struct{} +} + func newLocalityInfos() *localityInfos { return &localityInfos{ entries: make(map[string]VariableLocality), @@ -71,6 +82,64 @@ func newLocalityInfos() *localityInfos { } } +// SnapshotLocalityState returns an independent snapshot of p's locality +// metadata. Callers may use it after preflight has completed to seed isolated +// backend sessions without sharing mutable Program state. +func (p Program) SnapshotLocalityState() LocalityState { + p.localities.mu.RLock() + defer p.localities.mu.RUnlock() + state := LocalityState{ + entries: cloneLocalityEntries(p.localities.entries), + ownerlessEntries: cloneLocalityEntries(p.localities.ownerlessEntries), + activePackages: cloneLocalitySet(p.localities.activePackages), + parsedPackages: cloneParsedPackages(p.localities.parsedPackages), + } + state.declarationEntries = make(map[string]map[string]VariableLocality, len(p.localities.declarationEntries)) + for name, entries := range p.localities.declarationEntries { + state.declarationEntries[name] = cloneLocalityEntries(entries) + } + return state +} + +// RestoreLocalityState replaces p's locality metadata with an independent +// copy of state. The copied maps keep backend sessions independent. +func (p Program) RestoreLocalityState(state LocalityState) { + p.localities.mu.Lock() + p.localities.entries = cloneLocalityEntries(state.entries) + p.localities.ownerlessEntries = cloneLocalityEntries(state.ownerlessEntries) + p.localities.activePackages = cloneLocalitySet(state.activePackages) + p.localities.parsedPackages = cloneParsedPackages(state.parsedPackages) + p.localities.declarationEntries = make(map[string]map[string]VariableLocality, len(state.declarationEntries)) + for name, entries := range state.declarationEntries { + p.localities.declarationEntries[name] = cloneLocalityEntries(entries) + } + p.localities.mu.Unlock() +} + +func cloneLocalityEntries(entries map[string]VariableLocality) map[string]VariableLocality { + ret := make(map[string]VariableLocality, len(entries)) + for name, entry := range entries { + ret[name] = entry + } + return ret +} + +func cloneLocalitySet(entries map[string]struct{}) map[string]struct{} { + ret := make(map[string]struct{}, len(entries)) + for name := range entries { + ret[name] = struct{}{} + } + return ret +} + +func cloneParsedPackages(entries map[*types.Package]struct{}) map[*types.Package]struct{} { + ret := make(map[*types.Package]struct{}, len(entries)) + for pkg := range entries { + ret[pkg] = struct{}{} + } + return ret +} + func (p *localityInfos) update(name string, update func(*VariableLocality)) { p.mu.Lock() info := p.entries[name] diff --git a/ssa/locality_test.go b/ssa/locality_test.go index 1d6ad63cb5..b2a79f0ad1 100644 --- a/ssa/locality_test.go +++ b/ssa/locality_test.go @@ -63,6 +63,34 @@ func TestLocalityInfos(t *testing.T) { } } +func TestLocalityStateSnapshotIsIndependent(t *testing.T) { + 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) + + dest := NewProgram(nil) + defer dest.Dispose() + dest.RestoreLocalityState(source.SnapshotLocalityState()) + got, ok := dest.VariableLocalityFor(pkg, name) + if !ok || got.Locality != GoroutineLocal || got.LocalStorage != LocalStoragePackage { + t.Fatalf("restored locality = %+v, %v", got, ok) + } + if !dest.NeedsLocalContext() || !dest.PackageSyntaxParsed(pkg) { + t.Fatal("restored locality state lost active or parsed metadata") + } + + source.SetLocalStorageFor(pkg, name, LocalStorageNativeTLS) + got, ok = dest.VariableLocalityFor(pkg, name) + if !ok || got.LocalStorage != LocalStoragePackage { + t.Fatalf("restored locality aliased source state: %+v, %v", got, ok) + } +} + func TestPackageLocalitiesRetainDeclarationOwners(t *testing.T) { prog := NewProgram(nil) std := types.NewPackage("runtime", "runtime") From bfdcaae86750b5708f3aa1a01ee736b2ec7a8796 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 4 Aug 2026 00:06:40 +0800 Subject: [PATCH 3/4] build: share prepared locality state across backends --- internal/build/build.go | 12 ++--- ssa/locality.go | 113 +++++++++++++++------------------------- ssa/locality_test.go | 46 +++++++++++++--- ssa/package.go | 5 +- 4 files changed, 88 insertions(+), 88 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index 9bb73fbcee..caac8f778b 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -596,7 +596,7 @@ func Build(inv Invocation) ([]Package, error) { backendTemplate.pythonPackage = pkg.Types } backendTemplate.inputs = collectBackendProgramInputs(prog, initial, altPkgs) - backendTemplate.localities = prog.SnapshotLocalityState() + backendTemplate.localities = prog.FreezeLocalityState() backendTemplate.llvmTarget = export.LLVMTarget backendTemplate.targetABI = export.TargetABI @@ -934,14 +934,13 @@ func (c *context) closePackageMetas() { type backendProgramInput struct { fset *token.FileSet pkg *types.Package - info *types.Info files []*ast.File parseSyntax bool } -// backendProgramTemplate contains only 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. +// 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 @@ -1034,7 +1033,7 @@ func (t backendProgramTemplate) replayProgramState(prog llssa.Program) error { } } } - prog.RestoreLocalityState(t.localities) + prog.UseLocalityState(t.localities) return nil } @@ -1051,7 +1050,6 @@ func collectBackendProgramInputs(prog llssa.Program, groups ...[]*packages.Packa inputs = append(inputs, backendProgramInput{ fset: pkg.Fset, pkg: pkg.Types, - info: pkg.TypesInfo, files: slices.Clone(pkg.Syntax), parseSyntax: parsed && !llruntime.SkipToBuild(pkg.PkgPath), }) diff --git a/ssa/locality.go b/ssa/locality.go index cd3402b773..30c8d28226 100644 --- a/ssa/locality.go +++ b/ssa/locality.go @@ -51,25 +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 a copyable snapshot of the Go-side locality metadata owned -// by one Program. It contains no LLVM objects and can therefore be replayed -// safely into an independent backend Program. +// 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 { - entries map[string]VariableLocality - ownerlessEntries map[string]VariableLocality - declarationEntries map[string]map[string]VariableLocality - activePackages map[string]struct{} - parsedPackages map[*types.Package]struct{} + infos *localityInfos } func newLocalityInfos() *localityInfos { @@ -78,81 +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{}), - } -} - -// SnapshotLocalityState returns an independent snapshot of p's locality -// metadata. Callers may use it after preflight has completed to seed isolated -// backend sessions without sharing mutable Program state. -func (p Program) SnapshotLocalityState() LocalityState { - p.localities.mu.RLock() - defer p.localities.mu.RUnlock() - state := LocalityState{ - entries: cloneLocalityEntries(p.localities.entries), - ownerlessEntries: cloneLocalityEntries(p.localities.ownerlessEntries), - activePackages: cloneLocalitySet(p.localities.activePackages), - parsedPackages: cloneParsedPackages(p.localities.parsedPackages), - } - state.declarationEntries = make(map[string]map[string]VariableLocality, len(p.localities.declarationEntries)) - for name, entries := range p.localities.declarationEntries { - state.declarationEntries[name] = cloneLocalityEntries(entries) } - return state } -// RestoreLocalityState replaces p's locality metadata with an independent -// copy of state. The copied maps keep backend sessions independent. -func (p Program) RestoreLocalityState(state LocalityState) { +// 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.entries = cloneLocalityEntries(state.entries) - p.localities.ownerlessEntries = cloneLocalityEntries(state.ownerlessEntries) - p.localities.activePackages = cloneLocalitySet(state.activePackages) - p.localities.parsedPackages = cloneParsedPackages(state.parsedPackages) - p.localities.declarationEntries = make(map[string]map[string]VariableLocality, len(state.declarationEntries)) - for name, entries := range state.declarationEntries { - p.localities.declarationEntries[name] = cloneLocalityEntries(entries) - } + p.localities.frozen = true p.localities.mu.Unlock() + return LocalityState{infos: p.localities} } -func cloneLocalityEntries(entries map[string]VariableLocality) map[string]VariableLocality { - ret := make(map[string]VariableLocality, len(entries)) - for name, entry := range entries { - ret[name] = entry - } - return ret -} - -func cloneLocalitySet(entries map[string]struct{}) map[string]struct{} { - ret := make(map[string]struct{}, len(entries)) - for name := range entries { - ret[name] = struct{}{} +// 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 } - return ret -} - -func cloneParsedPackages(entries map[*types.Package]struct{}) map[*types.Package]struct{} { - ret := make(map[*types.Package]struct{}, len(entries)) - for pkg := range entries { - ret[pkg] = struct{}{} - } - return ret + 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) @@ -163,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) { @@ -185,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 @@ -195,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) { @@ -217,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 @@ -399,16 +368,16 @@ 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() + p.parsedPackagesMu.RLock() + _, ok := p.parsedPackages[pkg] + p.parsedPackagesMu.RUnlock() return ok } func (p Program) MarkPackageSyntaxParsed(pkg *types.Package) { - p.localities.mu.Lock() - p.localities.parsedPackages[pkg] = struct{}{} - p.localities.mu.Unlock() + p.parsedPackagesMu.Lock() + p.parsedPackages[pkg] = struct{}{} + p.parsedPackagesMu.Unlock() } // PackageLocalities returns the legacy canonical-only metadata view. Its diff --git a/ssa/locality_test.go b/ssa/locality_test.go index b2a79f0ad1..49ac203c6e 100644 --- a/ssa/locality_test.go +++ b/ssa/locality_test.go @@ -63,7 +63,14 @@ func TestLocalityInfos(t *testing.T) { } } -func TestLocalityStateSnapshotIsIndependent(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") @@ -73,22 +80,45 @@ func TestLocalityStateSnapshotIsIndependent(t *testing.T) { source.ActivateLocalitiesFor(pkg) source.MarkPackageSyntaxParsed(pkg) + state := source.FreezeLocalityState() dest := NewProgram(nil) defer dest.Dispose() - dest.RestoreLocalityState(source.SnapshotLocalityState()) + 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("restored locality = %+v, %v", got, ok) + t.Fatalf("shared locality = %+v, %v", got, ok) } - if !dest.NeedsLocalContext() || !dest.PackageSyntaxParsed(pkg) { - t.Fatal("restored locality state lost active or parsed metadata") + 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") } - source.SetLocalStorageFor(pkg, name, LocalStorageNativeTLS) + // 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.LocalStorage != LocalStoragePackage { - t.Fatalf("restored locality aliased source state: %+v, %v", got, ok) + 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) { diff --git a/ssa/package.go b/ssa/package.go index 498bbaf942..cb2eca524b 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -226,6 +226,8 @@ type aProgram struct { linkname map[string]string // pkgPath.nameInPkg => linkname closureEnvDirectives sync.Map // closureEnvDirectiveKey => none localities *localityInfos + parsedPackagesMu sync.RWMutex + parsedPackages map[*types.Package]struct{} noInterface map[string]none // pkgPath.T.method or pkgPath.(*T).method abiSymbol map[string]*AbiSymbol // abi symbol name => AbiSymbol @@ -317,7 +319,8 @@ func NewProgram(target *Target) Program { 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), + parsedPackages: make(map[*types.Package]struct{}), + noInterface: make(map[string]none), abiSymbol: make(map[string]*AbiSymbol), debugInfoOptimized: target.effectiveOptLevel() != optlevel.O0, } prog.abi.Init(uintptr(prog.ptrSize), (*goProgram)(unsafe.Pointer(prog))) From a30cc34f25148b6b357364ccf096370d4780dfb0 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 4 Aug 2026 14:31:08 +0800 Subject: [PATCH 4/4] build: share preloaded package syntax state --- cl/import.go | 57 +++++--- cl/import_coverage_test.go | 52 ++++++- internal/build/backend_program_test.go | 44 +++--- internal/build/build.go | 100 ++++--------- ssa/locality.go | 16 +- ssa/package.go | 42 ++---- ssa/package_syntax.go | 195 +++++++++++++++++++++++++ ssa/package_syntax_test.go | 116 +++++++++++++++ ssa/type.go | 2 +- ssa/type_cvt.go | 10 +- 10 files changed, 485 insertions(+), 149 deletions(-) create mode 100644 ssa/package_syntax.go create mode 100644 ssa/package_syntax_test.go 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/internal/build/backend_program_test.go b/internal/build/backend_program_test.go index 161d2aa2ae..bdc066123c 100644 --- a/internal/build/backend_program_test.go +++ b/internal/build/backend_program_test.go @@ -17,11 +17,10 @@ package build import ( + "go/token" "go/types" "testing" - "github.com/goplus/llgo/cl" - "github.com/goplus/llgo/internal/packages" llssa "github.com/goplus/llgo/ssa" ) @@ -68,22 +67,31 @@ func TestBackendProgramTemplateOptionalState(t *testing.T) { t.Fatal("backend program created without a default target") } - validTypes := types.NewPackage("example.com/valid", "valid") - valid := &packages.Package{PkgPath: "example.com/valid", Types: validTypes} - duplicate := &packages.Package{PkgPath: "example.com/duplicate", Types: validTypes} - missingTypes := &packages.Package{PkgPath: "example.com/missing"} - illTyped := &packages.Package{PkgPath: "example.com/ill", Types: types.NewPackage("example.com/ill", "ill"), IllTyped: true} - inputs := collectBackendProgramInputs(prog, []*packages.Package{missingTypes, illTyped, valid, duplicate}) - if len(inputs) != 1 || inputs[0].pkg != validTypes { - t.Fatalf("backend inputs = %#v, want one deduplicated package", inputs) + 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) } - - patched := backendProgramTemplate{} - appendPatchedBackendInputs(&patched, cl.Patches{ - "example.com/missing": {}, - "example.com/other": {}, - }, packages.NewDeduper()) - if len(patched.inputs) != 0 { - t.Fatalf("missing patched packages produced inputs: %#v", patched.inputs) + 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 caac8f778b..5e04ff77d0 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -501,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) } }) @@ -587,19 +587,6 @@ func Build(inv Invocation) ([]Package, error) { } return nil }) - 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.inputs = collectBackendProgramInputs(prog, initial, altPkgs) - backendTemplate.localities = prog.FreezeLocalityState() - backendTemplate.llvmTarget = export.LLVMTarget - backendTemplate.targetABI = export.TargetABI - buildMode := ssaBuildMode cabiOptimize := true passOpt := true @@ -617,7 +604,21 @@ func Build(inv Invocation) ([]Package, error) { progSSA := ssa.NewProgram(initial[0].Fset, buildMode) patches := make(cl.Patches, len(altPkgPaths)) altEntries := registerAltSSAPkgs(progSSA, patches, altPkgs[1:], conf, verbose) - appendPatchedBackendInputs(&backendTemplate, patches, dedup) + 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, @@ -931,13 +932,6 @@ func (c *context) closePackageMetas() { } } -type backendProgramInput struct { - fset *token.FileSet - pkg *types.Package - files []*ast.File - parseSyntax bool -} - // 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. @@ -953,7 +947,7 @@ type backendProgramTemplate struct { funcInfoSites bool runtimePackage *types.Package pythonPackage *types.Package - inputs []backendProgramInput + packageSyntax llssa.PackageSyntaxState localities llssa.LocalityState llvmTarget string targetABI string @@ -1015,67 +1009,37 @@ func (t backendProgramTemplate) newProgram() llssa.Program { func (t backendProgramTemplate) newSession() (backendSession, error) { prog := t.newProgram() - if err := t.replayProgramState(prog); err != nil { - prog.Dispose() - return backendSession{}, err - } + 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 (t backendProgramTemplate) replayProgramState(prog llssa.Program) error { - for _, input := range t.inputs { - if input.parseSyntax { - if err := cl.ParsePkgSyntax(prog, input.fset, input.pkg, input.files); err != nil { - return err - } - } - } - prog.UseLocalityState(t.localities) - return nil -} - -func collectBackendProgramInputs(prog llssa.Program, groups ...[]*packages.Package) []backendProgramInput { - seen := make(map[*types.Package]bool) - var inputs []backendProgramInput - for _, roots := range groups { - packages.Visit(roots, nil, func(pkg *packages.Package) { - if pkg == nil || pkg.Types == nil || pkg.IllTyped || seen[pkg.Types] { - return - } - seen[pkg.Types] = true - parsed := prog.PackageSyntaxParsed(pkg.Types) - inputs = append(inputs, backendProgramInput{ - fset: pkg.Fset, - pkg: pkg.Types, - files: slices.Clone(pkg.Syntax), - parseSyntax: parsed && !llruntime.SkipToBuild(pkg.PkgPath), - }) - }) - } - return inputs -} - -func appendPatchedBackendInputs(template *backendProgramTemplate, patches cl.Patches, dedup packages.Deduper) { +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 { + if alt == nil || len(alt.Syntax) == 0 || patch.Types == nil { continue } - template.inputs = append(template.inputs, backendProgramInput{ - fset: alt.Fset, - pkg: types.NewPackage(pkgPath, ""), - files: slices.Clone(alt.Syntax), - parseSyntax: true, - }) + 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 { diff --git a/ssa/locality.go b/ssa/locality.go index 30c8d28226..deb6f6452f 100644 --- a/ssa/locality.go +++ b/ssa/locality.go @@ -331,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 @@ -368,16 +367,11 @@ func linknameReachesLocal(name string, links map[string]string, localNames map[s } func (p Program) PackageSyntaxParsed(pkg *types.Package) bool { - p.parsedPackagesMu.RLock() - _, ok := p.parsedPackages[pkg] - p.parsedPackagesMu.RUnlock() - return ok + return p.packageSyntax.packageParsed(pkg) } func (p Program) MarkPackageSyntaxParsed(pkg *types.Package) { - p.parsedPackagesMu.Lock() - p.parsedPackages[pkg] = struct{}{} - p.parsedPackagesMu.Unlock() + p.packageSyntax.markPackageParsed(pkg) } // PackageLocalities returns the legacy canonical-only metadata view. Its diff --git a/ssa/package.go b/ssa/package.go index cb2eca524b..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,15 +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 - parsedPackagesMu sync.RWMutex - parsedPackages map[*types.Package]struct{} - 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 @@ -314,13 +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(), - parsedPackages: make(map[*types.Package]struct{}), - 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))) @@ -386,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 { @@ -397,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 } @@ -413,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 { @@ -440,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)