Skip to content

Commit 6e76a78

Browse files
cmd/build: support clang-style optimization flags
1 parent 9fa934f commit 6e76a78

File tree

9 files changed

+317
-27
lines changed

9 files changed

+317
-27
lines changed

cmd/internal/flags/flags.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/goplus/llgo/cmd/internal/compilerhash"
77
"github.com/goplus/llgo/internal/build"
88
"github.com/goplus/llgo/internal/buildenv"
9+
"github.com/goplus/llgo/internal/optlevel"
910
)
1011

1112
var OutputFile string
@@ -42,6 +43,7 @@ var SizeFormat string
4243
var SizeLevel string
4344
var ForceRebuild bool
4445
var PrintCommands bool
46+
var OptLevel optlevel.Level
4547

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

@@ -52,6 +54,20 @@ func AddCommonFlags(fs *flag.FlagSet) {
5254
func AddBuildFlags(fs *flag.FlagSet) {
5355
fs.BoolVar(&ForceRebuild, "a", false, "Force rebuilding of packages that are already up-to-date")
5456
fs.BoolVar(&PrintCommands, "x", false, "Print the commands")
57+
fs.Var(optLevelFlag(optlevel.O0), "O0", "Disable optimizations")
58+
fs.Var(optLevelFlag(optlevel.O1), "O1", "Optimize lightly")
59+
fs.Var(optLevelFlag(optlevel.O2), "O2", "Optimize for performance")
60+
fs.Var(optLevelFlag(optlevel.O3), "O3", "Optimize aggressively")
61+
fs.Var(optLevelFlag(optlevel.Os), "Os", "Optimize for size")
62+
fs.Var(optLevelFlag(optlevel.Oz), "Oz", "Optimize aggressively for size")
63+
fs.Func("O", "Optimization level (0,1,2,3,s,z)", func(val string) error {
64+
level, err := optlevel.Parse(val)
65+
if err != nil {
66+
return err
67+
}
68+
OptLevel = level
69+
return nil
70+
})
5571
fs.StringVar(&Tags, "tags", "", "Build tags")
5672
fs.StringVar(&BuildEnv, "buildenv", "", "Build environment")
5773
if buildenv.Dev {
@@ -177,6 +193,7 @@ func UpdateConfig(conf *build.Config) error {
177193
conf.Tags = Tags
178194
conf.Verbose = Verbose
179195
conf.PrintCommands = PrintCommands
196+
conf.OptLevel = OptLevel
180197
conf.Target = Target
181198
conf.Port = Port
182199
conf.BaudRate = BaudRate
@@ -235,3 +252,18 @@ func UpdateBuildConfig(conf *build.Config) error {
235252

236253
return nil
237254
}
255+
256+
type optLevelFlag optlevel.Level
257+
258+
func (f optLevelFlag) String() string {
259+
return optlevel.Level(f).Suffix()
260+
}
261+
262+
func (f optLevelFlag) Set(string) error {
263+
OptLevel = optlevel.Level(f)
264+
return nil
265+
}
266+
267+
func (f optLevelFlag) IsBoolFlag() bool {
268+
return true
269+
}

cmd/internal/flags/flags_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package flags
2+
3+
import (
4+
"flag"
5+
"testing"
6+
7+
"github.com/goplus/llgo/internal/optlevel"
8+
)
9+
10+
func resetBuildFlagGlobals() {
11+
OptLevel = optlevel.Unset
12+
}
13+
14+
func TestOptimizationFlags(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
args []string
18+
want optlevel.Level
19+
}{
20+
{name: "shorthand O2", args: []string{"-O2"}, want: optlevel.O2},
21+
{name: "shorthand Oz", args: []string{"-Oz"}, want: optlevel.Oz},
22+
{name: "explicit O value", args: []string{"-O=s"}, want: optlevel.Os},
23+
{name: "space separated O value", args: []string{"-O", "3"}, want: optlevel.O3},
24+
{name: "last flag wins", args: []string{"-O1", "-Oz"}, want: optlevel.Oz},
25+
}
26+
27+
for _, tt := range tests {
28+
t.Run(tt.name, func(t *testing.T) {
29+
resetBuildFlagGlobals()
30+
fs := flag.NewFlagSet("test", flag.ContinueOnError)
31+
AddBuildFlags(fs)
32+
if err := fs.Parse(tt.args); err != nil {
33+
t.Fatalf("Parse(%v) failed: %v", tt.args, err)
34+
}
35+
if OptLevel != tt.want {
36+
t.Fatalf("OptLevel = %v, want %v", OptLevel, tt.want)
37+
}
38+
})
39+
}
40+
}
41+
42+
func TestOptimizationFlagsRejectInvalidLevel(t *testing.T) {
43+
resetBuildFlagGlobals()
44+
fs := flag.NewFlagSet("test", flag.ContinueOnError)
45+
AddBuildFlags(fs)
46+
if err := fs.Parse([]string{"-O=fast"}); err == nil {
47+
t.Fatal("Parse(-O=fast) succeeded, want error")
48+
}
49+
}

cmd/internal/monitor/monitor.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/goplus/llgo/cmd/internal/flags"
2525
"github.com/goplus/llgo/internal/crosscompile"
2626
"github.com/goplus/llgo/internal/monitor"
27+
"github.com/goplus/llgo/internal/optlevel"
2728
)
2829

2930
// Cmd represents the monitor command.
@@ -34,6 +35,7 @@ var Cmd = &base.Command{
3435

3536
func init() {
3637
flags.AddCommonFlags(&Cmd.Flag)
38+
flags.AddBuildFlags(&Cmd.Flag)
3739
flags.AddEmbeddedFlags(&Cmd.Flag)
3840
Cmd.Run = runMonitor
3941
}
@@ -54,7 +56,11 @@ func runMonitor(cmd *base.Command, args []string) {
5456

5557
var serialPort []string
5658
if flags.Target != "" {
57-
conf, err := crosscompile.UseTarget(flags.Target)
59+
level := flags.OptLevel
60+
if !level.IsValid() {
61+
level = optlevel.Oz
62+
}
63+
conf, err := crosscompile.UseTarget(flags.Target, level)
5864
if err != nil {
5965
fmt.Fprintf(os.Stderr, "llgo monitor: %v\n", err)
6066
os.Exit(1)

internal/build/build.go

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import (
4848
"github.com/goplus/llgo/internal/header"
4949
"github.com/goplus/llgo/internal/mockable"
5050
"github.com/goplus/llgo/internal/monitor"
51+
"github.com/goplus/llgo/internal/optlevel"
5152
"github.com/goplus/llgo/internal/packages"
5253
"github.com/goplus/llgo/internal/typepatch"
5354
"github.com/goplus/llgo/ssa/abi"
@@ -117,6 +118,7 @@ type Config struct {
117118
Goos string
118119
Goarch string
119120
Target string // target name (e.g., "rp2040", "wasi") - takes precedence over Goos/Goarch
121+
OptLevel optlevel.Level
120122
BinPath string
121123
AppExt string // ".exe" on Windows, empty on Unix
122124
OutFile string // only valid for ModeBuild when len(pkgs) == 1
@@ -224,9 +226,11 @@ func Do(args []string, conf *Config) ([]Package, error) {
224226
if err := ensureSizeReporting(conf); err != nil {
225227
return nil, err
226228
}
229+
conf.OptLevel = effectiveOptLevel(conf)
230+
227231
// Handle crosscompile configuration first to set correct GOOS/GOARCH
228232
forceEspClang := conf.ForceEspClang || conf.Target != ""
229-
export, err := crosscompile.Use(conf.Goos, conf.Goarch, conf.Target, IsWasiThreadsEnabled(), forceEspClang)
233+
export, err := crosscompile.Use(conf.Goos, conf.Goarch, conf.Target, IsWasiThreadsEnabled(), forceEspClang, conf.OptLevel)
230234
if err != nil {
231235
return nil, fmt.Errorf("failed to setup crosscompile: %w", err)
232236
}
@@ -273,9 +277,10 @@ func Do(args []string, conf *Config) ([]Package, error) {
273277
llssa.Initialize(llssa.InitAll)
274278

275279
target := &llssa.Target{
276-
GOOS: conf.Goos,
277-
GOARCH: conf.Goarch,
278-
Target: conf.Target,
280+
GOOS: conf.Goos,
281+
GOARCH: conf.Goarch,
282+
Target: conf.Target,
283+
OptLevel: conf.OptLevel,
279284
}
280285

281286
prog := llssa.NewProgram(target)
@@ -340,16 +345,17 @@ func Do(args []string, conf *Config) ([]Package, error) {
340345
preCollectRuntimeLinknames(prog, altPkgs)
341346

342347
buildMode := ssaBuildMode
343-
cabiOptimize := true
344-
passOpt := true
348+
optLevel := conf.OptLevel
349+
cabiOptimize := optLevel.IsOptimized()
350+
passOpt := optLevel.IsOptimized()
345351
if IsDbgEnabled() || mode == ModeGen {
346352
passOpt = false
347353
}
348354
if IsDbgEnabled() {
349355
buildMode |= ssa.GlobalDebug
350356
cabiOptimize = false
351357
}
352-
if !IsOptimizeEnabled() {
358+
if optLevel == optlevel.O0 {
353359
buildMode |= ssa.NaiveForm
354360
}
355361
progSSA := ssa.NewProgram(initial[0].Fset, buildMode)
@@ -1671,10 +1677,6 @@ func IsDbgSymsEnabled() bool {
16711677
return isEnvOn(llgoDbgSyms, false)
16721678
}
16731679

1674-
func IsOptimizeEnabled() bool {
1675-
return isEnvOn(llgoOptimize, true)
1676-
}
1677-
16781680
func IsWasiThreadsEnabled() bool {
16791681
return isEnvOn(llgoWasiThreads, true)
16801682
}
@@ -1691,6 +1693,35 @@ func WasmRuntime() string {
16911693
return defaultEnv(llgoWasmRuntime, defaultWasmRuntime)
16921694
}
16931695

1696+
func defaultOptLevel(conf *Config) optlevel.Level {
1697+
if conf != nil && conf.Target != "" {
1698+
return optlevel.Oz
1699+
}
1700+
return optlevel.O2
1701+
}
1702+
1703+
func effectiveOptLevel(conf *Config) optlevel.Level {
1704+
if conf != nil && conf.OptLevel.IsValid() {
1705+
return conf.OptLevel
1706+
}
1707+
1708+
envVal := strings.TrimSpace(strings.ToLower(os.Getenv(llgoOptimize)))
1709+
if envVal == "" {
1710+
return defaultOptLevel(conf)
1711+
}
1712+
if level, err := optlevel.Parse(envVal); err == nil {
1713+
return level
1714+
}
1715+
switch envVal {
1716+
case "0", "false", "off", "no":
1717+
return optlevel.O0
1718+
case "1", "true", "on", "yes":
1719+
return defaultOptLevel(conf)
1720+
default:
1721+
return defaultOptLevel(conf)
1722+
}
1723+
}
1724+
16941725
func concatPkgLinkFiles(ctx *context, pkg *packages.Package, verbose bool) (parts []string) {
16951726
llgoPkgLinkFiles(ctx, pkg, func(linkFile string) {
16961727
parts = append(parts, linkFile)

internal/build/optlevel_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package build
2+
3+
import (
4+
"testing"
5+
6+
"github.com/goplus/llgo/internal/optlevel"
7+
)
8+
9+
func TestEffectiveOptLevelDefaults(t *testing.T) {
10+
t.Setenv(llgoOptimize, "")
11+
12+
if got := effectiveOptLevel(&Config{}); got != optlevel.O2 {
13+
t.Fatalf("host default opt level = %v, want %v", got, optlevel.O2)
14+
}
15+
if got := effectiveOptLevel(&Config{Target: "rp2040"}); got != optlevel.Oz {
16+
t.Fatalf("embedded default opt level = %v, want %v", got, optlevel.Oz)
17+
}
18+
}
19+
20+
func TestEffectiveOptLevelOverride(t *testing.T) {
21+
t.Setenv(llgoOptimize, "")
22+
23+
if got := effectiveOptLevel(&Config{OptLevel: optlevel.O3}); got != optlevel.O3 {
24+
t.Fatalf("explicit opt level = %v, want %v", got, optlevel.O3)
25+
}
26+
if got := effectiveOptLevel(&Config{Target: "esp32", OptLevel: optlevel.Os}); got != optlevel.Os {
27+
t.Fatalf("explicit embedded opt level = %v, want %v", got, optlevel.Os)
28+
}
29+
}
30+
31+
func TestEffectiveOptLevelLegacyEnv(t *testing.T) {
32+
t.Setenv(llgoOptimize, "off")
33+
if got := effectiveOptLevel(&Config{}); got != optlevel.O0 {
34+
t.Fatalf("LLGO_OPTIMIZE=off opt level = %v, want %v", got, optlevel.O0)
35+
}
36+
37+
t.Setenv(llgoOptimize, "on")
38+
if got := effectiveOptLevel(&Config{Target: "rp2040"}); got != optlevel.Oz {
39+
t.Fatalf("LLGO_OPTIMIZE=on embedded opt level = %v, want %v", got, optlevel.Oz)
40+
}
41+
42+
t.Setenv(llgoOptimize, "O1")
43+
if got := effectiveOptLevel(&Config{}); got != optlevel.O1 {
44+
t.Fatalf("LLGO_OPTIMIZE=O1 opt level = %v, want %v", got, optlevel.O1)
45+
}
46+
}

internal/crosscompile/crosscompile.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/goplus/llgo/internal/crosscompile/compile"
1414
"github.com/goplus/llgo/internal/env"
1515
"github.com/goplus/llgo/internal/flash"
16+
"github.com/goplus/llgo/internal/optlevel"
1617
"github.com/goplus/llgo/internal/targets"
1718
"github.com/goplus/llgo/internal/xtool/llvm"
1819
envllvm "github.com/goplus/llgo/xtool/env/llvm"
@@ -197,7 +198,10 @@ func compileWithConfig(
197198
return
198199
}
199200

200-
func use(goos, goarch string, wasiThreads, forceEspClang bool) (export Export, err error) {
201+
func use(goos, goarch string, wasiThreads, forceEspClang bool, optLevel optlevel.Level) (export Export, err error) {
202+
if !optLevel.IsValid() {
203+
return export, fmt.Errorf("invalid optimization level: %s", optLevel)
204+
}
201205
targetTriple := llvm.GetTargetTriple(goos, goarch)
202206
llgoRoot := env.LLGoROOT()
203207

@@ -247,6 +251,7 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool) (export Export, e
247251
}
248252
}
249253
export.CCFLAGS = []string{
254+
optLevel.Flag(),
250255
"-Qunused-arguments",
251256
"-Wno-unused-command-line-argument",
252257
}
@@ -323,6 +328,7 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool) (export Export, e
323328
// Use system clang and sysroot of wasi-sdk
324329
// Add compiler flags
325330
export.CCFLAGS = []string{
331+
optLevel.Flag(),
326332
"-target", targetTriple,
327333
"--sysroot=" + sysrootDir,
328334
"-resource-dir=" + libclangDir,
@@ -382,6 +388,7 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool) (export Export, e
382388
export.CC = "emcc"
383389
// Add compiler flags
384390
export.CCFLAGS = []string{
391+
optLevel.Flag(),
385392
"-target", targetTriple,
386393
"-Qunused-arguments",
387394
"-Wno-unused-command-line-argument",
@@ -424,7 +431,10 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool) (export Export, e
424431
}
425432

426433
// UseTarget loads configuration from a target name (e.g., "rp2040", "wasi")
427-
func UseTarget(targetName string) (export Export, err error) {
434+
func UseTarget(targetName string, optLevel optlevel.Level) (export Export, err error) {
435+
if !optLevel.IsValid() {
436+
return export, fmt.Errorf("invalid optimization level: %s", optLevel)
437+
}
428438
resolver := targets.NewDefaultResolver()
429439

430440
config, err := resolver.Resolve(targetName)
@@ -489,7 +499,7 @@ func UseTarget(targetName string) (export Export, err error) {
489499
// Convert LLVMTarget, CPU, Features to CCFLAGS/LDFLAGS
490500
// Enable ICF (Identical Code Folding) to reduce binary size
491501
ldflags := []string{"-S", "--icf=safe"}
492-
ccflags := []string{"-Oz"}
502+
ccflags := []string{optLevel.Flag()}
493503
cflags := []string{"-Wno-override-module", "-Qunused-arguments", "-Wno-unused-command-line-argument"}
494504
if config.LLVMTarget != "" {
495505
cflags = append(cflags, "--target="+config.LLVMTarget)
@@ -658,9 +668,9 @@ func UseTarget(targetName string) (export Export, err error) {
658668

659669
// Use extends the original Use function to support target-based configuration
660670
// If targetName is provided, it takes precedence over goos/goarch
661-
func Use(goos, goarch, targetName string, wasiThreads, forceEspClang bool) (export Export, err error) {
671+
func Use(goos, goarch, targetName string, wasiThreads, forceEspClang bool, optLevel optlevel.Level) (export Export, err error) {
662672
if targetName != "" && !strings.HasPrefix(targetName, "wasm") && !strings.HasPrefix(targetName, "wasi") {
663-
return UseTarget(targetName)
673+
return UseTarget(targetName, optLevel)
664674
}
665-
return use(goos, goarch, wasiThreads, forceEspClang)
675+
return use(goos, goarch, wasiThreads, forceEspClang, optLevel)
666676
}

0 commit comments

Comments
 (0)