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
3 changes: 3 additions & 0 deletions _demo/embed/targetsbuild/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ case "$test_dir" in
"riscv32"
"riscv64"
"rp2040"
# nintendoswitch: undefined symbol under lto, the compiled files is not enough.
# For no-lto, it works because the testcases does not use the missing fucntions.
"nintendoswitch"
)
;;
defer)
Expand Down
57 changes: 57 additions & 0 deletions cl/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,55 @@
package cl_test

import (
"go/ast"
"go/parser"
"go/token"
"go/types"
"os"
"runtime"
"strings"
"testing"

"github.com/goplus/gogen/packages"
"github.com/goplus/llgo/cl"
"github.com/goplus/llgo/cl/cltest"
"github.com/goplus/llgo/internal/build"
"github.com/goplus/llgo/internal/llgen"
"github.com/goplus/llgo/ssa/ssatest"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/ssa/ssautil"
)

func testCompile(t *testing.T, src, expected string) {
t.Helper()
cltest.TestCompileEx(t, src, "foo.go", expected, false)
}

func compileIRWithGoGlobalDCE(t *testing.T, src string) string {
t.Helper()
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments)
if err != nil {
t.Fatal("ParseFile failed:", err)
}
files := []*ast.File{f}
pkg := types.NewPackage(f.Name.Name, f.Name.Name)
imp := packages.NewImporter(fset)
foo, _, err := ssautil.BuildPackage(
&types.Config{Importer: imp}, fset, pkg, files, ssa.SanityCheckFunctions|ssa.InstantiateGenerics)
if err != nil {
t.Fatal("BuildPackage failed:", err)
}
prog := ssatest.NewProgramEx(t, nil, imp)
prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH))
prog.EnableGoGlobalDCE(true)
ret, err := cl.NewPackage(prog, foo, files)
if err != nil {
t.Fatal("cl.NewPackage failed:", err)
}
return ret.String()
}

