Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 21 additions & 40 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,14 @@ func Do(args []string, conf *Config) ([]Package, error) {
llssa.Initialize(llssa.InitAll)

target := &llssa.Target{
GOOS: conf.Goos,
GOARCH: conf.Goarch,
Target: conf.Target,
GOOS: conf.Goos,
GOARCH: conf.Goarch,
Target: conf.Target,
Triple: export.LLVMTarget,
CPU: export.CPU,
Features: export.Features,
CodeModel: export.CodeModel,
RelocationModel: export.RelocationModel,
}

prog := llssa.NewProgram(target)
Expand Down Expand Up @@ -999,7 +1004,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa
methodByName: methodByName,
abiSymbols: linkedModuleGlobals(linkedOrder),
})
entryObjFile, err := exportObject(ctx, "entry_main", entryPkg.ExportFile, []byte(entryPkg.LPkg.String()))
entryObjFile, err := exportObject(ctx, "entry_main", entryPkg.ExportFile, entryPkg.LPkg.Module())
if err != nil {
return err
}
Expand Down Expand Up @@ -1088,11 +1093,10 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose
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)
fmt.Fprintf(os.Stderr, "# compiling %s from IR %s\n", oFile, objFile)
}
if err := ctx.compiler().Compile(args...); err != nil {
if err := ctx.emitIRFileObject("link", objFile, oFile); err != nil {
return fmt.Errorf("failed to compile %s: %v", objFile, err)
}
compiledObjFiles = append(compiledObjFiles, oFile)
Expand Down Expand Up @@ -1292,7 +1296,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)
}
Expand All @@ -1304,49 +1308,26 @@ 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
}
// Copy instead of rename so we can still compile to .o
if err := copyFileAtomic(f.Name(), llFile); err != nil {
if ctx.buildConf.GenLL || ctx.buildConf.CheckLLFiles {
if err := ctx.writeModuleIR(base, exportFile, mod); err != nil {
return "", err
}
}
// Always compile .ll to .o for linking
objFile, err := os.CreateTemp("", base+"-*.o")
if err != nil {
return "", err
}
objFile.Close()
args := []string{"-o", objFile.Name(), "-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)
fmt.Fprintf(os.Stderr, "# emitting %s for pkg: %s via LLVM\n", objFile.Name(), pkgPath)
}
cmd := ctx.compiler()
return objFile.Name(), cmd.Compile(args...)
if err := ctx.emitModuleObject(pkgPath, mod, objFile.Name()); err != nil {
os.Remove(objFile.Name())
return "", err
}
return objFile.Name(), nil
}

