diff --git a/internal/build/build.go b/internal/build/build.go index f9cf2a1cf9..a668b83bc0 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -349,6 +349,13 @@ func (c *Config) deadcodeDropEnabled() bool { return buildenv.Dev && c.DeadcodeDrop && !c.goGlobalDCEEnabled() } +// thinLTODeadcodeEnabled selects the package-owned rewrite path. ThinLTO +// packages are materialized only after the link-specific Go plan is known so +// the bitcode and its ThinLTO summary describe the rewritten method tables. +func (c *Config) thinLTODeadcodeEnabled() bool { + return c != nil && c.deadcodeDropEnabled() && c.ltoMode() == lto.Thin +} + func (c *Config) packageMetaEnabled() bool { return c.CollectPackageMeta || c.deadcodeDropEnabled() } @@ -1190,7 +1197,13 @@ func prePackageBuild(ctx *context, task *packageBuildTask, verbose bool) error { if err := ctx.collectFingerprint(aPkg); err != nil { return err } - ctx.tryLoadFromCache(aPkg) + // The ThinLTO writeback mode needs the package-owned LLVM module and must + // materialize a link-specific archive after the global plan is known. A + // regular package cache hit only contains the already-published archive, so + // keep this first implementation on the source-build path. + if !ctx.buildConf.thinLTODeadcodeEnabled() { + ctx.tryLoadFromCache(aPkg) + } if verbose { status := "MISS" if aPkg.CacheHit { @@ -1220,6 +1233,13 @@ func finalizePackageBuild(ctx *context, task *packageBuildTask, verbose bool) er if aPkg.CacheHit { return nil } + if ctx.buildConf.thinLTODeadcodeEnabled() { + // Package export is deferred until linkMainPkg computes the global plan. + if task.kind == cl.PkgLinkExtern { + appendExternalLinkArgs(ctx, aPkg, task.kindParam) + } + return nil + } if err := normalizeToArchive(ctx, aPkg, verbose); err != nil { return err } @@ -1517,6 +1537,30 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa linkArgs = append(linkArgs, rtLinkArgs...) archiveInputs = append(archiveInputs, rtLinkInputs...) } + if ctx.buildConf.thinLTODeadcodeEnabled() { + plan, err := buildDeadcodePlan(linkedOrder, needRuntime) + if err != nil { + return err + } + if err := materializeThinLTODeadcodePlan(ctx, linkedOrder, plan, verbose); err != nil { + return err + } + // Package archives were intentionally deferred until the global plan + // was available. Rebuild the link inputs from the rewritten archives. + archiveInputs = archiveInputs[:0] + for _, aPkg := range linkedOrder { + if aPkg == nil || aPkg.ArchiveFile == "" { + continue + } + if isRuntimePkg(aPkg.PkgPath) { + if needRuntime || needPyInit || ctx.buildConf.Target == "" { + archiveInputs = append(archiveInputs, aPkg.ArchiveFile) + } + continue + } + archiveInputs = append(archiveInputs, aPkg.ArchiveFile) + } + } // Generate main module file (needed for global variables even in library modes) // This is compiled directly to .o and added to linkInputs (not cached) @@ -1543,7 +1587,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa funcInfo: funcInfo, pcLineInfo: pcLineInfo, }) - if ctx.buildConf.deadcodeDropEnabled() { + if ctx.buildConf.deadcodeDropEnabled() && !ctx.buildConf.thinLTODeadcodeEnabled() { if err := applyDeadcodeDropOverrides(linkedOrder, entryPkg, needRuntime, verbose); err != nil { return err } @@ -1591,11 +1635,21 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa func linkedPackageMetas(pkgs []Package) []*meta.PackageMeta { metas := make([]*meta.PackageMeta, 0, len(pkgs)) for _, pkg := range pkgs { - metas = append(metas, pkg.Meta) + if pkg != nil && pkg.Meta != nil { + metas = append(metas, pkg.Meta) + } } return metas } +func buildDeadcodePlan(pkgs []Package, needRuntime bool) (map[string][]int, error) { + summary, err := meta.NewGlobalSummary(linkedPackageMetas(pkgs)) + if err != nil { + return nil, err + } + return deadcode.Analyze(summary, dceEntryRootCandidates(pkgs, needRuntime)), nil +} + func applyDeadcodeDropOverrides(pkgs []Package, entryPkg Package, needRuntime bool, verbose bool) error { metas := linkedPackageMetas(pkgs) summary, err := meta.NewGlobalSummary(metas) @@ -1609,6 +1663,78 @@ func applyDeadcodeDropOverrides(pkgs []Package, entryPkg Package, needRuntime bo return nil } +// materializeThinLTODeadcodePlan keeps the original package module untouched, +// writes it as canonical ThinLTO bitcode, and applies the link-specific plan to +// a fresh parsed module before creating the package archive. This is the +// package-owned counterpart to applyDeadcodeDropOverrides: no same-name global +// is emitted in the entry module. +func materializeThinLTODeadcodePlan(ctx *context, pkgs []Package, liveSlots map[string][]int, verbose bool) error { + for _, aPkg := range pkgs { + if aPkg == nil || aPkg.LPkg == nil || aPkg.Package == nil || aPkg.Package.ExportFile == "" { + continue + } + if aPkg.CacheHit { + return fmt.Errorf("thin LTO deadcode cannot rewrite cached package %s", aPkg.PkgPath) + } + + canonical, err := writeCanonicalThinLTOBitcode(aPkg.LPkg.Module()) + if err != nil { + return fmt.Errorf("write canonical ThinLTO bitcode for %s: %w", aPkg.PkgPath, err) + } + func() { + defer os.Remove(canonical) + llvmCtx := gllvm.NewContext() + defer llvmCtx.Dispose() + mod, parseErr := llvmCtx.ParseBitcodeFile(canonical) + if parseErr != nil { + err = fmt.Errorf("parse canonical ThinLTO bitcode for %s: %w", aPkg.PkgPath, parseErr) + return + } + defer mod.Dispose() + dcepass.RewriteTypeMethodTables(mod, liveSlots, verbose) + buf := gllvm.WriteThinLTOBitcodeToMemoryBuffer(mod) + if buf.IsNil() { + err = fmt.Errorf("write rewritten ThinLTO bitcode for %s: empty buffer", aPkg.PkgPath) + return + } + aPkg.ObjBuffers = append(aPkg.ObjBuffers, packageArchiveBuffer{ + name: filepath.Base(aPkg.Package.ExportFile) + ".o", + buffer: buf, + }) + }() + if err != nil { + return err + } + if err := normalizeToArchive(ctx, aPkg, verbose); err != nil { + return fmt.Errorf("archive rewritten ThinLTO package %s: %w", aPkg.PkgPath, err) + } + } + return nil +} + +func writeCanonicalThinLTOBitcode(mod gllvm.Module) (string, error) { + buf := gllvm.WriteThinLTOBitcodeToMemoryBuffer(mod) + if buf.IsNil() { + return "", errors.New("empty ThinLTO bitcode buffer") + } + defer buf.Dispose() + f, err := os.CreateTemp("", "llgo-thinlto-canonical-*.bc") + if err != nil { + return "", err + } + name := f.Name() + if _, err := f.Write(buf.Bytes()); err != nil { + f.Close() + os.Remove(name) + return "", err + } + if err := f.Close(); err != nil { + os.Remove(name) + return "", err + } + return name, nil +} + func dceSourceModules(pkgs []Package) []gllvm.Module { mods := make([]gllvm.Module, 0, len(pkgs)) for _, pkg := range pkgs { @@ -2118,6 +2244,11 @@ func compilePackageModule(ctx *context, aPkg *aPackage, externs []string, verbos aPkg.LinkArgs = append(aPkg.LinkArgs, goCgoLinkArgs(ctx.buildConf.Goos, aPkg.AltPkg.Syntax)...) } if pkg.ExportFile != "" { + if ctx.buildConf.thinLTODeadcodeEnabled() { + // Keep the package module alive until linkMainPkg has merged all + // Meta and can apply one link-specific owner rewrite. + return nil + } exportFile, exportBuffer, err := exportPackageObject(ctx, pkg.PkgPath, pkg.ExportFile, ret) if err != nil { return fmt.Errorf("export object of %v failed: %v", pkgPath, err) diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 53c9d53d71..11f2c79205 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -1580,6 +1580,27 @@ func TestDeadcodeDropEnabled(t *testing.T) { } } +func TestThinLTODeadcodeEnabled(t *testing.T) { + tests := []struct { + name string + conf *Config + want bool + }{ + {name: "thin lto with deadcode drop", conf: &Config{DeadcodeDrop: true, LTO: lto.Thin}, want: buildenv.Dev}, + {name: "thin lto without deadcode drop", conf: &Config{LTO: lto.Thin}, want: false}, + {name: "full lto uses global dce", conf: &Config{DeadcodeDrop: true, LTO: lto.Full}, want: false}, + {name: "full lto with global dce disabled", conf: &Config{DeadcodeDrop: true, LTO: lto.Full, DisableGoGlobalDCE: true}, want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.conf.thinLTODeadcodeEnabled(); got != tt.want { + t.Fatalf("thinLTODeadcodeEnabled() = %v, want %v", got, tt.want) + } + }) + } +} + func TestPackageMetaEnabled(t *testing.T) { tests := []struct { name string diff --git a/internal/build/deadcode_test.go b/internal/build/deadcode_test.go index 622078ba48..9a0861d49f 100644 --- a/internal/build/deadcode_test.go +++ b/internal/build/deadcode_test.go @@ -1,6 +1,8 @@ package build import ( + "os" + "path/filepath" "reflect" "strings" "testing" @@ -86,6 +88,41 @@ func TestDCEEntryRootCandidatesIncludesCExports(t *testing.T) { } } +func TestWriteCanonicalThinLTOBitcodeRoundTrip(t *testing.T) { + ctx := llvm.NewContext() + defer ctx.Dispose() + path := filepath.Join("..", "dcepass", "testdata", "method_slots", "in.ll") + buf, err := llvm.NewMemoryBufferFromFile(path) + if err != nil { + t.Fatal(err) + } + mod, err := ctx.ParseIR(buf) + if err != nil { + t.Fatal(err) + } + defer mod.Dispose() + + canonical, err := writeCanonicalThinLTOBitcode(mod) + if err != nil { + t.Fatal(err) + } + defer os.Remove(canonical) + if info, err := os.Stat(canonical); err != nil || info.Size() == 0 { + t.Fatalf("canonical ThinLTO bitcode stat = (%v, %v)", info, err) + } + + parsedCtx := llvm.NewContext() + defer parsedCtx.Dispose() + parsed, err := parsedCtx.ParseBitcodeFile(canonical) + if err != nil { + t.Fatal(err) + } + defer parsed.Dispose() + if err := llvm.VerifyModule(parsed, llvm.ReturnStatusAction); err != nil { + t.Fatalf("canonical ThinLTO bitcode is invalid: %v", err) + } +} + func buildDeadcodeMeta(t *testing.T) *meta.PackageMeta { t.Helper() b := meta.NewBuilder() diff --git a/internal/dcepass/dcepass.go b/internal/dcepass/dcepass.go index 36f179c5a9..b9a7948d46 100644 --- a/internal/dcepass/dcepass.go +++ b/internal/dcepass/dcepass.go @@ -38,6 +38,89 @@ func EmitStrongTypeOverrides(dst llvm.Module, srcMods []llvm.Module, liveSlots m } } +// RewriteTypeMethodTables rewrites ABI method table initializers in mod in +// place. It preserves the package-owned global, including its linkage and +// COMDAT, so ThinLTO can build summaries from the rewritten definition without +// introducing an entry-module override. +// +// A missing type entry in liveSlots means that no method slot is demanded. The +// method name and type operands remain intact while IFn/TFn point to the +// runtime unreachable stub, preserving the ABI table shape and reflection +// matching metadata. +func RewriteTypeMethodTables(mod llvm.Module, liveSlots map[string][]int, verbose bool) int { + if mod.IsNil() { + return 0 + } + rewriter := &moduleRewriter{mod: mod} + rewritten := 0 + for g := mod.FirstGlobal(); !g.IsNil(); g = llvm.NextGlobal(g) { + if g.IsDeclaration() || !g.IsGlobalConstant() { + continue + } + methodsVal, elemTy, ok := methodArray(g.Initializer()) + if !ok { + continue + } + if rewriter.rewriteGlobal(g, methodsVal, elemTy, liveSlotSet(liveSlots[g.Name()]), verbose) { + rewritten++ + } + } + return rewritten +} + +type moduleRewriter struct { + mod llvm.Module + unreachable llvm.Value +} + +func (r *moduleRewriter) unreachableMethod() llvm.Value { + if r.unreachable.IsNil() { + r.unreachable = r.mod.NamedFunction(unreachableMethodName) + } + if r.unreachable.IsNil() { + r.unreachable = llvm.AddFunction(r.mod, unreachableMethodName, + llvm.FunctionType(r.mod.Context().VoidType(), nil, false)) + } + return r.unreachable +} + +func (r *moduleRewriter) rewriteGlobal(g, methodsVal llvm.Value, elemTy llvm.Type, keepIdx map[int]bool, verbose bool) bool { + init := g.Initializer() + if init.IsNil() || init.OperandsCount() == 0 { + return false + } + fields := make([]llvm.Value, init.OperandsCount()) + for i := range fields { + fields[i] = init.Operand(i) + } + methods := make([]llvm.Value, methodsVal.OperandsCount()) + dropped := false + for i := range methods { + orig := methodsVal.Operand(i) + if keepIdx[i] { + methods[i] = orig + continue + } + dropped = true + if verbose { + fmt.Fprintf(os.Stderr, "[dce] drop method %s[%d] ifn=%s tfn=%s\n", g.Name(), i, orig.Operand(2).Name(), orig.Operand(3).Name()) + } + unreachable := r.unreachableMethod() + methods[i] = llvm.ConstNamedStruct(elemTy, []llvm.Value{ + orig.Operand(0), + orig.Operand(1), + unreachable, + unreachable, + }) + } + if !dropped { + return false + } + fields[len(fields)-1] = llvm.ConstArray(elemTy, methods) + g.SetInitializer(constStructOfType(init.Type(), fields)) + return true +} + type overrideEmitter struct { dst llvm.Module values map[llvm.Value]llvm.Value diff --git a/internal/dcepass/dcepass_test.go b/internal/dcepass/dcepass_test.go index d05d1145de..e5fc6c429d 100644 --- a/internal/dcepass/dcepass_test.go +++ b/internal/dcepass/dcepass_test.go @@ -59,6 +59,49 @@ func TestEmitStrongTypeOverrides(t *testing.T) { } } +func TestRewriteTypeMethodTablesInPlace(t *testing.T) { + ctx := llvm.NewContext() + defer ctx.Dispose() + mod := parseModule(t, &ctx, filepath.Join("testdata", "method_slots", "in.ll")) + defer mod.Dispose() + + typeName := taskTypeName + g := mod.NamedGlobal(typeName) + if g.IsNil() { + t.Fatalf("missing package-owned type global %q", typeName) + } + linkage := g.Linkage() + if got := RewriteTypeMethodTables(mod, map[string][]int{typeName: {1}, ptrTaskTypeName: {1}}, false); got != 2 { + t.Fatalf("RewriteTypeMethodTables rewrote %d globals, want 2", got) + } + if got := g.Linkage(); got != linkage { + t.Fatalf("type global linkage changed from %v to %v", linkage, got) + } + + out := mod.String() + if !strings.Contains(out, `ptr @"main.(*Task).Run", ptr @main.Task.Run`) { + t.Fatalf("live method slot was not preserved:\n%s", out) + } + if strings.Contains(out, `ptr @"main.(*Task).Drop", ptr @"main.Task.Drop"`) { + t.Fatalf("dead method slot still references Drop:\n%s", out) + } + if !strings.Contains(out, unreachableMethodName) { + t.Fatalf("rewritten module does not reference unreachable method:\n%s", out) + } + count := 0 + for global := mod.FirstGlobal(); !global.IsNil(); global = llvm.NextGlobal(global) { + if global.Name() == typeName { + count++ + } + } + if count != 1 { + t.Fatalf("package type global count = %d, want exactly one", count) + } + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatalf("rewritten package module is invalid: %v\n%s", err, out) + } +} + func TestMethodArray(t *testing.T) { ctx := llvm.NewContext() defer ctx.Dispose()