func requireEmbedTest(t *testing.T) {
t.Helper()
if os.Getenv("LLGO_EMBED_TESTS") != "1" {
Expand Down Expand Up @@ -321,6 +354,30 @@ func TestCgofullGeneratesC2func(t *testing.T) {
}
}

func TestGoGlobalDCEPhase1IR(t *testing.T) {
ir := compileIRWithGoGlobalDCE(t, `package foo

type I interface {
F(int) int
}

type T struct{}

func (T) F(x int) int { return x + 1 }
func (T) G() {}

func use(i I) int {
return i.F(1)
}
`)
if !strings.Contains(ir, "llvm.type.checked.load") {
t.Fatalf("missing llvm.type.checked.load in IR:\n%s", ir)
}
if !strings.Contains(ir, "go.method.F:") {
t.Fatalf("missing method capability metadata for F:\n%s", ir)
}
}

func TestGoPkgMath(t *testing.T) {
conf := build.NewDefaultConf(build.ModeInstall)
_, err := build.Do([]string{"math"}, conf)
Expand Down
50 changes: 50 additions & 0 deletions cmd/internal/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package flags

import (
"flag"
"fmt"
"strconv"

"github.com/goplus/llgo/cmd/internal/compilerhash"
"github.com/goplus/llgo/internal/build"
Expand Down Expand Up @@ -43,6 +45,49 @@ var SizeLevel string
var ForceRebuild bool
var PrintCommands bool

type optionalBool struct {
Specified bool
Value bool
}

func (o *optionalBool) String() string {
if !o.Specified {
return "auto"
}
return strconv.FormatBool(o.Value)
}

func (o *optionalBool) SetValue(v string) error {
val, err := strconv.ParseBool(v)
if err != nil {
return fmt.Errorf("invalid bool value %q", v)
}
o.Specified = true
o.Value = val
return nil
}

func (o *optionalBool) Set(v string) error {
return o.SetValue(v)
}

func (o *optionalBool) IsBoolFlag() bool {
return true
}

var LTO optionalBool

func AddLTOFlag(fs *flag.FlagSet) {
fs.Var(&LTO, "lto", "Enable LTO optimization (default: on for -target builds, off for non-target builds)")
}

func ResolveLTO(defaultValue bool) bool {
if LTO.Specified {
return LTO.Value
}
return defaultValue
}

const DefaultTestTimeout = "10m" // Matches Go's default test timeout

func AddCommonFlags(fs *flag.FlagSet) {
Expand All @@ -52,6 +97,7 @@ func AddCommonFlags(fs *flag.FlagSet) {
func AddBuildFlags(fs *flag.FlagSet) {
fs.BoolVar(&ForceRebuild, "a", false, "Force rebuilding of packages that are already up-to-date")
fs.BoolVar(&PrintCommands, "x", false, "Print the commands")
AddLTOFlag(fs)
fs.StringVar(&Tags, "tags", "", "Build tags")
fs.StringVar(&BuildEnv, "buildenv", "", "Build environment")
if buildenv.Dev {
Expand Down Expand Up @@ -181,6 +227,10 @@ func UpdateConfig(conf *build.Config) error {
conf.Port = Port
conf.BaudRate = BaudRate
conf.ForceRebuild = ForceRebuild
if LTO.Specified {
lto := LTO.Value
conf.LTO = &lto
}
if SizeReport || SizeFormat != "" || SizeLevel != "" {
conf.SizeReport = true
if SizeFormat != "" {
Expand Down
3 changes: 2 additions & 1 deletion cmd/internal/monitor/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var Cmd = &base.Command{

func init() {
flags.AddCommonFlags(&Cmd.Flag)
flags.AddLTOFlag(&Cmd.Flag)
flags.AddEmbeddedFlags(&Cmd.Flag)
Cmd.Run = runMonitor
}
Expand All @@ -54,7 +55,7 @@ func runMonitor(cmd *base.Command, args []string) {

var serialPort []string
if flags.Target != "" {
conf, err := crosscompile.UseTarget(flags.Target)
conf, err := crosscompile.UseTarget(flags.Target, flags.ResolveLTO(true))
if err != nil {
fmt.Fprintf(os.Stderr, "llgo monitor: %v\n", err)
os.Exit(1)
Expand Down
11 changes: 10 additions & 1 deletion internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ type Config struct {
Goos string
Goarch string
Target string // target name (e.g., "rp2040", "wasi") - takes precedence over Goos/Goarch
LTO *bool // nil means auto: on for target builds, off for non-target builds
BinPath string
AppExt string // ".exe" on Windows, empty on Unix
OutFile string // only valid for ModeBuild when len(pkgs) == 1
Expand Down Expand Up @@ -194,6 +195,13 @@ func envGOPATH() (string, error) {
return filepath.Join(home, "go"), nil
}

func (c *Config) ltoEnabled() bool {
if c.LTO != nil {
return *c.LTO
}
return c.Target != ""
}

// -----------------------------------------------------------------------------

const (
Expand Down Expand Up @@ -227,7 +235,7 @@ func Do(args []string, conf *Config) ([]Package, error) {
}
// Handle crosscompile configuration first to set correct GOOS/GOARCH
forceEspClang := conf.ForceEspClang || conf.Target != ""
export, err := crosscompile.Use(conf.Goos, conf.Goarch, conf.Target, IsWasiThreadsEnabled(), forceEspClang)
export, err := crosscompile.Use(conf.Goos, conf.Goarch, conf.Target, IsWasiThreadsEnabled(), forceEspClang, conf.ltoEnabled())
if err != nil {
return nil, fmt.Errorf("failed to setup crosscompile: %w", err)
}
Expand Down Expand Up @@ -280,6 +288,7 @@ func Do(args []string, conf *Config) ([]Package, error) {
}

prog := llssa.NewProgram(target)
prog.EnableGoGlobalDCE(conf.ltoEnabled())
sizes := func(sizes types.Sizes, compiler, arch string) types.Sizes {
if arch == "wasm" {
sizes = &types.StdSizes{WordSize: 4, MaxAlign: 4}
Expand Down
27 changes: 27 additions & 0 deletions internal/build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,30 @@ func Sigsetjmp()
t.Fatalf("pre-collected runtime linkname = (%q,%v), want (%q,%v)", got, ok, "C.sigsetjmp", true)
}
}

func TestLTOEnabledDefault(t *testing.T) {
host := &Config{Target: ""}
if host.ltoEnabled() {
t.Fatal("expected LTO disabled by default for non-target builds")
}

target := &Config{Target: "rp2040"}
if !target.ltoEnabled() {
t.Fatal("expected LTO enabled by default for target builds")
}
}

func TestLTOEnabledExplicitOverride(t *testing.T) {
on := true
off := false

hostOn := &Config{Target: "", LTO: &on}
if !hostOn.ltoEnabled() {
t.Fatal("expected explicit LTO=true to enable LTO for non-target build")
}

targetOff := &Config{Target: "rp2040", LTO: &off}
if targetOff.ltoEnabled() {
t.Fatal("expected explicit LTO=false to disable LTO for target build")
}
}
6 changes: 6 additions & 0 deletions internal/cabi/cabi.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,9 @@ func (p *Transformer) transformFuncType(ctx llvm.Context, info *FuncInfo) (llvm.

func (p *Transformer) transformFunc(m llvm.Module, fn llvm.Value) bool {
ctx := m.Context()
if fn.IntrinsicID() != 0 {
return false
}
info := p.GetFuncInfo(ctx, fn.GlobalValueType())
if !info.HasWrap() {
return false
Expand Down Expand Up @@ -526,6 +529,9 @@ func (p *Transformer) transformFuncBody(m llvm.Module, ctx llvm.Context, info *F

func (p *Transformer) transformCallInstr(m llvm.Module, ctx llvm.Context, call llvm.Value, fn llvm.Value) bool {
nfn := call.CalledValue()
if nfn.IntrinsicID() != 0 {
return false
}
info := p.GetFuncInfo(ctx, call.CalledFunctionType())
if !info.HasWrap() {
return false
Expand Down
37 changes: 30 additions & 7 deletions internal/crosscompile/crosscompile.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func compileWithConfig(
return
}

func use(goos, goarch string, wasiThreads, forceEspClang bool) (export Export, err error) {
func use(goos, goarch string, wasiThreads, forceEspClang, enableLTO bool) (export Export, err error) {
targetTriple := llvm.GetTargetTriple(goos, goarch)
llgoRoot := env.LLGoROOT()

Expand Down Expand Up @@ -225,8 +225,11 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool) (export Export, e
"-Wl,--error-limit=0",
"-fuse-ld=lld",
// Enable ICF (Identical Code Folding) to reduce binary size
"-Xlinker",
"--icf=safe",
"-Wl,--icf=safe",
}
if enableLTO {
// Enable ThinLTO, using default lto kind(thinlto).
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 comment mentions enabling ThinLTO, but the compiler flag being added on line 257 is -flto=full, which enables full LTO. This is inconsistent and could be confusing. Please update the comment to reflect that full LTO is being used. A similar issue exists on line 509.

export.LDFLAGS = append(export.LDFLAGS, "-Wl,--lto-O1", "-v")
}
if clangRoot != "" {
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.

Two issues on this line:

  1. Debug -v flag leaked into production: The -v flag causes clang to dump verbose output (full toolchain paths, sysroot, library search dirs, internal linker command lines) for every LTO-enabled build. This looks like a debugging leftover — it's not present in the UseTarget path (line 511). Should be removed before merge.

  2. Comment/code mismatch (ThinLTO vs Full LTO): The comment on line 233 says "Enable ThinLTO, using default lto kind(thinlto)" but -flto=full (line 259) selects full LTO, not thin LTO. These are fundamentally different strategies. Same mismatch exists in UseTarget at line 509. The comments should say "Full LTO" or the flags should use -flto=thin.

clangLib := filepath.Join(clangRoot, "lib")
Expand All @@ -250,6 +253,9 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool) (export Export, e
"-Qunused-arguments",
"-Wno-unused-command-line-argument",
}
if enableLTO {
export.CCFLAGS = append(export.CCFLAGS, "-flto=full")
}

// Add sysroot for macOS only
if goos == "darwin" {
Expand Down Expand Up @@ -424,7 +430,7 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool) (export Export, e
}

// UseTarget loads configuration from a target name (e.g., "rp2040", "wasi")
func UseTarget(targetName string) (export Export, err error) {
func UseTarget(targetName string, enableLTO bool) (export Export, err error) {
resolver := targets.NewDefaultResolver()

config, err := resolver.Resolve(targetName)
Expand Down Expand Up @@ -499,6 +505,13 @@ func UseTarget(targetName string) (export Export, err error) {
expandedCFlags := env.ExpandEnvSlice(config.CFlags, envs)
cflags = append(cflags, expandedCFlags...)

if config.Linker == "ld.lld" && enableLTO {
// Enable ThinLTO, Using default lto kind(thinlto).
ldflags = append(ldflags, "--lto-O1")
cflags = append(cflags, "-flto=full")
ccflags = append(ccflags, "-flto=full")
}

// The following parameters are inspired by tinygo/builder/library.go
// Handle CPU configuration
if cpu != "" {
Expand Down Expand Up @@ -545,6 +558,8 @@ func UseTarget(targetName string) (export Export, err error) {
ccflags = append(ccflags, "-fforce-enable-int128")
case "riscv64":
ccflags = append(ccflags, "-march=rv64gc")
// codegen option should be added to ldflags for lto
ldflags = append(ldflags, "-mllvm", "-march=rv64gc")
case "mips":
ccflags = append(ccflags, "-fno-pic")
}
Expand Down Expand Up @@ -574,9 +589,17 @@ func UseTarget(targetName string) (export Export, err error) {
// Handle code generation configuration
if config.CodeModel != "" {
ccflags = append(ccflags, "-mcmodel="+config.CodeModel)
if enableLTO {
// codegen option should be added to ldflags for lto
ldflags = append(ldflags, "-mllvm", "-code-model="+config.CodeModel)
}
}
if config.TargetABI != "" {
ccflags = append(ccflags, "-mabi="+config.TargetABI)
if enableLTO {
// codegen option should be added to ldflags for lto
ldflags = append(ldflags, "-mllvm", "-target-abi="+config.TargetABI)
}
}
if config.RelocationModel != "" {
switch config.RelocationModel {
Expand Down Expand Up @@ -658,9 +681,9 @@ func UseTarget(targetName string) (export Export, err error) {

// Use extends the original Use function to support target-based configuration
// If targetName is provided, it takes precedence over goos/goarch
func Use(goos, goarch, targetName string, wasiThreads, forceEspClang bool) (export Export, err error) {
func Use(goos, goarch, targetName string, wasiThreads, forceEspClang, enableLTO bool) (export Export, err error) {
if targetName != "" && !strings.HasPrefix(targetName, "wasm") && !strings.HasPrefix(targetName, "wasi") {
return UseTarget(targetName)
return UseTarget(targetName, enableLTO)
}
return use(goos, goarch, wasiThreads, forceEspClang)
return use(goos, goarch, wasiThreads, forceEspClang, enableLTO)
}
Loading
Loading