func llcCheck(env *llvm.Env, exportFile string) (msg string, err error) {
Expand Down
217 changes: 217 additions & 0 deletions internal/build/llvm_emit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package build

import (
"fmt"
"os"
"path/filepath"
"strings"

llssa "github.com/goplus/llgo/ssa"
gllvm "github.com/goplus/llvm"
)

type llvmCompileConfig struct {
target llssa.Target
passPipeline string
}

func (c *context) llvmCompileConfig() llvmCompileConfig {
cfg := llvmCompileConfig{}
if base := c.prog.Target(); base != nil {
cfg.target = *base
}
if cfg.target.Triple == "" && c.crossCompile.LLVMTarget != "" {
cfg.target.Triple = c.crossCompile.LLVMTarget
}
if cfg.target.CPU == "" && c.crossCompile.CPU != "" {
cfg.target.CPU = c.crossCompile.CPU
}
if cfg.target.Features == "" && c.crossCompile.Features != "" {
cfg.target.Features = c.crossCompile.Features
}
if cfg.target.CodeModel == "" && c.crossCompile.CodeModel != "" {
cfg.target.CodeModel = c.crossCompile.CodeModel
}
if cfg.target.RelocationModel == "" && c.crossCompile.RelocationModel != "" {
cfg.target.RelocationModel = c.crossCompile.RelocationModel
}

flags := c.compiler().CompilerFlags()
parseLLVMCompileFlags(&cfg, flags)
if cfg.target.RelocationModel == "" {
cfg.target.RelocationModel = defaultLLVMRelocationModel(cfg.target)
}
return cfg
}

func defaultLLVMRelocationModel(target llssa.Target) string {
switch target.GOOS {
case "linux", "android", "darwin", "ios", "freebsd", "netbsd", "openbsd":
if target.GOARCH != "wasm" {
return "pic"
}
}
return ""
}

func parseLLVMCompileFlags(cfg *llvmCompileConfig, flags []string) {
for i := 0; i < len(flags); i++ {
arg := flags[i]
switch {
case arg == "-O0":
cfg.passPipeline = "default<O0>"
cfg.target.CodeGenLevel = gllvm.CodeGenLevelNone
case arg == "-O1" || arg == "-O":
cfg.passPipeline = "default<O1>"
cfg.target.CodeGenLevel = gllvm.CodeGenLevelLess
case arg == "-O2":
cfg.passPipeline = "default<O2>"
cfg.target.CodeGenLevel = gllvm.CodeGenLevelDefault
case arg == "-O3":
cfg.passPipeline = "default<O3>"
cfg.target.CodeGenLevel = gllvm.CodeGenLevelAggressive
case arg == "-Os":
cfg.passPipeline = "default<Os>"
cfg.target.CodeGenLevel = gllvm.CodeGenLevelDefault
case arg == "-Oz":
cfg.passPipeline = "default<Oz>"
cfg.target.CodeGenLevel = gllvm.CodeGenLevelDefault
case arg == "-target" || arg == "--target":
if i+1 < len(flags) {
i++
cfg.target.Triple = flags[i]
}
case strings.HasPrefix(arg, "-target="):
cfg.target.Triple = strings.TrimPrefix(arg, "-target=")
case strings.HasPrefix(arg, "--target="):
cfg.target.Triple = strings.TrimPrefix(arg, "--target=")
case arg == "-mcpu" || arg == "-march" || arg == "-mmcu":
if i+1 < len(flags) {
i++
cfg.target.CPU = flags[i]
}
case strings.HasPrefix(arg, "-mcpu="):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: -march specifies a target architecture (e.g. armv7-a, haswell) while -mcpu specifies a CPU model — these are distinct concepts in LLVM. Mapping both to cfg.target.CPU conflates them. LLVM's CreateTargetMachine CPU parameter expects a CPU name, not an architecture string. Passing an architecture string may silently produce suboptimal code on some targets.

cfg.target.CPU = strings.TrimPrefix(arg, "-mcpu=")
case strings.HasPrefix(arg, "-march="):
cfg.target.CPU = strings.TrimPrefix(arg, "-march=")
case strings.HasPrefix(arg, "-mmcu="):
cfg.target.CPU = strings.TrimPrefix(arg, "-mmcu=")
case arg == "-mattr":
if i+1 < len(flags) {
i++
cfg.target.Features = flags[i]
}
case strings.HasPrefix(arg, "-mattr="):
cfg.target.Features = strings.TrimPrefix(arg, "-mattr=")
case arg == "-mcmodel":
if i+1 < len(flags) {
i++
cfg.target.CodeModel = flags[i]
}
case strings.HasPrefix(arg, "-mcmodel="):
cfg.target.CodeModel = strings.TrimPrefix(arg, "-mcmodel=")
case arg == "-fPIC":
cfg.target.RelocationModel = "pic"
case arg == "-fno-pic":
cfg.target.RelocationModel = "static"
case arg == "-mllvm":
if i+1 < len(flags) {
i++
parseLLVMBackendFlag(cfg, flags[i])
}
case strings.HasPrefix(arg, "-mllvm="):
parseLLVMBackendFlag(cfg, strings.TrimPrefix(arg, "-mllvm="))
}
}
}

func parseLLVMBackendFlag(cfg *llvmCompileConfig, arg string) {
switch {
case strings.HasPrefix(arg, "-mcpu="):
cfg.target.CPU = strings.TrimPrefix(arg, "-mcpu=")
case strings.HasPrefix(arg, "-mattr="):
cfg.target.Features = strings.TrimPrefix(arg, "-mattr=")
}
}

func (c *context) emitModuleObject(pkgPath string, mod gllvm.Module, objPath string) error {
cfg := c.llvmCompileConfig()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

llvmCompileConfig() is recomputed on every call to emitModuleObject — this re-reads env vars, re-instantiates a clang.Cmd, and re-parses flags, even though the config is invariant within a build. For large builds with many packages, consider computing this once and caching it on the context.


mod.SetTarget(cfg.target.Spec().Triple)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emitModuleObject mutates the caller's module in place (SetTarget, SetDataLayout, and potentially runs optimization passes). In exportObject and plan9asm, the current call ordering (write IR first, then emit) happens to be correct, but this is a fragile implicit contract. A future caller reading from the module after emission would get mutated IR. Worth a doc comment on the method warning that it modifies the module.

td, tm := cfg.target.CreateTargetMachine()
defer td.Dispose()
defer tm.Dispose()
mod.SetDataLayout(td.String())
if cfg.passPipeline != "" {
pbo := gllvm.NewPassBuilderOptions()
defer pbo.Dispose()
if err := mod.RunPasses(cfg.passPipeline, tm, pbo); err != nil {
return fmt.Errorf("%s: run LLVM passes %q: %w", pkgPath, cfg.passPipeline, err)
}
}

buf, err := tm.EmitToMemoryBuffer(mod, gllvm.ObjectFile)
if err != nil {
return fmt.Errorf("%s: emit object: %w", pkgPath, err)
}
defer buf.Dispose()

if err := os.WriteFile(objPath, buf.Bytes(), 0644); err != nil {
return fmt.Errorf("%s: write object %s: %w", pkgPath, objPath, err)
}
return nil
}

func (c *context) emitIRFileObject(pkgPath string, llPath string, objPath string) error {
buf, err := gllvm.NewMemoryBufferFromFile(llPath)
if err != nil {
return fmt.Errorf("%s: open IR file %s: %w", pkgPath, llPath, err)
}

ctx := gllvm.NewContext()
defer ctx.Dispose()

mod, err := ctx.ParseIR(buf)
if err != nil {
return fmt.Errorf("%s: parse IR file %s: %w", pkgPath, llPath, err)
}
defer mod.Dispose()

return c.emitModuleObject(pkgPath, mod, objPath)
}

func (c *context) writeModuleIR(base string, exportFile string, mod gllvm.Module) error {
ll := mod.String()

tmp, err := os.CreateTemp("", base+"-*.ll")
if err != nil {
return err
}
tmpPath := tmp.Name()
if _, err := tmp.WriteString(ll); err != nil {
tmp.Close()
os.Remove(tmpPath)
return err
}
if err := tmp.Close(); err != nil {
os.Remove(tmpPath)
return err
}
defer os.Remove(tmpPath)

if c.buildConf.CheckLLFiles {
if msg, err := llcCheck(c.env, tmpPath); err != nil {
fmt.Fprintf(os.Stderr, "==> lcc %v: %v\n%v\n", filepath.Base(exportFile), tmpPath, msg)
}
}
if c.buildConf.GenLL {
llFile := exportFile + ".ll"
if err := os.Chmod(tmpPath, 0644); err != nil {
return err
}
if err := copyFileAtomic(tmpPath, llFile); err != nil {
return err
}
}
return nil
}
38 changes: 8 additions & 30 deletions internal/build/plan9asm.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,35 +77,13 @@ func compilePkgSFiles(ctx *context, aPkg *aPackage, pkg *packages.Package, verbo
if pkg.PkgPath != "runtime" {
ctx.cTransformer.TransformModule(pkg.PkgPath, mod)
}
ll := mod.String()
mod.Dispose()

baseName := aPkg.ExportFile + filepath.Base(sfile) // used for stable debug output paths
tmpPrefix := "plan9asm-" + filepath.Base(sfile) + "-"

llFile, err := os.CreateTemp("", tmpPrefix+"*.ll")
if err != nil {
return nil, fmt.Errorf("%s: create temp .ll for %s: %w", pkg.PkgPath, sfile, err)
}
llPath := llFile.Name()
if _, err := llFile.WriteString(ll); err != nil {
llFile.Close()
os.Remove(llPath)
return nil, fmt.Errorf("%s: write temp .ll for %s: %w", pkg.PkgPath, sfile, err)
}
if err := llFile.Close(); err != nil {
os.Remove(llPath)
return nil, fmt.Errorf("%s: close temp .ll for %s: %w", pkg.PkgPath, sfile, err)
}
defer os.Remove(llPath)

// Keep a copy of translated .ll when GenLL is enabled (mirrors clFile/exportObject).
if ctx.buildConf.GenLL {
dst := baseName + ".ll"
if err := os.Chmod(llPath, 0644); err != nil {
return nil, fmt.Errorf("%s: chmod temp .ll for %s: %w", pkg.PkgPath, sfile, err)
}
if err := copyFileAtomic(llPath, dst); err != nil {
if ctx.buildConf.GenLL || ctx.buildConf.CheckLLFiles {
if err := ctx.writeModuleIR(tmpPrefix, baseName, mod); err != nil {
mod.Dispose()
return nil, fmt.Errorf("%s: keep .ll for %s: %w", pkg.PkgPath, sfile, err)
}
}
Expand All @@ -117,15 +95,15 @@ func compilePkgSFiles(ctx *context, aPkg *aPackage, pkg *packages.Package, verbo
objPath := objFile.Name()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If os.CreateTemp fails here, the function returns without calling mod.Dispose(). The error paths at lines 86 and 102 correctly dispose the module, but this one leaks it.

Consider using defer mod.Dispose() right after obtaining the module (after line 79) to uniformly cover all exit paths, rather than manual Dispose calls on each error branch.

objFile.Close()

args := []string{"-o", objPath, "-c", llPath, "-Wno-override-module"}
if ctx.shouldPrintCommands(verbose) {
fmt.Fprintf(os.Stderr, "# compiling %s for pkg: %s\n", objPath, pkg.PkgPath)
fmt.Fprintln(os.Stderr, "clang", args)
fmt.Fprintf(os.Stderr, "# emitting %s for pkg: %s via LLVM\n", objPath, pkg.PkgPath)
}
if err := ctx.compiler().Compile(args...); err != nil {
if err := ctx.emitModuleObject(pkg.PkgPath, mod, objPath); err != nil {
mod.Dispose()
os.Remove(objPath)
return nil, fmt.Errorf("%s: clang compile asm ll for %s: %w", pkg.PkgPath, sfile, err)
return nil, fmt.Errorf("%s: emit asm object for %s: %w", pkg.PkgPath, sfile, err)
}
mod.Dispose()
Comment on lines +102 to +106
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The explicit calls to mod.Dispose() on both the error path (line 102) and the success path (line 106) can be simplified. By using a defer mod.Dispose() statement after mod is initialized (around line 71), you can ensure that Dispose is always called, which makes the code cleaner and less error-prone. This would also allow removing the mod.Dispose() call in another error path at line 86.

objFiles = append(objFiles, objPath)
}

Expand Down
2 changes: 1 addition & 1 deletion internal/cabi/cabi.go
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ func (p *Transformer) transformCallInstr(m llvm.Module, ctx llvm.Context, call l
updateCallAttr(instr)
}
call.ReplaceAllUsesWith(instr)
call.RemoveFromParentAsInstruction()
call.EraseFromParentAsInstruction()
return true
}

Expand Down
Loading
Loading