From 880b903088c1a41b2f17d540f22c82a6d1b5d209 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 4 Aug 2026 08:41:38 +0800 Subject: [PATCH 1/3] ssa: make recursive type conversion order-independent --- ssa/type_cvt.go | 172 ++++++++++++++++++++++++++++++++++---- ssa/type_cvt_test.go | 195 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 353 insertions(+), 14 deletions(-) create mode 100644 ssa/type_cvt_test.go diff --git a/ssa/type_cvt.go b/ssa/type_cvt.go index bd8ab36ed4..a5302a77b4 100644 --- a/ssa/type_cvt.go +++ b/ssa/type_cvt.go @@ -28,15 +28,27 @@ import ( // ----------------------------------------------------------------------------- type goTypes struct { - typs map[unsafe.Pointer]unsafe.Pointer - typbg sync.Map + // typs and cvtneed are owned by the single lowering goroutine for one + // Program. typbg is populated during concurrent package syntax preloading, + // before lowering starts, so it remains a sync.Map. + typs map[unsafe.Pointer]unsafe.Pointer + cvtneed map[*types.Named]conversionRequirement + typbg sync.Map } func newGoTypes() goTypes { typs := make(map[unsafe.Pointer]unsafe.Pointer) - return goTypes{typs: typs} + return goTypes{typs: typs, cvtneed: make(map[*types.Named]conversionRequirement)} } +type conversionRequirement uint8 + +const ( + conversionUnknown conversionRequirement = iota + conversionNotNeeded + conversionNeeded +) + type Background int const ( @@ -101,7 +113,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 !p.shouldConvertNamed(t) { break } return p.cvtNamed(t) @@ -161,37 +173,169 @@ func namedLinkname(t *types.Named) string { return obj.Name() } +func (p goTypes) shouldConvertNamed(t *types.Named) bool { + v, ok := p.typbg.Load(namedLinkname(t)) + return !ok || v.(Background) != InC +} + func (p goTypes) cvtNamed(t *types.Named) (raw *types.Named, cvt bool) { if v, ok := p.typs[unsafe.Pointer(t)]; ok { raw = (*types.Named)(v) cvt = t != raw return } + // Decide whether the complete recursive type graph needs conversion before + // installing the recursion placeholder. Previously the placeholder was the + // original type. For mutually recursive named types, that made the result + // depend on which member of the cycle happened to be converted first: a + // closure reachable through a later member could leave an earlier member + // permanently cached in its unconverted form. + if !p.namedNeedsTypeConversion(t) { + p.typs[unsafe.Pointer(t)] = unsafe.Pointer(t) + return t, false + } n := t.NumMethods() methods := make([]*types.Func, n) for i := 0; i < n; i++ { m := t.Method(i) // don't need to convert method signature methods[i] = m } - named := types.NewNamed(t.Obj(), types.Typ[types.Int], methods) + origin := types.NewNamed(t.Obj(), types.Typ[types.Int], methods) if tp := t.TypeParams(); tp != nil { list := make([]*types.TypeParam, tp.Len()) for i := 0; i < tp.Len(); i++ { param := tp.At(i) list[i] = types.NewTypeParam(param.Obj(), param.Constraint()) } - named.SetTypeParams(list) + origin.SetTypeParams(list) } - p.typs[unsafe.Pointer(t)] = unsafe.Pointer(t) - if tund, cvt := p.cvtType(t.Underlying()); cvt { - named.SetUnderlying(tund) - if typ, ok := Instantiate(named, t); ok { - named = typ.(*types.Named) + named := origin + if typ, ok := Instantiate(origin, t); ok { + named = typ.(*types.Named) + } + // Publish the converted placeholder before descending so every back-edge in + // the cycle observes the same conversion decision. + p.typs[unsafe.Pointer(t)] = unsafe.Pointer(named) + tund, _ := p.cvtType(t.Underlying()) + // Generic instances derive their underlying type lazily from the origin. + // Fill the origin before any caller observes named.Underlying(), so a + // recursive My[T] back-edge resolves to the converted My[args] instance. + origin.SetUnderlying(tund) + return named, true +} + +type conversionNeedState struct { + visiting bool + seen bool +} + +type conversionNeedQuery map[*types.Named]conversionNeedState + +func (p goTypes) namedNeedsTypeConversion(t *types.Named) bool { + if requirement := p.cvtneed[t]; requirement != conversionUnknown { + return requirement == conversionNeeded + } + query := make(conversionNeedQuery) + needed := p.needsTypeConversion(t, query) + if !needed { + // A complete negative query proves that every named type it reached is + // also conversion-free. Negative results observed only on a cycle + // back-edge are never stored here. + for named, state := range query { + if state.seen { + p.cvtneed[named] = conversionNotNeeded + } } - p.typs[unsafe.Pointer(t)] = unsafe.Pointer(named) - return named, true } - return t, false + return needed +} + +// needsTypeConversion reports whether cvtType changes any part of typ. The +// recursion set deliberately belongs to one query: a cycle back-edge alone is +// not a conversion, but another member of that cycle may still require one. +// Keep its traversal and conversion predicates in lock-step with cvtType. +func (p goTypes) needsTypeConversion(typ types.Type, query conversionNeedQuery) bool { + if _, ok := cvtGoSSAOpaqueType(typ); ok { + return true + } + switch t := typ.(type) { + case *types.Basic: + return false + case *types.Pointer: + return p.needsTypeConversion(t.Elem(), query) + case *types.Interface: + for i := 0; i < t.NumExplicitMethods(); i++ { + sig := t.ExplicitMethod(i).Type().(*types.Signature) + if p.needsTypeConversion(sig.Params(), query) || p.needsTypeConversion(sig.Results(), query) { + return true + } + } + for i := 0; i < t.NumEmbeddeds(); i++ { + if p.needsTypeConversion(t.EmbeddedType(i), query) { + return true + } + } + return false + case *types.Slice: + return p.needsTypeConversion(t.Elem(), query) + case *types.Map: + return p.needsTypeConversion(t.Key(), query) || p.needsTypeConversion(t.Elem(), query) + case *types.Struct: + if IsClosure(t) { + return false + } + for i := 0; i < t.NumFields(); i++ { + if p.needsTypeConversion(t.Field(i).Type(), query) { + return true + } + } + return false + case *types.Named: + if !p.shouldConvertNamed(t) { + return false + } + if requirement := p.cvtneed[t]; requirement != conversionUnknown { + return requirement == conversionNeeded + } + state := query[t] + state.seen = true + if state.visiting { + query[t] = state + return false + } + state.visiting = true + query[t] = state + ret := p.needsTypeConversion(t.Underlying(), query) + state = query[t] + state.visiting = false + query[t] = state + if ret { + p.cvtneed[t] = conversionNeeded + } + return ret + case *types.Signature: + return true + case *types.Array: + return p.needsTypeConversion(t.Elem(), query) + case *types.Chan: + return p.needsTypeConversion(t.Elem(), query) + case *types.Tuple: + for i := 0; i < t.Len(); i++ { + if p.needsTypeConversion(t.At(i).Type(), query) { + return true + } + } + return false + case *types.TypeParam: + return false + case *types.Alias: + return p.needsTypeConversion(types.Unalias(t), query) + case *types.Union: + // cvtUnion currently always creates a raw union. + return true + default: + panic(fmt.Sprintf("needsTypeConversion: unexpected type - %T", typ)) + } } func Instantiate(orig types.Type, t *types.Named) (types.Type, bool) { diff --git a/ssa/type_cvt_test.go b/ssa/type_cvt_test.go new file mode 100644 index 0000000000..f599b024e2 --- /dev/null +++ b/ssa/type_cvt_test.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 ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" +) + +func TestNamedTypeConversionIsIndependentOfTraversalOrder(t *testing.T) { + pkg := types.NewPackage("example.com/cycle", "cycle") + a := types.NewNamed(types.NewTypeName(token.NoPos, pkg, "A", nil), types.Typ[types.Int], nil) + b := types.NewNamed(types.NewTypeName(token.NoPos, pkg, "B", nil), types.Typ[types.Int], nil) + a.SetUnderlying(types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, pkg, "B", types.NewPointer(b), false), + }, nil)) + b.SetUnderlying(types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, pkg, "A", types.NewPointer(a), false), + types.NewField(token.NoPos, pkg, "F", types.NewSignatureType(nil, nil, nil, nil, nil, false), false), + }, nil)) + + convert := func(first, second *types.Named) (*types.Named, *types.Named) { + cvt := newGoTypes() + cvt.cvtNamed(first) + rawSecond, _ := cvt.cvtNamed(second) + rawFirst, _ := cvt.cvtNamed(first) + if first == a { + return rawFirst, rawSecond + } + return rawSecond, rawFirst + } + aFromA, bFromA := convert(a, b) + aFromB, bFromB := convert(b, a) + + for name, got := range map[string]*types.Named{ + "A after A-first conversion": aFromA, + "B after A-first conversion": bFromA, + "A after B-first conversion": aFromB, + "B after B-first conversion": bFromB, + } { + original := a + if name[0] == 'B' { + original = b + } + if got == original { + t.Errorf("%s retained the unconverted recursive type", name) + } + } + if got, want := types.TypeString(aFromA, nil), types.TypeString(aFromB, nil); got != want { + t.Errorf("A conversion depends on traversal order:\nA-first: %s\nB-first: %s", got, want) + } + if got, want := types.TypeString(bFromA, nil), types.TypeString(bFromB, nil); got != want { + t.Errorf("B conversion depends on traversal order:\nA-first: %s\nB-first: %s", got, want) + } + assertCycle := func(name string, rawA, rawB *types.Named) { + t.Helper() + aStruct := rawA.Underlying().(*types.Struct) + if got := aStruct.Field(0).Type().(*types.Pointer).Elem(); got != rawB { + t.Errorf("%s: converted A points to %v, want converted B", name, got) + } + bStruct := rawB.Underlying().(*types.Struct) + if got := bStruct.Field(0).Type().(*types.Pointer).Elem(); got != rawA { + t.Errorf("%s: converted B points to %v, want converted A", name, got) + } + if got, ok := bStruct.Field(1).Type().(*types.Struct); !ok || !IsClosure(got) { + t.Errorf("%s: converted B.F type = %v, want closure", name, got) + } + } + assertCycle("A-first", aFromA, bFromA) + assertCycle("B-first", aFromB, bFromB) +} + +func TestRecursiveGenericNamedTypeConversion(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "generic.go", `package generic +type My[T any] struct { + F func(T) + Next *My[T] +} +`, 0) + if err != nil { + t.Fatal(err) + } + pkg, err := (&types.Config{}).Check("example.com/generic", fset, []*ast.File{file}, nil) + if err != nil { + t.Fatal(err) + } + origin := pkg.Scope().Lookup("My").Type() + instantiated, err := types.Instantiate(nil, origin, []types.Type{types.Typ[types.Int]}, false) + if err != nil { + t.Fatal(err) + } + original := instantiated.(*types.Named) + + cvt := newGoTypes() + raw, changed := cvt.cvtNamed(original) + if !changed || raw == original { + t.Fatal("recursive generic type was not converted") + } + underlying, ok := raw.Underlying().(*types.Struct) + if !ok { + t.Fatalf("converted generic underlying = %T, want *types.Struct", raw.Underlying()) + } + if closure, ok := underlying.Field(0).Type().(*types.Struct); !ok || !IsClosure(closure) { + t.Fatalf("converted generic F = %v, want closure", underlying.Field(0).Type()) + } + next := underlying.Field(1).Type().(*types.Pointer).Elem() + if next != raw { + t.Fatalf("converted generic Next points to %v, want converted instance %v", next, raw) + } +} + +func TestTypeConversionRequirementShapes(t *testing.T) { + sig := types.NewSignatureType(nil, nil, nil, nil, nil, false) + sigParam := types.NewSignatureType(nil, nil, nil, + types.NewTuple(types.NewVar(token.NoPos, nil, "f", sig)), nil, false) + method := types.NewFunc(token.NoPos, nil, "M", sigParam) + methodInterface := types.NewInterfaceType([]*types.Func{method}, nil) + methodInterface.Complete() + embeddedInterface := types.NewInterfaceType(nil, []types.Type{methodInterface}) + embeddedInterface.Complete() + typeParam := types.NewTypeParam( + types.NewTypeName(token.NoPos, nil, "T", nil), types.Universe.Lookup("any").Type()) + alias := types.NewAlias(types.NewTypeName(token.NoPos, nil, "Alias", nil), sig) + union := types.NewUnion([]*types.Term{types.NewTerm(false, types.Typ[types.Int])}) + + tests := []struct { + name string + typ types.Type + want bool + }{ + {name: "basic", typ: types.Typ[types.Int]}, + {name: "pointer", typ: types.NewPointer(sig), want: true}, + {name: "interface method", typ: methodInterface, want: true}, + {name: "embedded interface", typ: embeddedInterface, want: true}, + {name: "slice", typ: types.NewSlice(sig), want: true}, + {name: "map key", typ: types.NewMap(sig, types.Typ[types.Int]), want: true}, + {name: "map value", typ: types.NewMap(types.Typ[types.Int], sig), want: true}, + {name: "closure", typ: newGoTypes().cvtClosure(sig)}, + {name: "struct", typ: types.NewStruct([]*types.Var{types.NewField(token.NoPos, nil, "F", sig, false)}, nil), want: true}, + {name: "signature", typ: sig, want: true}, + {name: "array", typ: types.NewArray(sig, 1), want: true}, + {name: "channel", typ: types.NewChan(types.SendRecv, sig), want: true}, + {name: "tuple", typ: types.NewTuple(types.NewVar(token.NoPos, nil, "F", sig)), want: true}, + {name: "type parameter", typ: typeParam}, + {name: "alias", typ: alias, want: true}, + {name: "union", typ: union, want: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cvt := newGoTypes() + query := make(conversionNeedQuery) + if got := cvt.needsTypeConversion(test.typ, query); got != test.want { + t.Fatalf("needsTypeConversion(%v) = %v, want %v", test.typ, got, test.want) + } + }) + } +} + +func TestRecursiveNamedTypesWithoutConversionKeepTheirIdentity(t *testing.T) { + pkg := types.NewPackage("example.com/plaincycle", "plaincycle") + a := types.NewNamed(types.NewTypeName(token.NoPos, pkg, "A", nil), types.Typ[types.Int], nil) + b := types.NewNamed(types.NewTypeName(token.NoPos, pkg, "B", nil), types.Typ[types.Int], nil) + a.SetUnderlying(types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, pkg, "B", types.NewPointer(b), false), + }, nil)) + b.SetUnderlying(types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, pkg, "A", types.NewPointer(a), false), + }, nil)) + + cvt := newGoTypes() + if got, changed := cvt.cvtNamed(a); changed || got != a { + t.Fatalf("plain recursive A conversion = (%v, %v), want original type", got, changed) + } + if got, changed := cvt.cvtNamed(b); changed || got != b { + t.Fatalf("plain recursive B conversion = (%v, %v), want original type", got, changed) + } +} From 06b5378478de95164e190b43102a96a5bd8887ac Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Thu, 6 Aug 2026 00:08:56 +0800 Subject: [PATCH 2/3] test(ssa): keep conversion predicate in sync --- ssa/type_cvt_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ssa/type_cvt_test.go b/ssa/type_cvt_test.go index f599b024e2..bd10a5a13d 100644 --- a/ssa/type_cvt_test.go +++ b/ssa/type_cvt_test.go @@ -170,6 +170,9 @@ func TestTypeConversionRequirementShapes(t *testing.T) { if got := cvt.needsTypeConversion(test.typ, query); got != test.want { t.Fatalf("needsTypeConversion(%v) = %v, want %v", test.typ, got, test.want) } + if _, got := newGoTypes().cvtType(test.typ); got != test.want { + t.Fatalf("cvtType(%v) changed = %v, want %v", test.typ, got, test.want) + } }) } } From c1728af80cbb41408b54cb1b1b94e4902edc36ff Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Wed, 5 Aug 2026 21:29:27 +0800 Subject: [PATCH 3/3] build: prepare read-only package backend state --- cl/caller_tracking_precompute_test.go | 61 +++++++++++++++++ cl/compile.go | 9 ++- cl/import.go | 59 +++++++++++++++-- cl/instr.go | 24 ++++++- cl/preloaded_syntax_test.go | 82 +++++++++++++++++++++++ internal/build/backend_program_test.go | 48 ++++++++++++++ internal/build/build.go | 73 ++++++++++++++++++--- ssa/backend_program_test.go | 85 ++++++++++++++++++++++++ ssa/locality.go | 19 ++---- ssa/package.go | 79 +++++++++++++++------- ssa/package_syntax.go | 90 ++++++++++++++++++++++++++ ssa/type.go | 3 +- ssa/type_cvt.go | 27 +++++--- 13 files changed, 593 insertions(+), 66 deletions(-) create mode 100644 cl/caller_tracking_precompute_test.go create mode 100644 cl/preloaded_syntax_test.go create mode 100644 internal/build/backend_program_test.go create mode 100644 ssa/backend_program_test.go create mode 100644 ssa/package_syntax.go diff --git a/cl/caller_tracking_precompute_test.go b/cl/caller_tracking_precompute_test.go new file mode 100644 index 0000000000..f99428580b --- /dev/null +++ b/cl/caller_tracking_precompute_test.go @@ -0,0 +1,61 @@ +//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 TestCallerTrackingPrecomputeSupportsConcurrentReads(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{dep, root}) + 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 + for range 32 { + wg.Add(1) + go func() { + defer wg.Done() + if !runtimeCallerBaseSet(tracking, dep)[dep.Func("Where")] || + !runtimeCallerFuncSet(tracking, root)[root.Func("Logs")] { + t.Error("concurrent read lost precomputed caller tracking data") + } + }() + } + wg.Wait() +} diff --git a/cl/compile.go b/cl/compile.go index c18ac435a9..909f89b939 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -74,6 +74,9 @@ type Options struct { Trace bool ExportRename bool ShadowStack bool + // PreloadedSyntax means all Program-side source metadata was collected + // before lowering and is now shared read-only by backend Programs. + PreloadedSyntax bool } func legacyOptions() Options { @@ -2231,8 +2234,10 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri pkg.Pkg = pkgTypes patch.Alt.Pkg = pkgTypes } - if err = ParsePkgSyntax(prog, pkgProg.Fset, pkgTypes, files); err != nil { - return nil, nil, err + if !options.PreloadedSyntax { + if err = ParsePkgSyntaxWithOptions(prog, pkgProg.Fset, pkgTypes, files, options); err != nil { + return nil, nil, err + } } if err = prog.ValidateLocalitiesFor(pkgTypes); err != nil { return nil, nil, err diff --git a/cl/import.go b/cl/import.go index 728d162049..21eedd0818 100644 --- a/cl/import.go +++ b/cl/import.go @@ -146,6 +146,9 @@ func (p *context) importPkg(pkg *types.Package, i *pkgInfo) { } start: i.kind = kind + if p.frontendOptions().PreloadedSyntax { + return + } fset := p.fset names := scope.Names() syms := newPkgSymInfo() @@ -177,11 +180,18 @@ start: } func (p *context) initFiles(pkgPath string, files []*ast.File, cPkg bool) { + preloaded := p.frontendOptions().PreloadedSyntax for _, file := range files { for _, decl := range file.Decls { switch decl := decl.(type) { case *ast.FuncDecl: fullName, inPkgName := astFuncName(pkgPath, decl) + if preloaded { + if exportName, ok := p.prog.PackageExport(fullName); ok { + p.pkg.SetExport(fullName, exportName) + } + continue + } p.processNoInterfaceByDoc(decl.Doc, fullName) if !p.processLinknameByDoc(decl.Doc, fullName, inPkgName, false, true) && cPkg { // package C (https://github.com/goplus/llgo/issues/1165) @@ -197,7 +207,14 @@ 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 preloaded { + if exportName, ok := p.prog.PackageExport(fullName); ok { + p.pkg.SetExport(fullName, exportName) + } + } else { + p.processLinknameByDoc(decl.Doc, fullName, inPkgName, true, true) + } } } case token.CONST: @@ -279,6 +296,10 @@ 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) { + _, _ = collectDeclarationDirectivesWithOptions(prog, fset, doc, fullName, inPkgName, funcPos, legacyOptions()) +} + +func collectDeclarationDirectivesWithOptions(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 +315,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 +334,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 +798,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 +825,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 := collectDeclarationDirectivesWithOptions(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 := collectDeclarationDirectivesWithOptions(prog, fset, decl.Doc, pkgPath+"."+inPkgName, inPkgName, token.NoPos, options); err != nil { + return err + } } } vars, err := locality.ScanPackageVar(fset, decl) diff --git a/cl/instr.go b/cl/instr.go index 4b52624f1b..230c21a94f 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -980,14 +980,32 @@ 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 before workers start and then share the plain maps read-only. type CallerTracking struct { base map[*ssa.Package]map[*ssa.Function]bool extended map[*ssa.Package]map[*ssa.Function]bool } +// Precompute resolves caller-tracking data before package backends start. +// Once it returns, callers may share c for concurrent read-only lookups as long +// as pkgs contains every package that can be passed to this compilation. +func (c *CallerTracking) Precompute(pkgs []*ssa.Package) { + if c == nil { + return + } + for _, pkg := range pkgs { + if pkg != nil { + runtimeCallerBaseSet(c, pkg) + } + } + for _, pkg := range pkgs { + if pkg != nil { + runtimeCallerFuncSet(c, pkg) + } + } +} + // NewCallerTracking creates the caller-tracking memoization for one // compilation. func NewCallerTracking() *CallerTracking { diff --git a/cl/preloaded_syntax_test.go b/cl/preloaded_syntax_test.go new file mode 100644 index 0000000000..dc16fda600 --- /dev/null +++ b/cl/preloaded_syntax_test.go @@ -0,0 +1,82 @@ +//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 ( + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "testing" + + "github.com/goplus/llgo/internal/goembed" + "github.com/goplus/llgo/ssa/ssatest" + "golang.org/x/tools/go/ssa" +) + +func TestPreloadedSyntaxFeedsBackendWithoutLateDiscovery(t *testing.T) { + const source = `package C + +//export callback +func Callback() {} + +func XDefault() {} +` + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "preloaded.go", source, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + info := newLocalityTypeInfo() + imp := importer.Default() + pkg, err := (&types.Config{Importer: imp}).Check("example.com/C", fset, files, info) + if err != nil { + t.Fatal(err) + } + coordinator := ssatest.NewProgramEx(t, nil, imp) + if err := ParsePkgSyntaxWithOptions(coordinator, fset, pkg, files, Options{ExportRename: true}); err != nil { + t.Fatal(err) + } + backend := coordinator.NewBackendProgram() + defer backend.Dispose() + + goProg := ssa.NewProgram(fset, ssa.SanityCheckFunctions) + ssaPkg := goProg.CreatePackage(pkg, files, info, true) + ssaPkg.Build() + compiled, _, err := NewPackageExWithEmbedMetaOptions( + backend, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, false, + Options{ExportRename: true, PreloadedSyntax: true}, + ) + if err != nil { + t.Fatal(err) + } + for fullName, want := range map[string]string{ + "example.com/C.Callback": "callback", + "example.com/C.XDefault": "Default", + } { + if link, ok := backend.Linkname(fullName); !ok || link != want { + t.Errorf("Linkname(%q) = (%q, %v), want (%q, true)", fullName, link, ok, want) + } + if export, ok := compiled.ExportFuncs()[fullName]; !ok || export != want { + t.Errorf("ExportFuncs()[%q] = (%q, %v), want (%q, true)", fullName, export, ok, want) + } + } +} diff --git a/internal/build/backend_program_test.go b/internal/build/backend_program_test.go new file mode 100644 index 0000000000..7af22f6416 --- /dev/null +++ b/internal/build/backend_program_test.go @@ -0,0 +1,48 @@ +/* + * 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 ( + "runtime" + "testing" + + llssa "github.com/goplus/llgo/ssa" +) + +func TestNewBackendSessionCreatesIndependentLLVMState(t *testing.T) { + coordinator := llssa.NewProgram(&llssa.Target{GOOS: runtime.GOOS, GOARCH: runtime.GOARCH}) + defer coordinator.Dispose() + ctx := &context{ + prog: coordinator, + buildConf: &Config{ + Goos: runtime.GOOS, + Goarch: runtime.GOARCH, + }, + } + first := ctx.newBackendSession() + defer first.prog.Dispose() + second := ctx.newBackendSession() + defer second.prog.Dispose() + if first.transformer == nil || second.transformer == nil { + t.Fatal("backend session missing C ABI transformer") + } + 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") + } +} diff --git a/internal/build/build.go b/internal/build/build.go index 9af327bd3d..522a00cda6 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -448,6 +448,7 @@ func Build(inv Invocation) ([]Package, error) { ExportRename: conf.Target != "", ShadowStack: isEnvOn(llgoShadowStack, false), } + preloadOptions := frontendOptions llssaInitOnce.Do(func() { llssa.Initialize(llssa.InitAll) }) @@ -502,7 +503,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, preloadOptions); err != nil { recordSyntaxErr(err) } }) @@ -579,15 +580,12 @@ func Build(inv Invocation) ([]Package, error) { return nil, err } - prog.SetRuntime(func() *types.Package { - return altPkgs[0].Types - }) - prog.SetPython(func() *types.Package { - return dedup.Check(llssa.PkgPython).Types - }) - if err := prepareLocalVariables(prog, initial, altPkgs); err != nil { - return nil, err + prog.SetRuntime(altPkgs[0].Types) + var pythonPackage *types.Package + if python := dedup.Check(llssa.PkgPython); python != nil { + pythonPackage = python.Types } + prog.SetPython(func() *types.Package { return pythonPackage }) buildMode := ssaBuildMode cabiOptimize := true @@ -603,6 +601,13 @@ 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) + if err := preloadPatchedPackageSyntax(prog, patches, dedup, preloadOptions); err != nil { + return nil, err + } + if err := prepareLocalVariables(prog, initial, altPkgs); err != nil { + return nil, err + } + frontendOptions.PreloadedSyntax = true output := conf.OutFile != "" ctx := &context{conf: cfg, progSSA: progSSA, prog: prog, dedup: dedup, @@ -634,6 +639,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...) @@ -913,6 +919,55 @@ func (c *context) closePackageMetas() { } } +// backendSession owns all LLVM state used to lower one package. The Program +// shares only the coordinator's already-prepared Go metadata. +type backendSession struct { + prog llssa.Program + transformer *cabi.Transformer +} + +func (c *context) newBackendSession() backendSession { + prog := c.prog.NewBackendProgram() + return backendSession{ + prog: prog, + transformer: cabi.NewTransformer( + prog, + c.crossCompile.LLVMTarget, + c.crossCompile.TargetABI, + c.buildConf.AbiMode, + !shouldEmitDebugInfo(c.buildConf, &c.crossCompile), + ), + } +} + +// preloadPatchedPackageSyntax prepares the effective types.Package used by +// patched lowering. Normal and alternate packages are already covered by the +// packages loader's preload callback, but patch.Types has a distinct identity. +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/backend_program_test.go b/ssa/backend_program_test.go new file mode 100644 index 0000000000..06df50fdc3 --- /dev/null +++ b/ssa/backend_program_test.go @@ -0,0 +1,85 @@ +/* + * 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" + "testing" +) + +func TestNewBackendProgramSharesPreparedGoState(t *testing.T) { + coordinator := NewProgram(nil) + defer coordinator.Dispose() + coordinator.DisableBoundsChecks(true) + coordinator.EnableGoGlobalDCE(true) + coordinator.EnableDeadcodeDrop(true) + coordinator.SetPthreadStackSize(4096) + coordinator.EnableLTOPluginMarkers(true) + coordinator.EnableFuncInfoMetadata(true) + coordinator.EnableFuncInfoSites(true) + coordinator.SetDebugInfoOptimized(false) + + pkg := types.NewPackage("example.com/p", "p") + fset := token.NewFileSet() + coordinator.SetLinkname("example.com/p.Entry", "entry") + coordinator.SetPackageExport("example.com/p.Entry", "entry") + coordinator.SetNoInterfaceMethod("example.com/p.T.Hidden") + coordinator.SetTypeBackground("example.com/p.CType", InC) + coordinator.SetClosureEnvDirective(fset, "example.com/p.Entry", token.Pos(7)) + coordinator.MarkPackageSyntaxParsed(pkg) + coordinator.SetLocalityInfo("example.com/p.Value", LocalityInfo{Locality: ThreadLocal}) + coordinator.SetPython(func() *types.Package { return nil }) + + backend := coordinator.NewBackendProgram() + defer backend.Dispose() + if backend.ctx.C == coordinator.ctx.C { + t.Fatal("backend Program shares the coordinator LLVM context") + } + if backend.tm.C == coordinator.tm.C { + t.Fatal("backend Program shares the coordinator TargetMachine") + } + if backend.packageSyntax != coordinator.packageSyntax || backend.localities != coordinator.localities { + t.Fatal("backend Program did not share prepared Go metadata") + } + if backend.gocvt.typs == nil || len(backend.gocvt.typs) != 0 || backend.named == nil || backend.abiSymbol == nil { + t.Fatal("backend Program did not start with fresh lowering caches") + } + if link, ok := backend.Linkname("example.com/p.Entry"); !ok || link != "entry" { + t.Fatalf("Linkname = (%q, %v), want (entry, true)", link, ok) + } + if export, ok := backend.PackageExport("example.com/p.Entry"); !ok || export != "entry" { + t.Fatalf("PackageExport = (%q, %v), want (entry, true)", export, ok) + } + if !backend.HasClosureEnvDirective(fset, "example.com/p.Entry", token.Pos(7)) || !backend.PackageSyntaxParsed(pkg) { + t.Fatal("backend Program lost prepared syntax metadata") + } + if background, ok := backend.packageTypeBackground("example.com/p.CType"); !ok || background != InC { + t.Fatalf("type background = (%v, %v), want (InC, true)", background, ok) + } + if locality, ok := backend.VariableLocality("example.com/p.Value"); !ok || locality.Locality != ThreadLocal { + t.Fatalf("locality = (%+v, %v), want ThreadLocal", locality, ok) + } + if backend.python() != nil { + t.Fatal("backend Program changed the prepared optional Python package") + } + if !backend.disableBoundsChecks || !backend.enableGoGlobalDCE || !backend.enableDeadcodeDrop || + backend.pthreadStackSize != 4096 || !backend.enableLTOPluginMarker || + !backend.enableFuncInfoMetadata || !backend.enableFuncInfoSites || backend.debugInfoOptimized { + t.Fatal("backend Program did not preserve coordinator configuration") + } +} diff --git a/ssa/locality.go b/ssa/locality.go index 0e9b41e6cd..172a6b8cff 100644 --- a/ssa/locality.go +++ b/ssa/locality.go @@ -58,7 +58,6 @@ type localityInfos struct { ownerlessEntries map[string]VariableLocality declarationEntries map[string]map[string]VariableLocality activePackages map[string]struct{} - parsedPackages map[*types.Package]struct{} } func newLocalityInfos() *localityInfos { @@ -67,7 +66,6 @@ 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{}), } } @@ -293,12 +291,12 @@ 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 { + p.packageSyntax.mu.RLock() + links := make(map[string]string, len(p.packageSyntax.linknames)) + for name, target := range p.packageSyntax.linknames { links[name] = strings.TrimPrefix(target, "go:") } - p.linknameMu.RUnlock() + p.packageSyntax.mu.RUnlock() for name := range links { if strings.HasPrefix(name, prefix) && linknameReachesLocal(name, links, localNames) { nameSet[name] = true @@ -330,16 +328,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.packageSyntaxParsed(pkg) } func (p Program) MarkPackageSyntaxParsed(pkg *types.Package) { - p.localities.mu.Lock() - p.localities.parsedPackages[pkg] = struct{}{} - p.localities.mu.Unlock() + p.markPackageSyntaxParsed(pkg) } // PackageLocalities returns the legacy canonical-only metadata view. Its diff --git a/ssa/package.go b/ssa/package.go index aaf91dbb25..dacee7f3b8 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,18 +308,47 @@ func NewProgram(target *Target) Program { ctx.Finalize() */ is32Bits := (td.PointerSize() == 4 || is32Bits(target.GOARCH)) + packageSyntax := newPackageSyntaxData() prog := &aProgram{ - ctx: ctx, gocvt: newGoTypes(), + ctx: ctx, gocvt: newGoTypes(packageSyntax), 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: packageSyntax, localities: newLocalityInfos(), + abiSymbol: make(map[string]*AbiSymbol), debugInfoOptimized: target.effectiveOptLevel() != optlevel.O0, } prog.abi.Init(uintptr(prog.ptrSize), (*goProgram)(unsafe.Pointer(prog))) return prog } +// NewBackendProgram creates a Program with fresh LLVM-owned state and the same +// build configuration as p. Go-side package syntax and locality metadata are +// shared directly; callers must finish preparing them before backend Programs +// are created and use them read-only afterwards. +func (p Program) NewBackendProgram() Program { + var target *Target + if p.target != nil { + targetCopy := *p.target + target = &targetCopy + } + backend := NewProgram(target) + backend.sizes = p.sizes + backend.rt, backend.rtget = p.rt, p.rtget + backend.py, backend.pyget = p.py, p.pyget + backend.packageSyntax = p.packageSyntax + backend.gocvt.packageSyntax = p.packageSyntax + backend.localities = p.localities + backend.enableGoGlobalDCE = p.enableGoGlobalDCE + backend.enableDeadcodeDrop = p.enableDeadcodeDrop + backend.disableBoundsChecks = p.disableBoundsChecks + backend.pthreadStackSize = p.pthreadStackSize + backend.enableLTOPluginMarker = p.enableLTOPluginMarker + backend.enableFuncInfoMetadata = p.enableFuncInfoMetadata + backend.enableFuncInfoSites = p.enableFuncInfoSites + backend.debugInfoOptimized = p.debugInfoOptimized + return backend +} + func (p Program) Target() *Target { return p.target } @@ -389,7 +414,9 @@ func (p Program) SetDebugInfoOptimized(enable bool) { } func (p Program) SetNoInterfaceMethod(fullName string) { - p.noInterface[fullName] = none{} + p.packageSyntax.mu.Lock() + p.packageSyntax.noInterface[fullName] = none{} + p.packageSyntax.mu.Unlock() } func (p Program) isNoInterfaceMethod(fn *types.Func) bool { @@ -400,7 +427,9 @@ 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)] + p.packageSyntax.mu.RLock() + _, ok = p.packageSyntax.noInterface[FuncName(fn.Pkg(), fn.Name(), sig.Recv(), true)] + p.packageSyntax.mu.RUnlock() return ok } @@ -416,19 +445,21 @@ func (p Program) SetRuntime(runtime any) { } func (p Program) SetTypeBackground(fullName string, bg Background) { - p.gocvt.typbg.Store(fullName, bg) + p.packageSyntax.mu.Lock() + p.packageSyntax.typeBackgrounds[fullName] = bg + p.packageSyntax.mu.Unlock() } func (p Program) SetLinkname(name, link string) { - p.linknameMu.Lock() - p.linkname[name] = link - p.linknameMu.Unlock() + p.packageSyntax.mu.Lock() + p.packageSyntax.linknames[name] = link + p.packageSyntax.mu.Unlock() } func (p Program) Linkname(name string) (link string, ok bool) { - p.linknameMu.RLock() - link, ok = p.linkname[name] - p.linknameMu.RUnlock() + p.packageSyntax.mu.RLock() + link, ok = p.packageSyntax.linknames[name] + p.packageSyntax.mu.RUnlock() return } @@ -443,14 +474,18 @@ 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.mu.Lock() + p.packageSyntax.closureEnvDirectives[key] = none{} + p.packageSyntax.mu.Unlock() } // 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) + p.packageSyntax.mu.RLock() + _, ok := p.packageSyntax.closureEnvDirectives[key] + p.packageSyntax.mu.RUnlock() return ok } diff --git a/ssa/package_syntax.go b/ssa/package_syntax.go new file mode 100644 index 0000000000..6e3bdbe2b5 --- /dev/null +++ b/ssa/package_syntax.go @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ssa + +import ( + "go/types" + "sync" +) + +// packageSyntaxData is Go-owned metadata collected before LLVM package +// lowering. Build creates backend Programs only after this data is complete, +// so those Programs can share it directly for concurrent read-only access. +// One-shot compiler users keep the same Program-local mutation behavior. +type packageSyntaxData struct { + mu sync.RWMutex + 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), + } +} + +// SetPackageExport records an export directive before the LLVM Package that +// will later preserve the symbol exists. +func (p Program) SetPackageExport(name, export string) { + p.packageSyntax.mu.Lock() + p.packageSyntax.exports[name] = export + p.packageSyntax.mu.Unlock() +} + +// PackageExport returns the preloaded export name 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 Program) packageSyntaxParsed(pkg *types.Package) bool { + p.packageSyntax.mu.RLock() + _, ok := p.packageSyntax.parsedPackages[pkg] + p.packageSyntax.mu.RUnlock() + return ok +} + +func (p Program) markPackageSyntaxParsed(pkg *types.Package) { + p.packageSyntax.mu.Lock() + p.packageSyntax.parsedPackages[pkg] = struct{}{} + p.packageSyntax.mu.Unlock() +} + +func (p Program) packageTypeBackground(name string) (Background, bool) { + p.packageSyntax.mu.RLock() + background, ok := p.packageSyntax.typeBackgrounds[name] + p.packageSyntax.mu.RUnlock() + return background, ok +} + +func (p *packageSyntaxData) typeBackground(name string) (Background, bool) { + p.mu.RLock() + background, ok := p.typeBackgrounds[name] + p.mu.RUnlock() + return background, ok +} diff --git a/ssa/type.go b/ssa/type.go index c635cf16e9..7a84e67737 100644 --- a/ssa/type.go +++ b/ssa/type.go @@ -130,7 +130,8 @@ 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 { + prog := Program(unsafe.Pointer(p)) + if background, ok := prog.packageTypeBackground(namedLinkname(t)); ok && background == InC { return 0 } typ = t.Underlying() diff --git a/ssa/type_cvt.go b/ssa/type_cvt.go index a5302a77b4..f8d172fd87 100644 --- a/ssa/type_cvt.go +++ b/ssa/type_cvt.go @@ -21,7 +21,6 @@ import ( "go/token" "go/types" "reflect" - "sync" "unsafe" ) @@ -29,16 +28,24 @@ import ( type goTypes struct { // typs and cvtneed are owned by the single lowering goroutine for one - // Program. typbg is populated during concurrent package syntax preloading, - // before lowering starts, so it remains a sync.Map. - typs map[unsafe.Pointer]unsafe.Pointer - cvtneed map[*types.Named]conversionRequirement - typbg sync.Map + // Program. packageSyntax is prepared before backend lowering and shared + // read-only by Programs with independent LLVM contexts. + typs map[unsafe.Pointer]unsafe.Pointer + cvtneed map[*types.Named]conversionRequirement + packageSyntax *packageSyntaxData } -func newGoTypes() goTypes { +func newGoTypes(syntax ...*packageSyntaxData) goTypes { + packageSyntax := newPackageSyntaxData() + if len(syntax) != 0 && syntax[0] != nil { + packageSyntax = syntax[0] + } typs := make(map[unsafe.Pointer]unsafe.Pointer) - return goTypes{typs: typs, cvtneed: make(map[*types.Named]conversionRequirement)} + return goTypes{ + typs: typs, + cvtneed: make(map[*types.Named]conversionRequirement), + packageSyntax: packageSyntax, + } } type conversionRequirement uint8 @@ -174,8 +181,8 @@ func namedLinkname(t *types.Named) string { } func (p goTypes) shouldConvertNamed(t *types.Named) bool { - v, ok := p.typbg.Load(namedLinkname(t)) - return !ok || v.(Background) != InC + background, ok := p.packageSyntax.typeBackground(namedLinkname(t)) + return !ok || background != InC } func (p goTypes) cvtNamed(t *types.Named) (raw *types.Named, cvt bool) {