From b5a81267a3e7ff6ce6345b61af4262b56f61d469 Mon Sep 17 00:00:00 2001 From: luoliwoshang <2643523683@qq.com> Date: Mon, 2 Mar 2026 17:55:49 +0800 Subject: [PATCH 01/10] build: switch package pipeline to bitcode LTO linking --- internal/build/build.go | 272 ++++++++++++++++++++++++++------- internal/build/cache.go | 15 +- internal/build/cache_test.go | 26 ++-- internal/build/collect.go | 26 ++-- internal/build/collect_test.go | 39 +++-- 5 files changed, 272 insertions(+), 106 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index 841a6dbf03..ad9e7d676f 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -363,6 +363,7 @@ func Do(args []string, conf *Config) ([]Package, error) { pkgByID: map[string]Package{}, output: output, passOpt: passOpt, + bitcodeLTO: true, buildConf: conf, crossCompile: export, cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize), @@ -543,6 +544,9 @@ type context struct { plan9asmAll bool // when plan9asmAll=false: enabled set; when plan9asmAll=true: excluded set. plan9asmPkgs map[string]bool + + // Enable package bitcode outputs and final LLVM API linking. + bitcodeLTO bool } func (c *context) compiler() *clang.Cmd { @@ -580,30 +584,128 @@ func (c *context) hasAltPkg(pkgPath string) bool { return hasAltPkgForTarget(c.buildConf, pkgPath) } -// normalizeToArchive creates an archive from object files and sets ArchiveFile. -// This ensures the link step always consumes .a archives regardless of cache state. -func normalizeToArchive(ctx *context, aPkg *aPackage, verbose bool) error { - if len(aPkg.ObjFiles) == 0 { - return nil +func splitBitcodeAndNativeInputs(inputs []string) (bitcodeFiles []string, nativeInputs []string) { + for _, input := range inputs { + if strings.HasSuffix(input, ".bc") { + bitcodeFiles = append(bitcodeFiles, input) + continue + } + nativeInputs = append(nativeInputs, input) + } + return +} + +func tempNamePrefix(moduleName string) string { + name := strings.ReplaceAll(moduleName, "/", "_") + name = strings.ReplaceAll(name, "\\", "_") + name = strings.ReplaceAll(name, ":", "_") + name = strings.ReplaceAll(name, ".", "_") + if name == "" { + return "llgo" + } + return name +} + +func mergeBitcodeFiles(moduleName string, bitcodeFiles []string) (string, error) { + if len(bitcodeFiles) == 0 { + return "", nil } + if len(bitcodeFiles) == 1 { + return bitcodeFiles[0], nil + } + + ctx := gllvm.NewContext() + defer ctx.Dispose() - archiveFile, err := os.CreateTemp("", "pkg-*.a") + mod, err := ctx.ParseBitcodeFile(bitcodeFiles[0]) if err != nil { - return fmt.Errorf("create temp archive: %w", err) + return "", fmt.Errorf("parse bitcode %s: %w", bitcodeFiles[0], err) + } + defer mod.Dispose() + + for _, bitcodeFile := range bitcodeFiles[1:] { + srcMod, err := ctx.ParseBitcodeFile(bitcodeFile) + if err != nil { + return "", fmt.Errorf("parse bitcode %s: %w", bitcodeFile, err) + } + if err := gllvm.LinkModules(mod, srcMod); err != nil { + return "", fmt.Errorf("link bitcode module %s: %w", bitcodeFile, err) + } + } + + out, err := os.CreateTemp("", tempNamePrefix(moduleName)+"-*.bc") + if err != nil { + return "", fmt.Errorf("create merged bitcode file: %w", err) + } + defer out.Close() + + if err := gllvm.WriteBitcodeToFile(mod, out); err != nil { + return "", fmt.Errorf("write merged bitcode file: %w", err) } - archiveFile.Close() - archivePath := archiveFile.Name() + return out.Name(), nil +} - if err := ctx.createArchiveFile(archivePath, aPkg.ObjFiles, verbose); err != nil { - os.Remove(archivePath) - return fmt.Errorf("create archive for %s: %w", aPkg.PkgPath, err) +func normalizePackageOutputs(ctx *context, aPkg *aPackage, verbose bool) error { + if len(aPkg.ObjFiles) == 0 { + return nil } + bitcodeFiles, nativeInputs := splitBitcodeAndNativeInputs(aPkg.ObjFiles) aPkg.ObjFiles = nil - aPkg.ArchiveFile = archivePath + + if len(bitcodeFiles) > 0 { + mergedBitcode, err := mergeBitcodeFiles(aPkg.PkgPath, bitcodeFiles) + if err != nil { + return fmt.Errorf("merge bitcode for %s: %w", aPkg.PkgPath, err) + } + aPkg.BitcodeFile = mergedBitcode + } + + if len(nativeInputs) > 0 { + archiveFile, err := os.CreateTemp("", "pkg-native-*.a") + if err != nil { + return fmt.Errorf("create temp native archive: %w", err) + } + archiveFile.Close() + archivePath := archiveFile.Name() + + if err := ctx.createArchiveFile(archivePath, nativeInputs, verbose); err != nil { + os.Remove(archivePath) + return fmt.Errorf("create native archive for %s: %w", aPkg.PkgPath, err) + } + aPkg.ArchiveFile = archivePath + } return nil } +func compileLinkedBitcodeToObject(ctx *context, moduleName string, bitcodeFiles []string, verbose bool) (string, error) { + if len(bitcodeFiles) == 0 { + return "", nil + } + + mergedBitcode, err := mergeBitcodeFiles(moduleName, bitcodeFiles) + if err != nil { + return "", err + } + + objFile, err := os.CreateTemp("", tempNamePrefix(moduleName)+"-*.o") + if err != nil { + return "", fmt.Errorf("create linked object temp file: %w", err) + } + objFile.Close() + + args := []string{"-o", objFile.Name(), "-c", mergedBitcode, "-Wno-override-module"} + if ctx.shouldPrintCommands(verbose) { + fmt.Fprintf(os.Stderr, "# compiling linked bitcode for %s\n", moduleName) + fmt.Fprintln(os.Stderr, "clang", args) + } + if err := ctx.compiler().Compile(args...); err != nil { + return "", fmt.Errorf("compile linked bitcode for %s: %w", moduleName, err) + } + + return objFile.Name(), nil +} + func buildAllPkgs(ctx *context, pkgs []*aPackage, verbose bool) ([]*aPackage, error) { built := ctx.built @@ -648,7 +750,7 @@ func buildAllPkgs(ctx *context, pkgs []*aPackage, verbose bool) ([]*aPackage, er return err } if !aPkg.CacheHit { - if err := normalizeToArchive(ctx, aPkg, verbose); err != nil { + if err := normalizePackageOutputs(ctx, aPkg, verbose); err != nil { return err } if kind == cl.PkgLinkExtern { @@ -683,7 +785,7 @@ func buildAllPkgs(ctx *context, pkgs []*aPackage, verbose bool) ([]*aPackage, er needRuntime = needRuntime || aPkg.NeedRt needPyInit = needPyInit || aPkg.NeedPyInit if !aPkg.CacheHit { - if err := normalizeToArchive(ctx, aPkg, verbose); err != nil { + if err := normalizePackageOutputs(ctx, aPkg, verbose); err != nil { return err } if err := ctx.saveToCache(aPkg); err != nil && verbose { @@ -815,14 +917,15 @@ func validateRewriteInput(pkg, varName, value string) { } } -// compileExtraFiles compiles extra files (.s/.c) from target configuration and returns object files +// compileExtraFiles compiles extra files (.s/.c) from target configuration and returns link inputs. +// C-like files are emitted as .bc for LTO while assembly remains .o. func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { if len(ctx.crossCompile.ExtraFiles) == 0 { return nil, nil } printCmds := ctx.shouldPrintCommands(verbose) - var objFiles []string + var linkInputs []string llgoRoot := env.LLGoROOT() for _, extraFile := range ctx.crossCompile.ExtraFiles { @@ -855,8 +958,10 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { baseArgs = append(baseArgs, "-x", "assembler-with-cpp") } - // If GenLL is enabled, first emit .ll for debugging - if ctx.buildConf.GenLL { + emitBitcode := ext != ".S" && ext != ".s" + + // If GenLL is enabled, first emit .ll for debugging. + if ctx.buildConf.GenLL && emitBitcode { llFile := baseName + ".ll" llArgs := append(slices.Clone(baseArgs), "-emit-llvm", "-S", "-o", llFile, "-c", srcFile) if printCmds { @@ -868,22 +973,33 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { } } - // Always compile to .o for linking - objFile := baseName + ".o" - objArgs := append(baseArgs, "-o", objFile, "-c", srcFile) - if printCmds { - fmt.Fprintf(os.Stderr, "Compiling extra file: clang %s\n", strings.Join(objArgs, " ")) - } - cmd := ctx.compiler() - if err := cmd.Compile(objArgs...); err != nil { - return nil, fmt.Errorf("failed to compile extra file %s: %w", srcFile, err) + if emitBitcode { + bcFile := baseName + ".bc" + bcArgs := append(baseArgs, "-emit-llvm", "-o", bcFile, "-c", srcFile) + if printCmds { + fmt.Fprintf(os.Stderr, "Compiling extra file (bc): clang %s\n", strings.Join(bcArgs, " ")) + } + cmd := ctx.compiler() + if err := cmd.Compile(bcArgs...); err != nil { + return nil, fmt.Errorf("failed to compile extra file %s to .bc: %w", srcFile, err) + } + linkInputs = append(linkInputs, bcFile) + } else { + objFile := baseName + ".o" + objArgs := append(baseArgs, "-o", objFile, "-c", srcFile) + if printCmds { + fmt.Fprintf(os.Stderr, "Compiling extra file: clang %s\n", strings.Join(objArgs, " ")) + } + cmd := ctx.compiler() + if err := cmd.Compile(objArgs...); err != nil { + return nil, fmt.Errorf("failed to compile extra file %s: %w", srcFile, err) + } + linkInputs = append(linkInputs, objFile) } - - objFiles = append(objFiles, objFile) os.Remove(baseName) // Remove the temp file we created for naming } - return objFiles, nil + return linkInputs, nil } func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPath string, verbose bool) error { @@ -894,10 +1010,13 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa for _, v := range pkgs { allPkgs = append(allPkgs, v.Package) } - // linkInputs contains .a archives from all packages and .o files from main module - var linkInputs []string + // Bitcode modules are linked together first using LLVM APIs, then compiled to + // one native object and linked with native archives/objects. + var linkBitcodeInputs []string + var nativeLinkInputs []string var linkArgs []string - var rtLinkInputs []string + var rtBitcodeInputs []string + var rtNativeInputs []string var rtLinkArgs []string linkedPkgs := make(map[string]bool) // Track linked packages by ID to avoid duplicates packages.Visit(allPkgs, nil, func(p *packages.Package) { @@ -916,8 +1035,11 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa // Defer linking runtime packages unless we actually need the runtime. if isRuntimePkg(p.PkgPath) { rtLinkArgs = append(rtLinkArgs, aPkg.LinkArgs...) + if aPkg.BitcodeFile != "" { + rtBitcodeInputs = append(rtBitcodeInputs, aPkg.BitcodeFile) + } if aPkg.ArchiveFile != "" { - rtLinkInputs = append(rtLinkInputs, aPkg.ArchiveFile) + rtNativeInputs = append(rtNativeInputs, aPkg.ArchiveFile) } return } else { @@ -931,8 +1053,11 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa } linkArgs = append(linkArgs, aPkg.LinkArgs...) + if aPkg.BitcodeFile != "" { + linkBitcodeInputs = append(linkBitcodeInputs, aPkg.BitcodeFile) + } if aPkg.ArchiveFile != "" { - linkInputs = append(linkInputs, aPkg.ArchiveFile) + nativeLinkInputs = append(nativeLinkInputs, aPkg.ArchiveFile) } } }) @@ -940,25 +1065,40 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa // Only link runtime objects when needed (or for host builds where runtime is always required). if needRuntime || needPyInit || ctx.buildConf.Target == "" { linkArgs = append(linkArgs, rtLinkArgs...) - linkInputs = append(linkInputs, rtLinkInputs...) + linkBitcodeInputs = append(linkBitcodeInputs, rtBitcodeInputs...) + nativeLinkInputs = append(nativeLinkInputs, rtNativeInputs...) } // Generate main module file (needed for global variables even in library modes) - // This is compiled directly to .o and added to linkInputs (not cached) + // This is compiled to .bc and included in the program-level bitcode link (not cached). // Use a stable synthetic name to avoid confusing it with the real main package in traces/logs. entryPkg := genMainModule(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit, needAbiInit) - entryObjFile, err := exportObject(ctx, "entry_main", entryPkg.ExportFile, []byte(entryPkg.LPkg.String())) + entryBitcodeFile, err := exportObject(ctx, "entry_main", entryPkg.ExportFile, []byte(entryPkg.LPkg.String())) if err != nil { return err } - linkInputs = append(linkInputs, entryObjFile) + linkBitcodeInputs = append(linkBitcodeInputs, entryBitcodeFile) // Compile extra files from target configuration - extraObjFiles, err := compileExtraFiles(ctx, verbose) + extraLinkInputs, err := compileExtraFiles(ctx, verbose) if err != nil { return err } - linkInputs = append(linkInputs, extraObjFiles...) + extraBitcodeInputs, extraNativeInputs := splitBitcodeAndNativeInputs(extraLinkInputs) + linkBitcodeInputs = append(linkBitcodeInputs, extraBitcodeInputs...) + nativeLinkInputs = append(nativeLinkInputs, extraNativeInputs...) + + if ctx.bitcodeLTO { + programObjFile, err := compileLinkedBitcodeToObject(ctx, pkg.PkgPath, linkBitcodeInputs, verbose) + if err != nil { + return err + } + if programObjFile != "" { + nativeLinkInputs = append(nativeLinkInputs, programObjFile) + } + } else { + nativeLinkInputs = append(nativeLinkInputs, linkBitcodeInputs...) + } if IsFullRpathEnabled() { // Treat every link-time library search path, specified by the -L parameter, as a runtime search path as well. @@ -977,7 +1117,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa } } - err = linkObjFiles(ctx, outputPath, linkInputs, linkArgs, verbose) + err = linkObjFiles(ctx, outputPath, nativeLinkInputs, linkArgs, verbose) if err != nil { return err } @@ -1260,24 +1400,24 @@ func exportObject(ctx *context, pkgPath string, exportFile string, data []byte) if err := os.Chmod(f.Name(), 0644); err != nil { return "", err } - // Copy instead of rename so we can still compile to .o + // Copy instead of rename so we can still compile to .bc if err := copyFileAtomic(f.Name(), llFile); err != nil { return "", err } } - // Always compile .ll to .o for linking - objFile, err := os.CreateTemp("", base+"-*.o") + // Compile .ll to .bc for program-level bitcode linking. + bitcodeFile, err := os.CreateTemp("", base+"-*.bc") if err != nil { return "", err } - objFile.Close() - args := []string{"-o", objFile.Name(), "-c", f.Name(), "-Wno-override-module"} + bitcodeFile.Close() + args := []string{"-o", bitcodeFile.Name(), "-emit-llvm", "-c", f.Name(), "-Wno-override-module"} if ctx.shouldPrintCommands(false) { fmt.Fprintf(os.Stderr, "# compiling %s for pkg: %s\n", f.Name(), pkgPath) fmt.Fprintln(os.Stderr, "clang", args) } cmd := ctx.compiler() - return objFile.Name(), cmd.Compile(args...) + return bitcodeFile.Name(), cmd.Compile(args...) } func llcCheck(env *llvm.Env, exportFile string) (msg string, err error) { @@ -1341,8 +1481,9 @@ type aPackage struct { NeedPyInit bool LinkArgs []string - ObjFiles []string // object files: .o or .ll (output of compiler, input to archiver) - ArchiveFile string // archive file: .a (output of archiver, used for linking) + ObjFiles []string // per-file outputs before normalization (.bc/.o) + BitcodeFile string // merged package bitcode (.bc) + ArchiveFile string // optional native archive (.a) for non-bitcode objects rewriteVars map[string]string // Cache related fields @@ -1381,6 +1522,7 @@ func buildSSAPkgs(ctx *context, initial []*packages.Package, verbose bool) ([]*a NeedPyInit: false, LinkArgs: nil, ObjFiles: nil, + BitcodeFile: "", rewriteVars: rewrites, } ctx.pkgs[p] = aPkg @@ -1650,17 +1792,20 @@ func clFiles(ctx *context, files string, pkg *packages.Package, procFile func(li func clFile(ctx *context, args []string, cFile, expFile, pkgPath string, procFile func(linkFile string), verbose bool) { baseName := expFile + filepath.Base(cFile) ext := filepath.Ext(cFile) + compileArgs := slices.Clone(args) // default clang++ will use c++ to compile c file,will cause symbol be mangled if ext == ".c" { - args = append(args, "-x", "c") + compileArgs = append(compileArgs, "-x", "c") } - // If GenLL is enabled, first emit .ll for debugging, then compile to .o + emitBitcode := ext != ".S" && ext != ".s" + + // If GenLL is enabled, first emit .ll for debugging. printCmds := ctx.shouldPrintCommands(verbose) - if ctx.buildConf.GenLL { + if ctx.buildConf.GenLL && emitBitcode { llFile := baseName + ".ll" - llArgs := append(slices.Clone(args), "-emit-llvm", "-S", "-o", llFile, "-c", cFile) + llArgs := append(slices.Clone(compileArgs), "-emit-llvm", "-S", "-o", llFile, "-c", cFile) if printCmds { fmt.Fprintf(os.Stderr, "# compiling %s for pkg: %s\n", llFile, pkgPath) fmt.Fprintln(os.Stderr, "clang", llArgs) @@ -1670,9 +1815,22 @@ func clFile(ctx *context, args []string, cFile, expFile, pkgPath string, procFil check(err) } - // Always compile to .o for linking + if emitBitcode { + bcFile := baseName + ".bc" + bcArgs := append(compileArgs, "-emit-llvm", "-o", bcFile, "-c", cFile) + if printCmds { + fmt.Fprintf(os.Stderr, "# compiling %s for pkg: %s\n", bcFile, pkgPath) + fmt.Fprintln(os.Stderr, "clang", bcArgs) + } + cmd := ctx.compiler() + err := cmd.Compile(bcArgs...) + check(err) + procFile(bcFile) + return + } + objFile := baseName + ".o" - objArgs := append(args, "-o", objFile, "-c", cFile) + objArgs := append(compileArgs, "-o", objFile, "-c", cFile) if printCmds { fmt.Fprintf(os.Stderr, "# compiling %s for pkg: %s\n", objFile, pkgPath) fmt.Fprintln(os.Stderr, "clang", objArgs) diff --git a/internal/build/cache.go b/internal/build/cache.go index f8870f6d92..30d25bc9c7 100644 --- a/internal/build/cache.go +++ b/internal/build/cache.go @@ -29,6 +29,7 @@ import ( const ( cacheBuildDirName = "build" + cacheBitcodeExt = ".bc" cacheArchiveExt = ".a" cacheManifestExt = ".manifest" ) @@ -54,7 +55,8 @@ func newCacheManager() *cacheManager { // cachePaths holds the paths for a cached package type cachePaths struct { Dir string // Directory containing cache files - Archive string // Path to .a file + Bitcode string // Path to .bc file + Archive string // Path to optional native .a file Manifest string // Path to .manifest file } @@ -64,6 +66,7 @@ func (cm *cacheManager) PackagePaths(targetTriple, pkgPath, fingerprint string) fingerprint = sanitizeComponent(fingerprint) return cachePaths{ Dir: dir, + Bitcode: filepath.Join(dir, fingerprint+cacheBitcodeExt), Archive: filepath.Join(dir, fingerprint+cacheArchiveExt), Manifest: filepath.Join(dir, fingerprint+cacheManifestExt), } @@ -176,8 +179,8 @@ func readManifest(path string) (string, error) { // cacheExists checks if a valid cache entry exists func (cm *cacheManager) cacheExists(paths cachePaths) bool { - // Both archive and manifest must exist - if _, err := os.Stat(paths.Archive); err != nil { + // Bitcode and manifest must exist. Native archive is optional. + if _, err := os.Stat(paths.Bitcode); err != nil { return false } if _, err := os.Stat(paths.Manifest); err != nil { @@ -211,8 +214,8 @@ func (cm *cacheManager) listCachedPackages(targetTriple, pkgPath string) ([]stri var fingerprints []string for _, entry := range entries { name := entry.Name() - if strings.HasSuffix(name, cacheArchiveExt) { - fp := strings.TrimSuffix(name, cacheArchiveExt) + if strings.HasSuffix(name, cacheBitcodeExt) { + fp := strings.TrimSuffix(name, cacheBitcodeExt) fingerprints = append(fingerprints, fp) } } @@ -239,7 +242,7 @@ func (cm *cacheManager) stats() (cacheStats, error) { } if !info.IsDir() { stats.TotalSize += info.Size() - if strings.HasSuffix(path, cacheArchiveExt) { + if strings.HasSuffix(path, cacheBitcodeExt) { stats.TotalPackages++ } } diff --git a/internal/build/cache_test.go b/internal/build/cache_test.go index 6c6d4a0eb2..b006089dd5 100644 --- a/internal/build/cache_test.go +++ b/internal/build/cache_test.go @@ -64,6 +64,10 @@ func TestCacheManager_PackagePaths(t *testing.T) { if paths.Archive != expectedArchive { t.Errorf("Archive = %q, want %q", paths.Archive, expectedArchive) } + expectedBitcode := filepath.Join(expectedDir, "abc123.bc") + if paths.Bitcode != expectedBitcode { + t.Errorf("Bitcode = %q, want %q", paths.Bitcode, expectedBitcode) + } expectedManifest := filepath.Join(expectedDir, "abc123.manifest") if paths.Manifest != expectedManifest { @@ -167,7 +171,7 @@ func TestCacheManager_CacheExists(t *testing.T) { if err := cm.EnsureDir(paths); err != nil { t.Fatal(err) } - os.WriteFile(paths.Archive, []byte("archive"), 0644) + os.WriteFile(paths.Bitcode, []byte("bitcode"), 0644) // Still should not exist (manifest missing) if cm.cacheExists(paths) { @@ -179,7 +183,7 @@ func TestCacheManager_CacheExists(t *testing.T) { // Now should exist if !cm.cacheExists(paths) { - t.Error("cache should exist with both files") + t.Error("cache should exist with bitcode and manifest files") } } @@ -215,7 +219,7 @@ func TestCacheManager_CleanPackageCache(t *testing.T) { // Create cache cm.EnsureDir(paths) - os.WriteFile(paths.Archive, []byte("archive"), 0644) + os.WriteFile(paths.Bitcode, []byte("bitcode"), 0644) os.WriteFile(paths.Manifest, []byte("manifest"), 0644) // Clean @@ -243,8 +247,8 @@ func TestCacheManager_CleanAllCache(t *testing.T) { cm.EnsureDir(paths1) cm.EnsureDir(paths2) - os.WriteFile(paths1.Archive, []byte("1"), 0644) - os.WriteFile(paths2.Archive, []byte("2"), 0644) + os.WriteFile(paths1.Bitcode, []byte("1"), 0644) + os.WriteFile(paths2.Bitcode, []byte("2"), 0644) // Clean all if err := cm.cleanAllCache(); err != nil { @@ -278,8 +282,8 @@ func TestCacheManager_ListCachedPackages(t *testing.T) { paths1 := cm.PackagePaths("arm64-darwin", "test/pkg", "fp1") paths2 := cm.PackagePaths("arm64-darwin", "test/pkg", "fp2") cm.EnsureDir(paths1) - os.WriteFile(paths1.Archive, []byte("1"), 0644) - os.WriteFile(paths2.Archive, []byte("2"), 0644) + os.WriteFile(paths1.Bitcode, []byte("1"), 0644) + os.WriteFile(paths2.Bitcode, []byte("2"), 0644) fps, err = cm.listCachedPackages("arm64-darwin", "test/pkg") if err != nil { @@ -304,10 +308,10 @@ func TestCacheManager_Stats(t *testing.T) { cm.EnsureDir(paths1) cm.EnsureDir(paths2) - content1 := []byte("archive content 1") - content2 := []byte("archive content 2 longer") - os.WriteFile(paths1.Archive, content1, 0644) - os.WriteFile(paths2.Archive, content2, 0644) + content1 := []byte("bitcode content 1") + content2 := []byte("bitcode content 2 longer") + os.WriteFile(paths1.Bitcode, content1, 0644) + os.WriteFile(paths2.Bitcode, content2, 0644) os.WriteFile(paths1.Manifest, []byte("m1"), 0644) os.WriteFile(paths2.Manifest, []byte("m2"), 0644) diff --git a/internal/build/collect.go b/internal/build/collect.go index bdd1977cd0..5a5453d16d 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -329,8 +329,8 @@ func (c *context) tryLoadFromCache(pkg *aPackage) bool { cm := c.ensureCacheManager() paths := cm.PackagePaths(c.targetTriple(), pkg.PkgPath, pkg.Fingerprint) - // Check if archive file exists - if _, err := os.Stat(paths.Archive); err != nil { + // Check if bitcode cache exists. + if _, err := os.Stat(paths.Bitcode); err != nil { return false } @@ -346,8 +346,11 @@ func (c *context) tryLoadFromCache(pkg *aPackage) bool { return false } - // Use the .a archive directly for linking (no extraction needed) - pkg.ArchiveFile = paths.Archive + // Use cached package bitcode for final LLVM API linking. + pkg.BitcodeFile = paths.Bitcode + if _, err := os.Stat(paths.Archive); err == nil { + pkg.ArchiveFile = paths.Archive + } pkg.LinkArgs = meta.LinkArgs pkg.NeedRt = meta.NeedRt pkg.NeedPyInit = meta.NeedPyInit @@ -444,18 +447,17 @@ func (c *context) saveToCache(pkg *aPackage) error { return err } - // If ArchiveFile is already set (from normalizeToArchive), copy it to cache + // Package bitcode is mandatory for bitcode LTO cache entries. + if pkg.BitcodeFile == "" { + return nil + } + if err := copyFileAtomic(pkg.BitcodeFile, paths.Bitcode); err != nil { + return err + } if pkg.ArchiveFile != "" { if err := copyFileAtomic(pkg.ArchiveFile, paths.Archive); err != nil { return err } - } else if len(pkg.ObjFiles) > 0 { - // Otherwise, create archive from object files - if err := c.createArchiveFile(paths.Archive, pkg.ObjFiles); err != nil { - return err - } - } else { - return nil } // Append metadata to existing manifest (pkg.Manifest was built in collectFingerprint). diff --git a/internal/build/collect_test.go b/internal/build/collect_test.go index 1c859c919b..1e26fffb81 100644 --- a/internal/build/collect_test.go +++ b/internal/build/collect_test.go @@ -411,15 +411,14 @@ func TestTryLoadFromCache_ForceRebuild(t *testing.T) { }(), } - // Create a temporary .o file - objFile, err := os.CreateTemp(td, "test-*.o") + // Create a temporary .bc file + bcFile, err := os.CreateTemp(td, "test-*.bc") if err != nil { t.Fatalf("CreateTemp: %v", err) } - objFile.WriteString("fake object file") - objFile.Close() - - pkg.ObjFiles = []string{objFile.Name()} + bcFile.WriteString("fake bitcode file") + bcFile.Close() + pkg.BitcodeFile = bcFile.Name() // First save to cache ctx.buildConf.ForceRebuild = false @@ -430,15 +429,15 @@ func TestTryLoadFromCache_ForceRebuild(t *testing.T) { // Verify cache exists cm := ctx.ensureCacheManager() paths := cm.PackagePaths("arm64-apple-darwin", "example.com/cached", "test123") - if _, err := os.Stat(paths.Archive); err != nil { + if _, err := os.Stat(paths.Bitcode); err != nil { t.Fatalf("cache should exist: %v", err) } // Now enable ForceRebuild and try to load ctx.buildConf.ForceRebuild = true - // Clear ObjFiles to verify it's not loaded from cache - pkg.ObjFiles = nil + // Clear fields to verify they are not loaded from cache. + pkg.BitcodeFile = "" pkg.ArchiveFile = "" pkg.CacheHit = false @@ -450,8 +449,8 @@ func TestTryLoadFromCache_ForceRebuild(t *testing.T) { t.Error("CacheHit should remain false when ForceRebuild is enabled") } - if pkg.ArchiveFile != "" { - t.Error("ArchiveFile should not be populated when ForceRebuild is enabled") + if pkg.BitcodeFile != "" { + t.Error("BitcodeFile should not be populated when ForceRebuild is enabled") } } @@ -511,19 +510,19 @@ func TestSaveToCache_Success(t *testing.T) { }, } - // Create a temporary .o file - objFile, err := os.CreateTemp(td, "test-*.o") + // Create a temporary .bc file + bcFile, err := os.CreateTemp(td, "test-*.bc") if err != nil { t.Fatalf("CreateTemp: %v", err) } - objFile.WriteString("fake object file") - objFile.Close() + bcFile.WriteString("fake bitcode file") + bcFile.Close() pkg := &aPackage{ Package: &packages.Package{ PkgPath: "example.com/lib", Name: "lib", - GoFiles: []string{objFile.Name()}, // Add GoFiles for manifest generation + GoFiles: []string{bcFile.Name()}, // Add GoFiles for manifest generation }, Fingerprint: "def456", Manifest: func() string { @@ -532,7 +531,7 @@ func TestSaveToCache_Success(t *testing.T) { m.pkg.PkgPath = "example.com/lib" return m.Build() }(), - ObjFiles: []string{objFile.Name()}, + BitcodeFile: bcFile.Name(), } if err := ctx.saveToCache(pkg); err != nil { @@ -559,9 +558,9 @@ func TestSaveToCache_Success(t *testing.T) { t.Errorf("metadata should be empty when no link args/runtime flags") } - // Check archive exists - if _, err := os.Stat(paths.Archive); err != nil { - t.Errorf("archive should exist: %v", err) + // Check bitcode exists + if _, err := os.Stat(paths.Bitcode); err != nil { + t.Errorf("bitcode should exist: %v", err) } } From 3fca57865101840ac04d64c039b48996f6b7e841 Mon Sep 17 00:00:00 2001 From: luoliwoshang <2643523683@qq.com> Date: Mon, 2 Mar 2026 23:18:16 +0800 Subject: [PATCH 02/10] build: make bitcode LTO path unconditional --- internal/build/build.go | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index ad9e7d676f..d79ac4d8c0 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -363,7 +363,6 @@ func Do(args []string, conf *Config) ([]Package, error) { pkgByID: map[string]Package{}, output: output, passOpt: passOpt, - bitcodeLTO: true, buildConf: conf, crossCompile: export, cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize), @@ -544,9 +543,6 @@ type context struct { plan9asmAll bool // when plan9asmAll=false: enabled set; when plan9asmAll=true: excluded set. plan9asmPkgs map[string]bool - - // Enable package bitcode outputs and final LLVM API linking. - bitcodeLTO bool } func (c *context) compiler() *clang.Cmd { @@ -1088,16 +1084,12 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa linkBitcodeInputs = append(linkBitcodeInputs, extraBitcodeInputs...) nativeLinkInputs = append(nativeLinkInputs, extraNativeInputs...) - if ctx.bitcodeLTO { - programObjFile, err := compileLinkedBitcodeToObject(ctx, pkg.PkgPath, linkBitcodeInputs, verbose) - if err != nil { - return err - } - if programObjFile != "" { - nativeLinkInputs = append(nativeLinkInputs, programObjFile) - } - } else { - nativeLinkInputs = append(nativeLinkInputs, linkBitcodeInputs...) + programObjFile, err := compileLinkedBitcodeToObject(ctx, pkg.PkgPath, linkBitcodeInputs, verbose) + if err != nil { + return err + } + if programObjFile != "" { + nativeLinkInputs = append(nativeLinkInputs, programObjFile) } if IsFullRpathEnabled() { From ee2761f16cc031c9312858aea3da2927535ef19c Mon Sep 17 00:00:00 2001 From: luoliwoshang <2643523683@qq.com> Date: Mon, 2 Mar 2026 23:22:23 +0800 Subject: [PATCH 03/10] build: replace ArchiveFile with native link input slice --- internal/build/build.go | 37 +++++++++++++++++----------------- internal/build/collect.go | 12 ++++++++--- internal/build/collect_test.go | 5 ++++- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index d79ac4d8c0..3715405fb8 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -648,6 +648,7 @@ func normalizePackageOutputs(ctx *context, aPkg *aPackage, verbose bool) error { bitcodeFiles, nativeInputs := splitBitcodeAndNativeInputs(aPkg.ObjFiles) aPkg.ObjFiles = nil + aPkg.NativeLinkInputs = nil if len(bitcodeFiles) > 0 { mergedBitcode, err := mergeBitcodeFiles(aPkg.PkgPath, bitcodeFiles) @@ -669,7 +670,7 @@ func normalizePackageOutputs(ctx *context, aPkg *aPackage, verbose bool) error { os.Remove(archivePath) return fmt.Errorf("create native archive for %s: %w", aPkg.PkgPath, err) } - aPkg.ArchiveFile = archivePath + aPkg.NativeLinkInputs = []string{archivePath} } return nil } @@ -1034,9 +1035,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa if aPkg.BitcodeFile != "" { rtBitcodeInputs = append(rtBitcodeInputs, aPkg.BitcodeFile) } - if aPkg.ArchiveFile != "" { - rtNativeInputs = append(rtNativeInputs, aPkg.ArchiveFile) - } + rtNativeInputs = append(rtNativeInputs, aPkg.NativeLinkInputs...) return } else { // Only let non-runtime packages influence whether runtime is needed. @@ -1052,9 +1051,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa if aPkg.BitcodeFile != "" { linkBitcodeInputs = append(linkBitcodeInputs, aPkg.BitcodeFile) } - if aPkg.ArchiveFile != "" { - nativeLinkInputs = append(nativeLinkInputs, aPkg.ArchiveFile) - } + nativeLinkInputs = append(nativeLinkInputs, aPkg.NativeLinkInputs...) } }) @@ -1475,8 +1472,9 @@ type aPackage struct { LinkArgs []string ObjFiles []string // per-file outputs before normalization (.bc/.o) BitcodeFile string // merged package bitcode (.bc) - ArchiveFile string // optional native archive (.a) for non-bitcode objects - rewriteVars map[string]string + // optional native link inputs for non-bitcode objects (typically one temp .a) + NativeLinkInputs []string + rewriteVars map[string]string // Cache related fields Fingerprint string // fingerprint digest @@ -1506,16 +1504,17 @@ func buildSSAPkgs(ctx *context, initial []*packages.Package, verbose bool) ([]*a } rewrites := collectRewriteVars(ctx, pkgPath) aPkg := &aPackage{ - Package: p, - SSA: ssaPkg, - AltPkg: altPkg, - LPkg: nil, - NeedRt: false, - NeedPyInit: false, - LinkArgs: nil, - ObjFiles: nil, - BitcodeFile: "", - rewriteVars: rewrites, + Package: p, + SSA: ssaPkg, + AltPkg: altPkg, + LPkg: nil, + NeedRt: false, + NeedPyInit: false, + LinkArgs: nil, + ObjFiles: nil, + BitcodeFile: "", + NativeLinkInputs: nil, + rewriteVars: rewrites, } ctx.pkgs[p] = aPkg ctx.pkgByID[p.ID] = aPkg diff --git a/internal/build/collect.go b/internal/build/collect.go index 5a5453d16d..81f5622c36 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -349,7 +349,9 @@ func (c *context) tryLoadFromCache(pkg *aPackage) bool { // Use cached package bitcode for final LLVM API linking. pkg.BitcodeFile = paths.Bitcode if _, err := os.Stat(paths.Archive); err == nil { - pkg.ArchiveFile = paths.Archive + pkg.NativeLinkInputs = []string{paths.Archive} + } else { + pkg.NativeLinkInputs = nil } pkg.LinkArgs = meta.LinkArgs pkg.NeedRt = meta.NeedRt @@ -454,8 +456,12 @@ func (c *context) saveToCache(pkg *aPackage) error { if err := copyFileAtomic(pkg.BitcodeFile, paths.Bitcode); err != nil { return err } - if pkg.ArchiveFile != "" { - if err := copyFileAtomic(pkg.ArchiveFile, paths.Archive); err != nil { + if len(pkg.NativeLinkInputs) > 0 { + if len(pkg.NativeLinkInputs) == 1 && strings.HasSuffix(pkg.NativeLinkInputs[0], ".a") { + if err := copyFileAtomic(pkg.NativeLinkInputs[0], paths.Archive); err != nil { + return err + } + } else if err := c.createArchiveFile(paths.Archive, pkg.NativeLinkInputs); err != nil { return err } } diff --git a/internal/build/collect_test.go b/internal/build/collect_test.go index 1e26fffb81..fdb71d0ecd 100644 --- a/internal/build/collect_test.go +++ b/internal/build/collect_test.go @@ -438,7 +438,7 @@ func TestTryLoadFromCache_ForceRebuild(t *testing.T) { // Clear fields to verify they are not loaded from cache. pkg.BitcodeFile = "" - pkg.ArchiveFile = "" + pkg.NativeLinkInputs = nil pkg.CacheHit = false if ctx.tryLoadFromCache(pkg) { @@ -452,6 +452,9 @@ func TestTryLoadFromCache_ForceRebuild(t *testing.T) { if pkg.BitcodeFile != "" { t.Error("BitcodeFile should not be populated when ForceRebuild is enabled") } + if len(pkg.NativeLinkInputs) != 0 { + t.Error("NativeLinkInputs should not be populated when ForceRebuild is enabled") + } } func TestSaveToCache_MainPackage(t *testing.T) { From 52f759921309ad2c7f284b7d82f510ce2572977e Mon Sep 17 00:00:00 2001 From: luoliwoshang <2643523683@qq.com> Date: Tue, 3 Mar 2026 16:39:09 +0800 Subject: [PATCH 04/10] build: emit linked verbose IR from in-memory LLVM module --- internal/build/build.go | 63 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index 3715405fb8..4becf80824 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -592,11 +592,8 @@ func splitBitcodeAndNativeInputs(inputs []string) (bitcodeFiles []string, native } func tempNamePrefix(moduleName string) string { - name := strings.ReplaceAll(moduleName, "/", "_") - name = strings.ReplaceAll(name, "\\", "_") - name = strings.ReplaceAll(name, ":", "_") - name = strings.ReplaceAll(name, ".", "_") - if name == "" { + name := path.Base(moduleName) + if name == "" || name == "." || name == "/" { return "llgo" } return name @@ -641,6 +638,47 @@ func mergeBitcodeFiles(moduleName string, bitcodeFiles []string) (string, error) return out.Name(), nil } +func mergeBitcodeFilesWithIR(moduleName string, bitcodeFiles []string) (string, string, error) { + if len(bitcodeFiles) == 0 { + return "", "", nil + } + + ctx := gllvm.NewContext() + defer ctx.Dispose() + + mod, err := ctx.ParseBitcodeFile(bitcodeFiles[0]) + if err != nil { + return "", "", fmt.Errorf("parse bitcode %s: %w", bitcodeFiles[0], err) + } + defer mod.Dispose() + + for _, bitcodeFile := range bitcodeFiles[1:] { + srcMod, err := ctx.ParseBitcodeFile(bitcodeFile) + if err != nil { + return "", "", fmt.Errorf("parse bitcode %s: %w", bitcodeFile, err) + } + if err := gllvm.LinkModules(mod, srcMod); err != nil { + return "", "", fmt.Errorf("link bitcode module %s: %w", bitcodeFile, err) + } + } + + ir := mod.String() + if len(bitcodeFiles) == 1 { + return bitcodeFiles[0], ir, nil + } + + out, err := os.CreateTemp("", tempNamePrefix(moduleName)+"-*.bc") + if err != nil { + return "", "", fmt.Errorf("create merged bitcode file: %w", err) + } + defer out.Close() + + if err := gllvm.WriteBitcodeToFile(mod, out); err != nil { + return "", "", fmt.Errorf("write merged bitcode file: %w", err) + } + return out.Name(), ir, nil +} + func normalizePackageOutputs(ctx *context, aPkg *aPackage, verbose bool) error { if len(aPkg.ObjFiles) == 0 { return nil @@ -680,10 +718,21 @@ func compileLinkedBitcodeToObject(ctx *context, moduleName string, bitcodeFiles return "", nil } - mergedBitcode, err := mergeBitcodeFiles(moduleName, bitcodeFiles) + mergedBitcode, linkedModuleIR, err := mergeBitcodeFilesWithIR(moduleName, bitcodeFiles) if err != nil { return "", err } + printCmds := ctx.shouldPrintCommands(verbose) + + // In verbose mode, emit the final linked module as .ll beside the .bc so + // users can inspect exactly what will be compiled to native object code. + if printCmds { + linkedLL := strings.TrimSuffix(mergedBitcode, filepath.Ext(mergedBitcode)) + ".ll" + fmt.Fprintf(os.Stderr, "# emitting linked bitcode ll for %s: %s\n", moduleName, linkedLL) + if err := os.WriteFile(linkedLL, []byte(linkedModuleIR), 0o644); err != nil { + fmt.Fprintf(os.Stderr, "warning: failed to emit linked bitcode ll for %s: %v\n", moduleName, err) + } + } objFile, err := os.CreateTemp("", tempNamePrefix(moduleName)+"-*.o") if err != nil { @@ -692,7 +741,7 @@ func compileLinkedBitcodeToObject(ctx *context, moduleName string, bitcodeFiles objFile.Close() args := []string{"-o", objFile.Name(), "-c", mergedBitcode, "-Wno-override-module"} - if ctx.shouldPrintCommands(verbose) { + if printCmds { fmt.Fprintf(os.Stderr, "# compiling linked bitcode for %s\n", moduleName) fmt.Fprintln(os.Stderr, "clang", args) } From a0b6a185f12328aeb7e7cf2fcde6de0c02384ae9 Mon Sep 17 00:00:00 2001 From: luoliwoshang <2643523683@qq.com> Date: Tue, 3 Mar 2026 16:49:47 +0800 Subject: [PATCH 05/10] build: make GenLL emit ll from generated bitcode --- internal/build/build.go | 79 +++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 46 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index 4becf80824..d0c9cc2ea3 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -679,6 +679,22 @@ func mergeBitcodeFilesWithIR(moduleName string, bitcodeFiles []string) (string, return out.Name(), ir, nil } +func writeLLVMIRFromBitcode(bitcodeFile, llFile string) error { + ctx := gllvm.NewContext() + defer ctx.Dispose() + + mod, err := ctx.ParseBitcodeFile(bitcodeFile) + if err != nil { + return fmt.Errorf("parse bitcode %s: %w", bitcodeFile, err) + } + defer mod.Dispose() + + if err := os.WriteFile(llFile, []byte(mod.String()), 0o644); err != nil { + return fmt.Errorf("write ll file %s: %w", llFile, err) + } + return nil +} + func normalizePackageOutputs(ctx *context, aPkg *aPackage, verbose bool) error { if len(aPkg.ObjFiles) == 0 { return nil @@ -1006,19 +1022,6 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { emitBitcode := ext != ".S" && ext != ".s" - // If GenLL is enabled, first emit .ll for debugging. - if ctx.buildConf.GenLL && emitBitcode { - llFile := baseName + ".ll" - llArgs := append(slices.Clone(baseArgs), "-emit-llvm", "-S", "-o", llFile, "-c", srcFile) - if printCmds { - fmt.Fprintf(os.Stderr, "Compiling extra file (ll): clang %s\n", strings.Join(llArgs, " ")) - } - cmd := ctx.compiler() - if err := cmd.Compile(llArgs...); err != nil { - return nil, fmt.Errorf("failed to compile extra file %s to .ll: %w", srcFile, err) - } - } - if emitBitcode { bcFile := baseName + ".bc" bcArgs := append(baseArgs, "-emit-llvm", "-o", bcFile, "-c", srcFile) @@ -1029,6 +1032,15 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { if err := cmd.Compile(bcArgs...); err != nil { return nil, fmt.Errorf("failed to compile extra file %s to .bc: %w", srcFile, err) } + if ctx.buildConf.GenLL { + llFile := baseName + ".ll" + if printCmds { + fmt.Fprintf(os.Stderr, "Emitting extra file (ll): %s (from %s)\n", llFile, bcFile) + } + if err := writeLLVMIRFromBitcode(bcFile, llFile); err != nil { + return nil, fmt.Errorf("failed to emit extra file %s to .ll: %w", srcFile, err) + } + } linkInputs = append(linkInputs, bcFile) } else { objFile := baseName + ".o" @@ -1192,26 +1204,6 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose buildArgs = append(buildArgs, "-gdwarf-4") } - if ctx.buildConf.GenLL { - var compiledObjFiles []string - for _, objFile := range objFiles { - if strings.HasSuffix(objFile, ".ll") { - oFile := strings.TrimSuffix(objFile, ".ll") + ".o" - args := []string{"-o", oFile, "-c", objFile, "-Wno-override-module"} - if printCmds { - fmt.Fprintln(os.Stderr, "clang", args) - } - if err := ctx.compiler().Compile(args...); err != nil { - return fmt.Errorf("failed to compile %s: %v", objFile, err) - } - compiledObjFiles = append(compiledObjFiles, oFile) - } else { - compiledObjFiles = append(compiledObjFiles, objFile) - } - } - objFiles = compiledObjFiles - } - buildArgs = append(buildArgs, objFiles...) cmd := ctx.linker() @@ -1841,20 +1833,7 @@ func clFile(ctx *context, args []string, cFile, expFile, pkgPath string, procFil emitBitcode := ext != ".S" && ext != ".s" - // If GenLL is enabled, first emit .ll for debugging. printCmds := ctx.shouldPrintCommands(verbose) - if ctx.buildConf.GenLL && emitBitcode { - llFile := baseName + ".ll" - llArgs := append(slices.Clone(compileArgs), "-emit-llvm", "-S", "-o", llFile, "-c", cFile) - if printCmds { - fmt.Fprintf(os.Stderr, "# compiling %s for pkg: %s\n", llFile, pkgPath) - fmt.Fprintln(os.Stderr, "clang", llArgs) - } - cmd := ctx.compiler() - err := cmd.Compile(llArgs...) - check(err) - } - if emitBitcode { bcFile := baseName + ".bc" bcArgs := append(compileArgs, "-emit-llvm", "-o", bcFile, "-c", cFile) @@ -1865,6 +1844,14 @@ func clFile(ctx *context, args []string, cFile, expFile, pkgPath string, procFil cmd := ctx.compiler() err := cmd.Compile(bcArgs...) check(err) + if ctx.buildConf.GenLL { + llFile := baseName + ".ll" + if printCmds { + fmt.Fprintf(os.Stderr, "# emitting %s for pkg: %s (from %s)\n", llFile, pkgPath, bcFile) + } + err := writeLLVMIRFromBitcode(bcFile, llFile) + check(err) + } procFile(bcFile) return } From dd77508aa6392bc6adfa7b2a2e2929e38752b0b0 Mon Sep 17 00:00:00 2001 From: luoliwoshang <2643523683@qq.com> Date: Tue, 3 Mar 2026 17:16:54 +0800 Subject: [PATCH 06/10] build: unify package link artifacts in ObjFiles --- internal/build/build.go | 59 +++++++++++++--------------------- internal/build/collect.go | 29 ++++++++++------- internal/build/collect_test.go | 14 +++----- 3 files changed, 46 insertions(+), 56 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index d0c9cc2ea3..50bdc17b27 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -701,15 +701,14 @@ func normalizePackageOutputs(ctx *context, aPkg *aPackage, verbose bool) error { } bitcodeFiles, nativeInputs := splitBitcodeAndNativeInputs(aPkg.ObjFiles) - aPkg.ObjFiles = nil - aPkg.NativeLinkInputs = nil + var normalized []string if len(bitcodeFiles) > 0 { mergedBitcode, err := mergeBitcodeFiles(aPkg.PkgPath, bitcodeFiles) if err != nil { return fmt.Errorf("merge bitcode for %s: %w", aPkg.PkgPath, err) } - aPkg.BitcodeFile = mergedBitcode + normalized = append(normalized, mergedBitcode) } if len(nativeInputs) > 0 { @@ -724,8 +723,9 @@ func normalizePackageOutputs(ctx *context, aPkg *aPackage, verbose bool) error { os.Remove(archivePath) return fmt.Errorf("create native archive for %s: %w", aPkg.PkgPath, err) } - aPkg.NativeLinkInputs = []string{archivePath} + normalized = append(normalized, archivePath) } + aPkg.ObjFiles = normalized return nil } @@ -1068,13 +1068,11 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa for _, v := range pkgs { allPkgs = append(allPkgs, v.Package) } - // Bitcode modules are linked together first using LLVM APIs, then compiled to - // one native object and linked with native archives/objects. - var linkBitcodeInputs []string - var nativeLinkInputs []string + // Package link inputs are collected first, then split into bitcode/native + // before the program-level bitcode link and final native link. + var pkgLinkInputs []string var linkArgs []string - var rtBitcodeInputs []string - var rtNativeInputs []string + var rtPkgLinkInputs []string var rtLinkArgs []string linkedPkgs := make(map[string]bool) // Track linked packages by ID to avoid duplicates packages.Visit(allPkgs, nil, func(p *packages.Package) { @@ -1093,10 +1091,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa // Defer linking runtime packages unless we actually need the runtime. if isRuntimePkg(p.PkgPath) { rtLinkArgs = append(rtLinkArgs, aPkg.LinkArgs...) - if aPkg.BitcodeFile != "" { - rtBitcodeInputs = append(rtBitcodeInputs, aPkg.BitcodeFile) - } - rtNativeInputs = append(rtNativeInputs, aPkg.NativeLinkInputs...) + rtPkgLinkInputs = append(rtPkgLinkInputs, aPkg.ObjFiles...) return } else { // Only let non-runtime packages influence whether runtime is needed. @@ -1109,19 +1104,16 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa } linkArgs = append(linkArgs, aPkg.LinkArgs...) - if aPkg.BitcodeFile != "" { - linkBitcodeInputs = append(linkBitcodeInputs, aPkg.BitcodeFile) - } - nativeLinkInputs = append(nativeLinkInputs, aPkg.NativeLinkInputs...) + pkgLinkInputs = append(pkgLinkInputs, aPkg.ObjFiles...) } }) // Only link runtime objects when needed (or for host builds where runtime is always required). if needRuntime || needPyInit || ctx.buildConf.Target == "" { linkArgs = append(linkArgs, rtLinkArgs...) - linkBitcodeInputs = append(linkBitcodeInputs, rtBitcodeInputs...) - nativeLinkInputs = append(nativeLinkInputs, rtNativeInputs...) + pkgLinkInputs = append(pkgLinkInputs, rtPkgLinkInputs...) } + linkBitcodeInputs, nativeLinkInputs := splitBitcodeAndNativeInputs(pkgLinkInputs) // Generate main module file (needed for global variables even in library modes) // This is compiled to .bc and included in the program-level bitcode link (not cached). @@ -1511,11 +1503,8 @@ type aPackage struct { NeedPyInit bool LinkArgs []string - ObjFiles []string // per-file outputs before normalization (.bc/.o) - BitcodeFile string // merged package bitcode (.bc) - // optional native link inputs for non-bitcode objects (typically one temp .a) - NativeLinkInputs []string - rewriteVars map[string]string + ObjFiles []string // normalized package link inputs (.bc and optional native .a) + rewriteVars map[string]string // Cache related fields Fingerprint string // fingerprint digest @@ -1545,17 +1534,15 @@ func buildSSAPkgs(ctx *context, initial []*packages.Package, verbose bool) ([]*a } rewrites := collectRewriteVars(ctx, pkgPath) aPkg := &aPackage{ - Package: p, - SSA: ssaPkg, - AltPkg: altPkg, - LPkg: nil, - NeedRt: false, - NeedPyInit: false, - LinkArgs: nil, - ObjFiles: nil, - BitcodeFile: "", - NativeLinkInputs: nil, - rewriteVars: rewrites, + Package: p, + SSA: ssaPkg, + AltPkg: altPkg, + LPkg: nil, + NeedRt: false, + NeedPyInit: false, + LinkArgs: nil, + ObjFiles: nil, + rewriteVars: rewrites, } ctx.pkgs[p] = aPkg ctx.pkgByID[p.ID] = aPkg diff --git a/internal/build/collect.go b/internal/build/collect.go index 81f5622c36..41093cf851 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -346,12 +346,10 @@ func (c *context) tryLoadFromCache(pkg *aPackage) bool { return false } - // Use cached package bitcode for final LLVM API linking. - pkg.BitcodeFile = paths.Bitcode + // Use cached package link inputs for final linking. + pkg.ObjFiles = []string{paths.Bitcode} if _, err := os.Stat(paths.Archive); err == nil { - pkg.NativeLinkInputs = []string{paths.Archive} - } else { - pkg.NativeLinkInputs = nil + pkg.ObjFiles = append(pkg.ObjFiles, paths.Archive) } pkg.LinkArgs = meta.LinkArgs pkg.NeedRt = meta.NeedRt @@ -449,19 +447,28 @@ func (c *context) saveToCache(pkg *aPackage) error { return err } + bitcodeFiles, nativeInputs := splitBitcodeAndNativeInputs(pkg.ObjFiles) // Package bitcode is mandatory for bitcode LTO cache entries. - if pkg.BitcodeFile == "" { + if len(bitcodeFiles) == 0 { return nil } - if err := copyFileAtomic(pkg.BitcodeFile, paths.Bitcode); err != nil { + pkgBitcode := bitcodeFiles[0] + if len(bitcodeFiles) > 1 { + merged, err := mergeBitcodeFiles(pkg.PkgPath, bitcodeFiles) + if err != nil { + return fmt.Errorf("merge bitcode for cache %s: %w", pkg.PkgPath, err) + } + pkgBitcode = merged + } + if err := copyFileAtomic(pkgBitcode, paths.Bitcode); err != nil { return err } - if len(pkg.NativeLinkInputs) > 0 { - if len(pkg.NativeLinkInputs) == 1 && strings.HasSuffix(pkg.NativeLinkInputs[0], ".a") { - if err := copyFileAtomic(pkg.NativeLinkInputs[0], paths.Archive); err != nil { + if len(nativeInputs) > 0 { + if len(nativeInputs) == 1 && strings.HasSuffix(nativeInputs[0], ".a") { + if err := copyFileAtomic(nativeInputs[0], paths.Archive); err != nil { return err } - } else if err := c.createArchiveFile(paths.Archive, pkg.NativeLinkInputs); err != nil { + } else if err := c.createArchiveFile(paths.Archive, nativeInputs); err != nil { return err } } diff --git a/internal/build/collect_test.go b/internal/build/collect_test.go index fdb71d0ecd..ae0b7e5ce6 100644 --- a/internal/build/collect_test.go +++ b/internal/build/collect_test.go @@ -418,7 +418,7 @@ func TestTryLoadFromCache_ForceRebuild(t *testing.T) { } bcFile.WriteString("fake bitcode file") bcFile.Close() - pkg.BitcodeFile = bcFile.Name() + pkg.ObjFiles = []string{bcFile.Name()} // First save to cache ctx.buildConf.ForceRebuild = false @@ -437,8 +437,7 @@ func TestTryLoadFromCache_ForceRebuild(t *testing.T) { ctx.buildConf.ForceRebuild = true // Clear fields to verify they are not loaded from cache. - pkg.BitcodeFile = "" - pkg.NativeLinkInputs = nil + pkg.ObjFiles = nil pkg.CacheHit = false if ctx.tryLoadFromCache(pkg) { @@ -449,11 +448,8 @@ func TestTryLoadFromCache_ForceRebuild(t *testing.T) { t.Error("CacheHit should remain false when ForceRebuild is enabled") } - if pkg.BitcodeFile != "" { - t.Error("BitcodeFile should not be populated when ForceRebuild is enabled") - } - if len(pkg.NativeLinkInputs) != 0 { - t.Error("NativeLinkInputs should not be populated when ForceRebuild is enabled") + if len(pkg.ObjFiles) != 0 { + t.Error("ObjFiles should not be populated when ForceRebuild is enabled") } } @@ -534,7 +530,7 @@ func TestSaveToCache_Success(t *testing.T) { m.pkg.PkgPath = "example.com/lib" return m.Build() }(), - BitcodeFile: bcFile.Name(), + ObjFiles: []string{bcFile.Name()}, } if err := ctx.saveToCache(pkg); err != nil { From 4062fd0fde0754ac8aac26e9356f230537632ac6 Mon Sep 17 00:00:00 2001 From: luoliwoshang <2643523683@qq.com> Date: Tue, 3 Mar 2026 18:42:05 +0800 Subject: [PATCH 07/10] build: emit package bitcode directly via WriteBitcodeToFile --- internal/build/build.go | 80 +++++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index 50bdc17b27..08e2f393e6 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -1119,7 +1119,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa // This is compiled to .bc and included in the program-level bitcode link (not cached). // Use a stable synthetic name to avoid confusing it with the real main package in traces/logs. entryPkg := genMainModule(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit, needAbiInit) - entryBitcodeFile, err := exportObject(ctx, "entry_main", entryPkg.ExportFile, []byte(entryPkg.LPkg.String())) + entryBitcodeFile, err := exportObject(ctx, "entry_main", entryPkg.ExportFile, entryPkg.LPkg.Module()) if err != nil { return err } @@ -1385,7 +1385,7 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error { aPkg.LinkArgs = append(aPkg.LinkArgs, goCgoLinkArgs(ctx.buildConf.Goos, aPkg.AltPkg.Syntax)...) } if pkg.ExportFile != "" { - exportFile, err := exportObject(ctx, pkg.PkgPath, pkg.ExportFile, []byte(ret.String())) + exportFile, err := exportObject(ctx, pkg.PkgPath, pkg.ExportFile, ret.Module()) if err != nil { return fmt.Errorf("export object of %v failed: %v", pkgPath, err) } @@ -1397,49 +1397,59 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error { return nil } -func exportObject(ctx *context, pkgPath string, exportFile string, data []byte) (string, error) { +func exportObject(ctx *context, pkgPath string, exportFile string, mod gllvm.Module) (string, error) { base := filepath.Base(exportFile) - f, err := os.CreateTemp("", base+"-*.ll") - if err != nil { - return "", err - } - if _, err := f.Write(data); err != nil { - f.Close() - return "", err - } - err = f.Close() - if err != nil { - return exportFile, err - } - if ctx.buildConf.CheckLLFiles { - if msg, err := llcCheck(ctx.env, f.Name()); err != nil { - fmt.Fprintf(os.Stderr, "==> lcc %v: %v\n%v\n", pkgPath, f.Name(), msg) - } - } - // If GenLL is enabled, keep a copy of the .ll file for debugging - if ctx.buildConf.GenLL { - llFile := exportFile + ".ll" - if err := os.Chmod(f.Name(), 0644); err != nil { - return "", err + if ctx.buildConf.CheckLLFiles || ctx.buildConf.GenLL { + llText := mod.String() + if ctx.buildConf.CheckLLFiles { + irFile, err := os.CreateTemp("", base+"-*.ll") + if err != nil { + return "", err + } + irPath := irFile.Name() + if _, err := irFile.WriteString(llText); err != nil { + irFile.Close() + os.Remove(irPath) + return "", err + } + if err := irFile.Close(); err != nil { + os.Remove(irPath) + return "", err + } + defer os.Remove(irPath) + if msg, err := llcCheck(ctx.env, irPath); err != nil { + fmt.Fprintf(os.Stderr, "==> lcc %v: %v\n%v\n", pkgPath, irPath, msg) + } } - // Copy instead of rename so we can still compile to .bc - if err := copyFileAtomic(f.Name(), llFile); err != nil { - return "", err + // If GenLL is enabled, keep a copy of the module .ll for debugging. + if ctx.buildConf.GenLL { + llFile := exportFile + ".ll" + if err := os.WriteFile(llFile, []byte(llText), 0o644); err != nil { + return "", err + } } } - // Compile .ll to .bc for program-level bitcode linking. + + // Emit bitcode directly from the in-memory LLVM module. bitcodeFile, err := os.CreateTemp("", base+"-*.bc") if err != nil { return "", err } - bitcodeFile.Close() - args := []string{"-o", bitcodeFile.Name(), "-emit-llvm", "-c", f.Name(), "-Wno-override-module"} + bitcodePath := bitcodeFile.Name() + if err := gllvm.WriteBitcodeToFile(mod, bitcodeFile); err != nil { + bitcodeFile.Close() + os.Remove(bitcodePath) + return "", err + } + if err := bitcodeFile.Close(); err != nil { + os.Remove(bitcodePath) + return "", err + } + if ctx.shouldPrintCommands(false) { - fmt.Fprintf(os.Stderr, "# compiling %s for pkg: %s\n", f.Name(), pkgPath) - fmt.Fprintln(os.Stderr, "clang", args) + fmt.Fprintf(os.Stderr, "# emitting %s for pkg: %s\n", bitcodePath, pkgPath) } - cmd := ctx.compiler() - return bitcodeFile.Name(), cmd.Compile(args...) + return bitcodePath, nil } func llcCheck(env *llvm.Env, exportFile string) (msg string, err error) { From cc263020b44144a4b0ae40c87d0cac39c2847243 Mon Sep 17 00:00:00 2001 From: luoliwoshang <2643523683@qq.com> Date: Tue, 3 Mar 2026 19:24:18 +0800 Subject: [PATCH 08/10] build: restore exportObject ll-to-bc export path --- internal/build/build.go | 80 ++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 45 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index 08e2f393e6..50bdc17b27 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -1119,7 +1119,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa // This is compiled to .bc and included in the program-level bitcode link (not cached). // Use a stable synthetic name to avoid confusing it with the real main package in traces/logs. entryPkg := genMainModule(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit, needAbiInit) - entryBitcodeFile, err := exportObject(ctx, "entry_main", entryPkg.ExportFile, entryPkg.LPkg.Module()) + entryBitcodeFile, err := exportObject(ctx, "entry_main", entryPkg.ExportFile, []byte(entryPkg.LPkg.String())) if err != nil { return err } @@ -1385,7 +1385,7 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error { aPkg.LinkArgs = append(aPkg.LinkArgs, goCgoLinkArgs(ctx.buildConf.Goos, aPkg.AltPkg.Syntax)...) } if pkg.ExportFile != "" { - exportFile, err := exportObject(ctx, pkg.PkgPath, pkg.ExportFile, ret.Module()) + exportFile, err := exportObject(ctx, pkg.PkgPath, pkg.ExportFile, []byte(ret.String())) if err != nil { return fmt.Errorf("export object of %v failed: %v", pkgPath, err) } @@ -1397,59 +1397,49 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error { return nil } -func exportObject(ctx *context, pkgPath string, exportFile string, mod gllvm.Module) (string, error) { +func exportObject(ctx *context, pkgPath string, exportFile string, data []byte) (string, error) { base := filepath.Base(exportFile) - if ctx.buildConf.CheckLLFiles || ctx.buildConf.GenLL { - llText := mod.String() - if ctx.buildConf.CheckLLFiles { - irFile, err := os.CreateTemp("", base+"-*.ll") - if err != nil { - return "", err - } - irPath := irFile.Name() - if _, err := irFile.WriteString(llText); err != nil { - irFile.Close() - os.Remove(irPath) - return "", err - } - if err := irFile.Close(); err != nil { - os.Remove(irPath) - return "", err - } - defer os.Remove(irPath) - if msg, err := llcCheck(ctx.env, irPath); err != nil { - fmt.Fprintf(os.Stderr, "==> lcc %v: %v\n%v\n", pkgPath, irPath, msg) - } - } - // If GenLL is enabled, keep a copy of the module .ll for debugging. - if ctx.buildConf.GenLL { - llFile := exportFile + ".ll" - if err := os.WriteFile(llFile, []byte(llText), 0o644); err != nil { - return "", err - } - } - } - - // Emit bitcode directly from the in-memory LLVM module. - bitcodeFile, err := os.CreateTemp("", base+"-*.bc") + f, err := os.CreateTemp("", base+"-*.ll") if err != nil { return "", err } - bitcodePath := bitcodeFile.Name() - if err := gllvm.WriteBitcodeToFile(mod, bitcodeFile); err != nil { - bitcodeFile.Close() - os.Remove(bitcodePath) + if _, err := f.Write(data); err != nil { + f.Close() return "", err } - if err := bitcodeFile.Close(); err != nil { - os.Remove(bitcodePath) + err = f.Close() + if err != nil { + return exportFile, err + } + if ctx.buildConf.CheckLLFiles { + if msg, err := llcCheck(ctx.env, f.Name()); err != nil { + fmt.Fprintf(os.Stderr, "==> lcc %v: %v\n%v\n", pkgPath, f.Name(), msg) + } + } + // If GenLL is enabled, keep a copy of the .ll file for debugging + if ctx.buildConf.GenLL { + llFile := exportFile + ".ll" + if err := os.Chmod(f.Name(), 0644); err != nil { + return "", err + } + // Copy instead of rename so we can still compile to .bc + if err := copyFileAtomic(f.Name(), llFile); err != nil { + return "", err + } + } + // Compile .ll to .bc for program-level bitcode linking. + bitcodeFile, err := os.CreateTemp("", base+"-*.bc") + if err != nil { return "", err } - + bitcodeFile.Close() + args := []string{"-o", bitcodeFile.Name(), "-emit-llvm", "-c", f.Name(), "-Wno-override-module"} if ctx.shouldPrintCommands(false) { - fmt.Fprintf(os.Stderr, "# emitting %s for pkg: %s\n", bitcodePath, pkgPath) + fmt.Fprintf(os.Stderr, "# compiling %s for pkg: %s\n", f.Name(), pkgPath) + fmt.Fprintln(os.Stderr, "clang", args) } - return bitcodePath, nil + cmd := ctx.compiler() + return bitcodeFile.Name(), cmd.Compile(args...) } func llcCheck(env *llvm.Env, exportFile string) (msg string, err error) { From 1ee0bc8e52ba149c9a3b0d836478825c1533fbd5 Mon Sep 17 00:00:00 2001 From: luoliwoshang <2643523683@qq.com> Date: Wed, 4 Mar 2026 10:40:28 +0800 Subject: [PATCH 09/10] build/ssa: fix darwin lldb symbols for merged bitcode --- internal/build/build.go | 31 ++++++++++++++++++++++++++++++- ssa/eh.go | 10 ++++++++++ ssa/stmt_builder.go | 11 +++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/internal/build/build.go b/internal/build/build.go index 50bdc17b27..1e68fc545e 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -757,6 +757,9 @@ func compileLinkedBitcodeToObject(ctx *context, moduleName string, bitcodeFiles objFile.Close() args := []string{"-o", objFile.Name(), "-c", mergedBitcode, "-Wno-override-module"} + if IsDbgSymsEnabled() { + args = append(args, "-gdwarf-4") + } if printCmds { fmt.Fprintf(os.Stderr, "# compiling linked bitcode for %s\n", moduleName) fmt.Fprintln(os.Stderr, "clang", args) @@ -1200,7 +1203,33 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose cmd := ctx.linker() cmd.Verbose = printCmds - return cmd.Link(buildArgs...) + if err := cmd.Link(buildArgs...); err != nil { + return err + } + if err := emitDarwinDSYMIfNeeded(ctx, app, printCmds); err != nil { + return err + } + return nil +} + +func emitDarwinDSYMIfNeeded(ctx *context, app string, verbose bool) error { + if !IsDbgSymsEnabled() || ctx.buildConf.Goos != "darwin" || runtime.GOOS != "darwin" { + return nil + } + if ctx.buildConf.BuildMode != BuildModeExe { + return nil + } + dsymutil, err := exec.LookPath("dsymutil") + if err != nil { + return fmt.Errorf("dsymutil not found for debug-symbol build: %w", err) + } + cmd := exec.Command(dsymutil, app) + if verbose { + fmt.Fprintln(os.Stderr, cmd.String()) + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() } // archiver returns the archiving tool to use for the current context. diff --git a/ssa/eh.go b/ssa/eh.go index 841018d353..e03f412848 100644 --- a/ssa/eh.go +++ b/ssa/eh.go @@ -149,6 +149,16 @@ func (b Builder) Longjmp(jb, retval Expr) { func (p Function) deferInitBuilder() (b Builder, next BasicBlock) { b = p.NewBuilder() + // getDefer may switch to a fresh builder when wiring defer prologue. + // Seed a valid function-scope debug location so synthesized calls carry + // !dbg in debug builds (LLVM verifies this for inlinable calls). + if sp := p.impl.Subprogram(); sp.C != nil { + line := sp.SubprogramLine() + if line == 0 { + line = 1 + } + b.impl.SetCurrentDebugLocation(line, 0, sp, llvm.Metadata{}) + } next = b.setBlockMoveLast(p.blks[0]) p.blks[0].last = next.last return diff --git a/ssa/stmt_builder.go b/ssa/stmt_builder.go index b5671a9ae6..e2d9aa8589 100644 --- a/ssa/stmt_builder.go +++ b/ssa/stmt_builder.go @@ -130,6 +130,17 @@ func (b Builder) SetBlockEx(blk BasicBlock, pos InsertPoint, setBlk bool) { default: panic("SetBlockEx: invalid pos") } + // Some synthesized instructions (for example defer lowering) may be emitted + // without a source instruction position. Seed a function-scope debug + // location after changing insert point so those instructions still carry a + // valid !dbg in debug builds. + if sp := b.Func.impl.Subprogram(); sp.C != nil { + line := sp.SubprogramLine() + if line == 0 { + line = 1 + } + b.impl.SetCurrentDebugLocation(line, 0, sp, llvm.Metadata{}) + } if setBlk { b.blk = blk } From dc0dfd31e232c821562374a4cfa7c1171128d58f Mon Sep 17 00:00:00 2001 From: luoliwoshang <2643523683@qq.com> Date: Wed, 4 Mar 2026 18:55:58 +0800 Subject: [PATCH 10/10] build: add optional sizeopt linked-module pass mode --- cmd/internal/flags/flags.go | 3 +++ internal/build/build.go | 49 ++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/cmd/internal/flags/flags.go b/cmd/internal/flags/flags.go index 1001aaca85..4015475bde 100644 --- a/cmd/internal/flags/flags.go +++ b/cmd/internal/flags/flags.go @@ -40,6 +40,7 @@ var ForceEspClang bool var SizeReport bool var SizeFormat string var SizeLevel string +var SizeOptimize bool var ForceRebuild bool var PrintCommands bool @@ -65,6 +66,7 @@ func AddBuildFlags(fs *flag.FlagSet) { fs.BoolVar(&SizeReport, "size", false, "Print size report after build (default format=text, level=module)") fs.StringVar(&SizeFormat, "size-format", "", "Size report format (text,json). Default text.") fs.StringVar(&SizeLevel, "size-level", "", "Size report aggregation level (full,module,package). Default module.") + fs.BoolVar(&SizeOptimize, "sizeopt", false, "Enable size-oriented global IR optimization at final link stage") } func AddBuildModeFlags(fs *flag.FlagSet) { @@ -181,6 +183,7 @@ func UpdateConfig(conf *build.Config) error { conf.Port = Port conf.BaudRate = BaudRate conf.ForceRebuild = ForceRebuild + conf.SizeOpt = SizeOptimize if SizeReport || SizeFormat != "" || SizeLevel != "" { conf.SizeReport = true if SizeFormat != "" { diff --git a/internal/build/build.go b/internal/build/build.go index 1e68fc545e..e9e55bb6fa 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -141,6 +141,7 @@ type Config struct { SizeReport bool // print size report after successful build SizeFormat string // size report format: text,json (default text) SizeLevel string // size aggregation level: full,module,package (default module) + SizeOpt bool // enable size-oriented global IR optimization at final link stage CompilerHash string // metadata hash for the running compiler (development builds only) // GlobalRewrites specifies compile-time overrides for global string variables. // Keys are fully qualified package paths (e.g. "main" or "github.com/user/pkg"). @@ -599,6 +600,40 @@ func tempNamePrefix(moduleName string) string { return name } +const linkedBitcodeSizePassPipeline = "module(globalopt,ipsccp,globaldce)" + +func optimizeLinkedBitcode(ctx *context, moduleName, inputBitcode string) (string, error) { + llvmCtx := gllvm.NewContext() + defer llvmCtx.Dispose() + + mod, err := llvmCtx.ParseBitcodeFile(inputBitcode) + if err != nil { + return "", fmt.Errorf("parse linked bitcode %s: %w", inputBitcode, err) + } + defer mod.Dispose() + + mod.SetDataLayout(ctx.prog.DataLayout()) + mod.SetTarget(ctx.prog.Target().Spec().Triple) + + pbo := gllvm.NewPassBuilderOptions() + defer pbo.Dispose() + + if err := mod.RunPasses(linkedBitcodeSizePassPipeline, ctx.prog.TargetMachine(), pbo); err != nil { + return "", fmt.Errorf("run linked bitcode LLVM passes for %s failed: %w", moduleName, err) + } + + out, err := os.CreateTemp("", tempNamePrefix(moduleName)+"-postpass-*.bc") + if err != nil { + return "", fmt.Errorf("create post-pass bitcode file: %w", err) + } + defer out.Close() + + if err := gllvm.WriteBitcodeToFile(mod, out); err != nil { + return "", fmt.Errorf("write post-pass bitcode file: %w", err) + } + return out.Name(), nil +} + func mergeBitcodeFiles(moduleName string, bitcodeFiles []string) (string, error) { if len(bitcodeFiles) == 0 { return "", nil @@ -750,13 +785,25 @@ func compileLinkedBitcodeToObject(ctx *context, moduleName string, bitcodeFiles } } + compileInputBitcode := mergedBitcode + if ctx.buildConf.SizeOpt && ctx.passOpt { + if printCmds { + fmt.Fprintf(os.Stderr, "# running linked bitcode size-opt pass pipeline for %s: %s\n", moduleName, linkedBitcodeSizePassPipeline) + } + optimizedBitcode, err := optimizeLinkedBitcode(ctx, moduleName, mergedBitcode) + if err != nil { + return "", err + } + compileInputBitcode = optimizedBitcode + } + objFile, err := os.CreateTemp("", tempNamePrefix(moduleName)+"-*.o") if err != nil { return "", fmt.Errorf("create linked object temp file: %w", err) } objFile.Close() - args := []string{"-o", objFile.Name(), "-c", mergedBitcode, "-Wno-override-module"} + args := []string{"-o", objFile.Name(), "-c", compileInputBitcode, "-Wno-override-module"} if IsDbgSymsEnabled() { args = append(args, "-gdwarf-4") }