diff --git a/builder/build.go b/builder/build.go index 266179baaa..eb23adc05f 100644 --- a/builder/build.go +++ b/builder/build.go @@ -248,6 +248,9 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe if err != nil { return BuildResult{}, err } + if _, ok := globalValues["runtime"]["godebugDefault"]; !ok { + globalValues["runtime"]["godebugDefault"] = lprogram.MainPkg().DefaultGODEBUG + } result := BuildResult{ ModuleRoot: lprogram.MainPkg().Module.Dir, MainDir: lprogram.MainPkg().Dir, @@ -1365,6 +1368,9 @@ func createEmbedObjectFile(data, hexSum, sourceFile, sourceDir, tmpdir string, c // needed to convert a program to its final form. Some transformations are not // optional and must be run as the compiler expects them to run. func optimizeProgram(mod llvm.Module, config *compileopts.Config) error { + if err := transform.PrepareRuntimeStartup(mod); err != nil { + return err + } err := interp.Run(mod, config.Options.InterpTimeout, config.Options.InterpMaxLoopIterations, config.DumpSSA()) if err != nil { return err diff --git a/builder/testdata/binary-size.txt b/builder/testdata/binary-size.txt index cfdd88aaea..de501b2758 100644 --- a/builder/testdata/binary-size.txt +++ b/builder/testdata/binary-size.txt @@ -17,5 +17,5 @@ pico2 ./testdata/size-os z 6199 1209 128 5592 pico2 ./testdata/size-os 2 15290 1014 128 5592 cortex-m-qemu ./testdata/size-corpus z 9891 1301 80 4188 cortex-m-qemu ./testdata/size-corpus 2 17466 862 72 4188 -riscv-qemu ./testdata/size-corpus z 17508 1028 88 16660 -riscv-qemu ./testdata/size-corpus 2 25160 744 80 16644 +riscv-qemu ./testdata/size-corpus z 17556 1028 88 16660 +riscv-qemu ./testdata/size-corpus 2 23225 935 80 16644 diff --git a/compiler/compiler.go b/compiler/compiler.go index 421a9b7676..d28e36c095 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -2355,6 +2355,8 @@ func (b *builder) createBuiltin(argTypes []types.Type, argValues []llvm.Value, c // // This is also where compiler intrinsics are implemented. func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error) { + b.markRuntimeFeatureUse(instr) + // See if this is an intrinsic function that is handled specially. if fn := instr.StaticCallee(); fn != nil { // Direct function call, either to a named or anonymous (directly diff --git a/compiler/defer.go b/compiler/defer.go index 8340a3d83d..f1ec306c67 100644 --- a/compiler/defer.go +++ b/compiler/defer.go @@ -420,6 +420,8 @@ func (b *builder) loadDeferredCallParams(structType llvm.Type, ptr llvm.Value) [ // createDefer emits a single defer instruction, to be run when this function // returns. func (b *builder) createDefer(instr *ssa.Defer) { + b.markRuntimeFeatureUse(&instr.Call) + // The pointer to the previous defer struct, which we will replace to // make a linked list. next := b.CreateLoad(b.dataPtrType, b.deferPtr, "defer.next") diff --git a/compiler/goroutine.go b/compiler/goroutine.go index 72a0c7706b..5b399d17e9 100644 --- a/compiler/goroutine.go +++ b/compiler/goroutine.go @@ -14,6 +14,8 @@ import ( // createGo emits code to start a new goroutine. func (b *builder) createGo(instr *ssa.Go) { + b.markRuntimeFeatureUse(&instr.Call) + if builtin, ok := instr.Call.Value.(*ssa.Builtin); ok { // We cheat. None of the builtins do any long or blocking operation, so // we might as well run these builtins right away without the program diff --git a/compiler/runtimefeature.go b/compiler/runtimefeature.go new file mode 100644 index 0000000000..040460bbf5 --- /dev/null +++ b/compiler/runtimefeature.go @@ -0,0 +1,34 @@ +package compiler + +import ( + "golang.org/x/tools/go/ssa" + "tinygo.org/x/go-llvm" +) + +func (b *builder) markRuntimeFeatureUse(call *ssa.CallCommon) { + callee := call.StaticCallee() + if callee == nil { + return + } + + var markerName string + switch b.getFunctionInfo(callee).linkName { + case "runtime.GOROOT": + markerName = "tinygo.runtime.feature.goroot" + case "(*internal/godebug.Setting).Value": + markerName = "tinygo.runtime.feature.godebug" + case "internal/godebug.setUpdate": + if b.fn.Pkg != nil && b.fn.Pkg.Pkg.Path() == "internal/godebug" { + return + } + markerName = "tinygo.runtime.feature.godebug" + default: + return + } + + marker := b.mod.NamedFunction(markerName) + if marker.IsNil() { + marker = llvm.AddFunction(b.mod, markerName, llvm.FunctionType(b.ctx.VoidType(), nil, false)) + } + b.CreateCall(marker.GlobalValueType(), marker, nil, "") +} diff --git a/loader/loader.go b/loader/loader.go index 7996154bf3..bcbb68a8f3 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -54,14 +54,15 @@ type Program struct { // PackageJSON is a subset of the JSON struct returned from `go list`. type PackageJSON struct { - Dir string - ImportPath string - Name string - ForTest string - Root string - Standard bool - DepOnly bool - Module struct { + Dir string + ImportPath string + Name string + ForTest string + Root string + Standard bool + DepOnly bool + DefaultGODEBUG string + Module struct { Path string Main bool Dir string diff --git a/runtime_test.go b/runtime_test.go index 0fba3f2a26..bde803e82a 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -13,6 +13,7 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" "github.com/tinygo-org/tinygo/builder" + "github.com/tinygo-org/tinygo/goenv" ) // TestTimerStopResetRace checks that stopping or resetting a timer while its @@ -166,3 +167,71 @@ func TestStdin(t *testing.T) { } checkOutput(t, TESTDATA+"/stdin.txt", output.Bytes()) } + +func TestRuntimeGOROOT(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + environment []string + want string + }{ + {"startup environment", []string{"GOROOT=/startup"}, "/startup"}, + {"empty startup environment", []string{"GOROOT="}, goenv.Get("GOROOT")}, + } { + t.Run(test.name, func(t *testing.T) { + options := optionsFromTarget(*testTarget, sema) + config, err := builder.NewConfig(&options) + if err != nil { + t.Fatal(err) + } + + output := &bytes.Buffer{} + _, err = buildAndRun("testdata/runtimegoroot.go", config, output, nil, test.environment, time.Minute, func(cmd *exec.Cmd, _ builder.BuildResult) error { + return cmd.Run() + }) + if err != nil { + t.Fatal(err) + } + want := test.want + "\n" + test.want + "\n" + if output.String() != want { + t.Fatalf("unexpected output:\n%s\nwant:\n%s", output.String(), want) + } + }) + } +} + +func TestRuntimeGODEBUG(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + program string + environment []string + want string + }{ + {"compiled default", "testdata/runtimegodebug.go", nil, "true \"\"\ntrue \"\"\ntrue \"\"\ntrue \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"\"\n"}, + {"environment override", "testdata/runtimegodebug.go", []string{"GODEBUG=tarinsecurepath=1"}, "true \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"\"\ntrue \"tarinsecurepath=1\"\ntrue \"\"\ntrue \"\"\n"}, + {"setting compiled default", "testdata/runtimegodebugsetting.go", nil, "true\nfalse\ntrue\ntrue\n"}, + {"setting environment override", "testdata/runtimegodebugsetting.go", []string{"GODEBUG=tarinsecurepath=1"}, "false\nfalse\ntrue\ntrue\n"}, + } { + t.Run(test.name, func(t *testing.T) { + options := optionsFromTarget(*testTarget, sema) + config, err := builder.NewConfig(&options) + if err != nil { + t.Fatal(err) + } + + output := &bytes.Buffer{} + _, err = buildAndRun(test.program, config, output, nil, test.environment, time.Minute, func(cmd *exec.Cmd, _ builder.BuildResult) error { + return cmd.Run() + }) + if err != nil { + t.Fatal(err) + } + if output.String() != test.want { + t.Fatalf("unexpected output:\n%s\nwant:\n%s", output.String(), test.want) + } + }) + } +} diff --git a/src/runtime/env.go b/src/runtime/env.go index e6c96f2417..e2b81010ed 100644 --- a/src/runtime/env.go +++ b/src/runtime/env.go @@ -10,11 +10,11 @@ func syscallSetenv(key, value string) { keydata := cstring(key) valdata := cstring(value) setenv(&keydata[0], &valdata[0]) - if key == "GODEBUG" && godebugUpdate != nil { + if key == "GODEBUG" && godebugEnvEnabled() { // Starting with Go 1.20, we need to call a callback (set by // internal/godebug) to notify the GODEBUG environment variable has // changed. This is necessary to get archive/zip to pass tests. - godebugUpdate(key, value) + godebugSetEnv(value) } } @@ -25,6 +25,9 @@ func syscallSetenv(key, value string) { func syscallUnsetenv(key string) { keydata := cstring(key) unsetenv(&keydata[0]) + if key == "GODEBUG" && godebugEnvEnabled() { + godebugUnsetEnv() + } } // Clear the environment. diff --git a/src/runtime/env_nonhosted.go b/src/runtime/env_nonhosted.go index e3bef9dbcc..e183d2331d 100644 --- a/src/runtime/env_nonhosted.go +++ b/src/runtime/env_nonhosted.go @@ -8,15 +8,15 @@ func syscallSetenv(key, value string) { for i, e := range env { if envKey(e) == key { env[i] = entry - if key == "GODEBUG" && godebugUpdate != nil { - godebugUpdate(key, value) + if key == "GODEBUG" && godebugEnvEnabled() { + godebugSetEnv(value) } return } } env = append(env, entry) - if key == "GODEBUG" && godebugUpdate != nil { - godebugUpdate(key, value) + if key == "GODEBUG" && godebugEnvEnabled() { + godebugSetEnv(value) } } @@ -25,12 +25,15 @@ func syscallUnsetenv(key string) { for i, e := range env { if envKey(e) == key { env = append(env[:i], env[i+1:]...) - if key == "GODEBUG" && godebugUpdate != nil { - godebugUpdate(key, "") + if key == "GODEBUG" && godebugEnvEnabled() { + godebugUnsetEnv() } return } } + if key == "GODEBUG" { + godebugUnsetEnv() + } } func envKey(entry string) string { diff --git a/src/runtime/env_startup_libc.go b/src/runtime/env_startup_libc.go new file mode 100644 index 0000000000..dad2c686c7 --- /dev/null +++ b/src/runtime/env_startup_libc.go @@ -0,0 +1,27 @@ +//go:build darwin || windows || wasip1 || (linux && !baremetal && !wasm_unknown && !wasip2 && !nintendoswitch) + +package runtime + +import "unsafe" + +//export getenv +func libc_getenv(name *byte) *byte + +func startupEnv(key string) string { + name := make([]byte, len(key)+1) + copy(name, key) + value := libc_getenv(&name[0]) + if value == nil { + return "" + } + length := strlen(unsafe.Pointer(value)) + return string(unsafe.Slice(value, length)) +} + +func init() { + if gorootEnvEnabled() { + if value := startupEnv("GOROOT"); value != "" { + goroot = value + } + } +} diff --git a/src/runtime/env_wasip2.go b/src/runtime/env_wasip2.go index ffc18d9253..9460845312 100644 --- a/src/runtime/env_wasip2.go +++ b/src/runtime/env_wasip2.go @@ -8,14 +8,14 @@ package runtime //go:linkname syscallSetenv syscall.runtimeSetenv func syscallSetenv(key, value string) { - if key == "GODEBUG" && godebugUpdate != nil { - godebugUpdate(key, value) + if key == "GODEBUG" && godebugEnvEnabled() { + godebugSetEnv(value) } } //go:linkname syscallUnsetenv syscall.runtimeUnsetenv func syscallUnsetenv(key string) { - if key == "GODEBUG" && godebugUpdate != nil { - godebugUpdate(key, "") + if key == "GODEBUG" && godebugEnvEnabled() { + godebugUnsetEnv() } } diff --git a/src/runtime/godebug_startup_libc.go b/src/runtime/godebug_startup_libc.go new file mode 100644 index 0000000000..f32eee26cd --- /dev/null +++ b/src/runtime/godebug_startup_libc.go @@ -0,0 +1,9 @@ +//go:build (linux && !baremetal && !wasm_unknown && !wasip2 && !nintendoswitch) || darwin || windows + +package runtime + +func init() { + if godebugEnvEnabled() { + godebugStartup = startupEnv("GODEBUG") + } +} diff --git a/src/runtime/nonhosted.go b/src/runtime/nonhosted.go index 9f01a7621a..640820cefb 100644 --- a/src/runtime/nonhosted.go +++ b/src/runtime/nonhosted.go @@ -54,3 +54,23 @@ func init() { env = append(env, s[start:]) } } + +func init() { + if gorootEnvEnabled() { + if value := startupEnv("GOROOT"); value != "" { + goroot = value + } + } + if godebugEnvEnabled() { + godebugStartup = startupEnv("GODEBUG") + } +} + +func startupEnv(key string) string { + for _, entry := range env { + if len(entry) > len(key) && entry[:len(key)] == key && entry[len(key)] == '=' { + return entry[len(key)+1:] + } + } + return "" +} diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index 85684dd039..3f184919f9 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -1,6 +1,7 @@ package runtime import ( + "sync/atomic" "unsafe" ) @@ -33,8 +34,9 @@ func GOMAXPROCS(n int) int { var goroot string // set by the builder +func gorootEnvEnabled() bool + func GOROOT() string { - // TODO: Use GOROOT from the startup environment when it is set. return goroot } @@ -146,13 +148,49 @@ func makeStrongFromWeak(ptr unsafe.Pointer) unsafe.Pointer { return ptr } -var godebugUpdate func(string, string) +var godebugDefault string // set by the builder +var godebugStartup string +var godebugEmpty string +var godebugEnv atomic.Pointer[string] +var godebugUpdate atomic.Pointer[func(string, string)] + +func godebugEnvEnabled() bool //go:linkname godebug_setUpdate internal/godebug.setUpdate func godebug_setUpdate(update func(string, string)) { + if !godebugEnvEnabled() { + return + } // The 'update' function needs to be called whenever the GODEBUG environment // variable changes (for example, via os.Setenv). - godebugUpdate = update + p := new(func(string, string)) + *p = update + godebugUpdate.Store(p) + godebugNotify() +} + +func godebugSetEnv(value string) { + p := new(string) + *p = value + godebugEnv.Store(p) + godebugNotify() +} + +func godebugUnsetEnv() { + godebugEnv.Store(&godebugEmpty) + godebugNotify() +} + +func godebugNotify() { + update := godebugUpdate.Load() + if update == nil { + return + } + env := godebugStartup + if p := godebugEnv.Load(); p != nil { + env = *p + } + (*update)(godebugDefault, env) } //go:linkname godebug_setNewIncNonDefault internal/godebug.setNewIncNonDefault diff --git a/src/runtime/runtime_wasip1.go b/src/runtime/runtime_wasip1.go index 275599dcde..b2ea819ae7 100644 --- a/src/runtime/runtime_wasip1.go +++ b/src/runtime/runtime_wasip1.go @@ -17,6 +17,9 @@ func __wasm_call_ctors() // wasmtime run ./program.wasm arg1 arg2 func init() { __wasm_call_ctors() + if godebugEnvEnabled() { + godebugStartup = startupEnv("GODEBUG") + } } var args []string diff --git a/src/runtime/runtime_wasip2.go b/src/runtime/runtime_wasip2.go index 5dfe4de62d..80b989c345 100644 --- a/src/runtime/runtime_wasip2.go +++ b/src/runtime/runtime_wasip2.go @@ -13,12 +13,30 @@ import ( ) func init() { + if gorootEnvEnabled() { + if value := startupEnv("GOROOT"); value != "" { + goroot = value + } + } + if godebugEnvEnabled() { + godebugStartup = startupEnv("GODEBUG") + } + wasiclirun.Exports.Run = func() cm.BoolResult { callMain() return false } } +func startupEnv(key string) string { + for _, entry := range environment.GetEnvironment().Slice() { + if entry[0] == key { + return entry[1] + } + } + return "" +} + var args []string //go:linkname os_runtime_args os.runtime_args diff --git a/src/syscall/env_wasip2.go b/src/syscall/env_wasip2.go index f9468936bb..1d9105290d 100644 --- a/src/syscall/env_wasip2.go +++ b/src/syscall/env_wasip2.go @@ -54,5 +54,9 @@ func Unsetenv(key string) (err error) { } func Clearenv() { + _, hadGODEBUG := libc_envs["GODEBUG"] clear(libc_envs) + if hadGODEBUG { + runtimeUnsetenv("GODEBUG") + } } diff --git a/testdata/runtimegodebug.go b/testdata/runtimegodebug.go new file mode 100644 index 0000000000..52d86dc367 --- /dev/null +++ b/testdata/runtimegodebug.go @@ -0,0 +1,41 @@ +//go:debug tarinsecurepath=0 + +package main + +import ( + "fmt" + "strings" + "syscall" + _ "unsafe" +) + +var defaults, environment string + +//go:linkname setUpdate internal/godebug.setUpdate +func setUpdate(update func(defaults, environment string)) + +func init() { + setUpdate(func(newDefaults, newEnvironment string) { + defaults = newDefaults + environment = newEnvironment + }) +} + +func main() { + printState() + syscall.Unsetenv("GODEBUG") + printState() + syscall.Setenv("GODEBUG", "") + printState() + syscall.Setenv("GODEBUG", "tarinsecurepath=1") + printState() + syscall.Unsetenv("GODEBUG") + printState() + syscall.Setenv("GODEBUG", "tarinsecurepath=1") + syscall.Clearenv() + printState() +} + +func printState() { + fmt.Printf("%t %q\n", strings.Contains(defaults, "tarinsecurepath=0"), environment) +} diff --git a/testdata/runtimegodebugsetting.go b/testdata/runtimegodebugsetting.go new file mode 100644 index 0000000000..1768144b48 --- /dev/null +++ b/testdata/runtimegodebugsetting.go @@ -0,0 +1,43 @@ +//go:debug tarinsecurepath=0 + +package main + +import ( + "archive/tar" + "bytes" + "fmt" + "syscall" +) + +func main() { + var data bytes.Buffer + writer := tar.NewWriter(&data) + if err := writer.WriteHeader(&tar.Header{Name: "../test"}); err != nil { + panic(err) + } + if err := writer.Close(); err != nil { + panic(err) + } + printState := func() { + _, err := tar.NewReader(bytes.NewReader(data.Bytes())).Next() + if err != nil && err != tar.ErrInsecurePath { + panic(err) + } + fmt.Println(err == tar.ErrInsecurePath) + } + + printState() + if err := syscall.Setenv("GODEBUG", "tarinsecurepath=1"); err != nil { + panic(err) + } + printState() + if err := syscall.Unsetenv("GODEBUG"); err != nil { + panic(err) + } + printState() + if err := syscall.Setenv("GODEBUG", "tarinsecurepath=1"); err != nil { + panic(err) + } + syscall.Clearenv() + printState() +} diff --git a/testdata/runtimegoroot.go b/testdata/runtimegoroot.go new file mode 100644 index 0000000000..d4dad9a4e9 --- /dev/null +++ b/testdata/runtimegoroot.go @@ -0,0 +1,13 @@ +package main + +import ( + "fmt" + "os" + "runtime" +) + +func main() { + fmt.Println(runtime.GOROOT()) + os.Setenv("GOROOT", "/changed") + fmt.Println(runtime.GOROOT()) +} diff --git a/transform/optimizer.go b/transform/optimizer.go index 282170cf2b..5dae94f908 100644 --- a/transform/optimizer.go +++ b/transform/optimizer.go @@ -68,7 +68,6 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error { if err != nil { return []error{fmt.Errorf("could not build pass pipeline: %w", err)} } - // Run TinyGo-specific optimization passes. OptimizeStringToBytes(mod) maxStackSize := config.MaxStackAlloc() @@ -210,6 +209,96 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error { return nil } +func pruneRuntimeStartup(mod llvm.Module) error { + options := llvm.NewPassBuilderOptions() + defer options.Dispose() + if pruneUnusedRuntimeStartup(mod) { + if err := mod.RunPasses("globaldce", llvm.TargetMachine{}, options); err != nil { + return fmt.Errorf("could not run post-startup-pruning globaldce pass: %w", err) + } + } + return nil +} + +// PrepareRuntimeStartup resolves startup checks before interpreting initializers. +func PrepareRuntimeStartup(mod llvm.Module) error { + linkages := make(map[llvm.Value]llvm.Linkage, len(functionsUsedInTransforms)) + for _, name := range functionsUsedInTransforms { + fn := mod.NamedFunction(name) + if !fn.IsNil() { + linkages[fn] = fn.Linkage() + fn.SetLinkage(llvm.ExternalLinkage) + } + } + defer func() { + for fn, linkage := range linkages { + fn.SetLinkage(linkage) + } + }() + options := llvm.NewPassBuilderOptions() + defer options.Dispose() + err := mod.RunPasses("globaldce,function-attrs,adce,globaldce", llvm.TargetMachine{}, options) + if err != nil { + return fmt.Errorf("could not prepare runtime startup: %w", err) + } + return pruneRuntimeStartup(mod) +} + +func pruneUnusedRuntimeStartup(mod llvm.Module) bool { + changed := lowerRuntimeStartupCheck(mod, + "runtime.gorootEnvEnabled", + "tinygo.runtime.feature.goroot", + "runtime.GOROOT") + return lowerRuntimeStartupCheck(mod, + "runtime.godebugEnvEnabled", + "tinygo.runtime.feature.godebug", + "(*internal/godebug.Setting).Value") || changed +} + +func lowerRuntimeStartupCheck(mod llvm.Module, checkName, markerName, featureName string) bool { + marker := mod.NamedFunction(markerName) + enabled := !marker.IsNil() && hasUses(marker) + feature := mod.NamedFunction(featureName) + enabled = enabled || (!feature.IsNil() && hasUses(feature)) + + check := mod.NamedFunction(checkName) + if check.IsNil() { + return false + } + + for _, use := range getUses(check) { + if use.IsACallInst().IsNil() || use.CalledValue() != check { + panic("unexpected use of runtime startup check") + } + use.ReplaceAllUsesWith(llvm.ConstInt(use.Type(), boolToUint64(enabled), false)) + use.EraseFromParentAsInstruction() + } + if hasUses(check) { + panic("runtime startup check still has uses") + } + check.EraseFromParentAsFunction() + if !marker.IsNil() { + for _, use := range getUses(marker) { + if use.IsACallInst().IsNil() || use.CalledValue() != marker { + panic("unexpected use of runtime feature marker") + } + use.EraseFromParentAsInstruction() + } + if hasUses(marker) { + panic("runtime feature marker still has uses") + } + marker.EraseFromParentAsFunction() + } + return true +} + +func boolToUint64(value bool) uint64 { + if value { + return 1 + } + return 0 +} + func blockGlobalAllocPromotion(mod llvm.Module) { ctx := mod.Context() ptrType := llvm.PointerType(ctx.Int8Type(), 0) diff --git a/transform/optimizer_internal_test.go b/transform/optimizer_internal_test.go index 3358706dd0..ae06ff935c 100644 --- a/transform/optimizer_internal_test.go +++ b/transform/optimizer_internal_test.go @@ -31,3 +31,60 @@ func TestBlockGlobalAllocPromotionUses(t *testing.T) { t.Fatalf("got %d marker uses, want 1", len(uses)) } } + +func TestPruneUnusedRuntimeStartup(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + wantEnabled bool + }{ + { + name: "unused", + path: "testdata/runtime-startup-unused.ll", + }, + { + name: "used", + path: "testdata/runtime-startup-used.ll", + wantEnabled: true, + }, + { + name: "referenced", + path: "testdata/runtime-startup-referenced.ll", + wantEnabled: true, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ctx := llvm.NewContext() + defer ctx.Dispose() + buf, err := llvm.NewMemoryBufferFromFile(tc.path) + if err != nil { + t.Fatal(err) + } + mod, err := ctx.ParseIR(buf) + if err != nil { + t.Fatal(err) + } + defer mod.Dispose() + + if err := PrepareRuntimeStartup(mod); err != nil { + t.Fatal(err) + } + for _, name := range []string{"runtime.gorootEnvEnabled", "runtime.godebugEnvEnabled"} { + if !mod.NamedFunction(name).IsNil() { + t.Errorf("%s was not removed", name) + } + } + for _, name := range []string{"test.goroot", "test.godebug"} { + ret := mod.NamedFunction(name).EntryBasicBlock().LastInstruction() + got := ret.Operand(0).ZExtValue() != 0 + if got != tc.wantEnabled { + t.Errorf("%s returned %v, want %v", name, got, tc.wantEnabled) + } + } + }) + } +} diff --git a/transform/testdata/runtime-startup-referenced.ll b/transform/testdata/runtime-startup-referenced.ll new file mode 100644 index 0000000000..1602a5eea2 --- /dev/null +++ b/transform/testdata/runtime-startup-referenced.ll @@ -0,0 +1,19 @@ +@goroot = global ptr @runtime.GOROOT +@godebug = global ptr @"(*internal/godebug.Setting).Value" + +declare i1 @runtime.gorootEnvEnabled(ptr) +declare i1 @runtime.godebugEnvEnabled(ptr) +declare ptr @runtime.GOROOT(ptr) +declare ptr @"(*internal/godebug.Setting).Value"(ptr, ptr) + +define i1 @test.goroot() { +entry: + %enabled = call i1 @runtime.gorootEnvEnabled(ptr undef) + ret i1 %enabled +} + +define i1 @test.godebug() { +entry: + %enabled = call i1 @runtime.godebugEnvEnabled(ptr undef) + ret i1 %enabled +} diff --git a/transform/testdata/runtime-startup-unused.ll b/transform/testdata/runtime-startup-unused.ll new file mode 100644 index 0000000000..0e3330e3a7 --- /dev/null +++ b/transform/testdata/runtime-startup-unused.ll @@ -0,0 +1,35 @@ +declare i1 @runtime.gorootEnvEnabled(ptr) +declare i1 @runtime.godebugEnvEnabled(ptr) +declare void @tinygo.runtime.feature.goroot() +declare void @tinygo.runtime.feature.godebug() +declare void @"internal/godebug.setUpdate"(ptr, ptr) +declare ptr @runtime.GOROOT(ptr) +declare ptr @"(*internal/godebug.Setting).Value"(ptr, ptr) + +define internal void @test.noop(ptr %callback) { +entry: + ret void +} + +define internal void @test.unusedCallback() { +entry: + call void @tinygo.runtime.feature.goroot() + %goroot = call ptr @runtime.GOROOT(ptr undef) + call void @tinygo.runtime.feature.godebug() + %godebug = call ptr @"(*internal/godebug.Setting).Value"(ptr null, ptr undef) + ret void +} + +define i1 @test.goroot() { +entry: + call void @test.noop(ptr @test.unusedCallback) + %enabled = call i1 @runtime.gorootEnvEnabled(ptr undef) + ret i1 %enabled +} + +define i1 @test.godebug() { +entry: + call void @"internal/godebug.setUpdate"(ptr null, ptr undef) + %enabled = call i1 @runtime.godebugEnvEnabled(ptr undef) + ret i1 %enabled +} diff --git a/transform/testdata/runtime-startup-used.ll b/transform/testdata/runtime-startup-used.ll new file mode 100644 index 0000000000..d605b9ab03 --- /dev/null +++ b/transform/testdata/runtime-startup-used.ll @@ -0,0 +1,18 @@ +declare i1 @runtime.gorootEnvEnabled(ptr) +declare i1 @runtime.godebugEnvEnabled(ptr) +declare void @tinygo.runtime.feature.goroot() +declare void @tinygo.runtime.feature.godebug() + +define i1 @test.goroot() { +entry: + call void @tinygo.runtime.feature.goroot() + %enabled = call i1 @runtime.gorootEnvEnabled(ptr undef) + ret i1 %enabled +} + +define i1 @test.godebug() { +entry: + call void @tinygo.runtime.feature.godebug() + %enabled = call i1 @runtime.godebugEnvEnabled(ptr undef) + ret i1 %enabled +}