diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index b04652658d..83e5202eb0 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -180,7 +180,7 @@ jobs: test: name: test (${{ matrix.lane }}, ${{ matrix.os }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go }}, shard ${{ matrix.shard }}) continue-on-error: ${{ matrix.lane == 'compatibility' }} - timeout-minutes: 30 + timeout-minutes: ${{ startsWith(matrix.os, 'macos') && 45 || 30 }} strategy: matrix: # Keep compatibility and primary toolchains pinned to exact patches. diff --git a/cl/_testlto/_globaldce_reflect_method_by_name_ltoplugin_string_abi_unknown/in.go b/cl/_testlto/_globaldce_reflect_method_by_name_ltoplugin_string_abi_unknown/in.go new file mode 100644 index 0000000000..55c305178c --- /dev/null +++ b/cl/_testlto/_globaldce_reflect_method_by_name_ltoplugin_string_abi_unknown/in.go @@ -0,0 +1,25 @@ +package main + +import ( + "os" + "reflect" +) + +type Unknown struct{} + +//go:noinline +func (Unknown) UnknownA() {} + +//go:noinline +func (Unknown) UnknownB() {} + +func unknownName() string { + if len(os.Args) == 0 { + return "UnknownA" + } + return os.Args[0] +} + +func main() { + _, _ = reflect.TypeOf(Unknown{}).MethodByName(unknownName()) +} diff --git a/cl/_testlto/defer_dwarf/expect.txt b/cl/_testlto/defer_dwarf/expect.txt new file mode 100644 index 0000000000..e64905e911 --- /dev/null +++ b/cl/_testlto/defer_dwarf/expect.txt @@ -0,0 +1 @@ +[2 1] diff --git a/cl/_testlto/defer_dwarf/in.go b/cl/_testlto/defer_dwarf/in.go new file mode 100644 index 0000000000..38c8e283b1 --- /dev/null +++ b/cl/_testlto/defer_dwarf/in.go @@ -0,0 +1,19 @@ +package main + +import "fmt" + +func record(values *[]int, value int) { + *values = append(*values, value) +} + +func deferredValues(addSecond bool) (values []int) { + defer record(&values, 1) + if addSecond { + defer record(&values, 2) + } + return +} + +func main() { + fmt.Println(deferredValues(true)) +} diff --git a/cl/_testlto/globaldce_reflect_method_by_name_ltoplugin_string_abi/expect.txt b/cl/_testlto/globaldce_reflect_method_by_name_ltoplugin_string_abi/expect.txt new file mode 100644 index 0000000000..53c3919d14 --- /dev/null +++ b/cl/_testlto/globaldce_reflect_method_by_name_ltoplugin_string_abi/expect.txt @@ -0,0 +1,4 @@ +direct +concat +slice +forward diff --git a/cl/_testlto/globaldce_reflect_method_by_name_ltoplugin_string_abi/in.go b/cl/_testlto/globaldce_reflect_method_by_name_ltoplugin_string_abi/in.go new file mode 100644 index 0000000000..a0121e93b7 --- /dev/null +++ b/cl/_testlto/globaldce_reflect_method_by_name_ltoplugin_string_abi/in.go @@ -0,0 +1,53 @@ +// LITTEST +package main + +import ( + "reflect" +) + +// SYMBOL-NOT: globaldce_reflect_method_by_name_ltoplugin_string_abi{{.*}}Known{{.*}}Drop +// SYMBOL-DAG: globaldce_reflect_method_by_name_ltoplugin_string_abi{{.*}}Known{{.*}}Direct +// SYMBOL-DAG: globaldce_reflect_method_by_name_ltoplugin_string_abi{{.*}}Known{{.*}}Concat +// SYMBOL-DAG: globaldce_reflect_method_by_name_ltoplugin_string_abi{{.*}}Known{{.*}}Slice +// SYMBOL-DAG: globaldce_reflect_method_by_name_ltoplugin_string_abi{{.*}}Known{{.*}}Forward +// SYMBOL-NOT: globaldce_reflect_method_by_name_ltoplugin_string_abi{{.*}}Known{{.*}}Drop + +type Known struct{} + +//go:noinline +func (Known) Direct() string { return "direct" } + +//go:noinline +func (Known) Concat() string { return "concat" } + +//go:noinline +func (Known) Slice() string { return "slice" } + +//go:noinline +func (Known) Forward() string { return "forward" } + +//go:noinline +func (Known) Drop() string { panic("unreachable") } + +func callForward(name string) string { + out := reflect.ValueOf(Known{}).MethodByName(name).Call(nil) + return out[0].String() +} + +func callConcat(prefix, suffix string) string { + out := reflect.ValueOf(Known{}).MethodByName(prefix + suffix).Call(nil) + return out[0].String() +} + +func callSlice(source string) string { + out := reflect.ValueOf(Known{}).MethodByName(source[2:7]).Call(nil) + return out[0].String() +} + +func main() { + v := reflect.ValueOf(Known{}) + println(v.MethodByName("Direct").Call(nil)[0].String()) + println(callConcat("Con", "cat")) + println(callSlice("__Slice__")) + println(callForward("Forward")) +} diff --git a/cl/cltest/cltest.go b/cl/cltest/cltest.go index 2f412d5477..cd4aeef09e 100644 --- a/cl/cltest/cltest.go +++ b/cl/cltest/cltest.go @@ -355,6 +355,11 @@ func withModuleCapture(conf *build.Config, pkgDir string) (*build.Config, *strin conf = build.NewDefaultConf(build.ModeRun) } localConf := *conf + // Existing IR fixtures describe executable instructions, not debug records. + // Keep their snapshots stable unless a test explicitly requests DWARF. + if localConf.LinkOptions.DWARF == build.DWARFDefault { + localConf.LinkOptions.DWARF = build.DWARFOmit + } var module string prevHook := localConf.ModuleHook localConf.ModuleHook = func(pkg build.Package) { diff --git a/cl/cltest/cltest_test.go b/cl/cltest/cltest_test.go index 636295b984..7d94e8e168 100644 --- a/cl/cltest/cltest_test.go +++ b/cl/cltest/cltest_test.go @@ -5,8 +5,34 @@ import ( "path/filepath" "runtime" "testing" + + "github.com/goplus/llgo/internal/build" ) +func TestWithModuleCaptureDWARFMode(t *testing.T) { + for _, test := range []struct { + name string + mode build.DWARFMode + want build.DWARFMode + }{ + {name: "default", mode: build.DWARFDefault, want: build.DWARFOmit}, + {name: "preserve", mode: build.DWARFPreserve, want: build.DWARFPreserve}, + {name: "omit", mode: build.DWARFOmit, want: build.DWARFOmit}, + } { + t.Run(test.name, func(t *testing.T) { + conf := build.NewDefaultConf(build.ModeRun) + conf.LinkOptions.DWARF = test.mode + got, _ := withModuleCapture(conf, t.TempDir()) + if got.LinkOptions.DWARF != test.want { + t.Fatalf("DWARF mode = %v, want %v", got.LinkOptions.DWARF, test.want) + } + if conf.LinkOptions.DWARF != test.mode { + t.Fatalf("input DWARF mode = %v, want %v", conf.LinkOptions.DWARF, test.mode) + } + }) + } +} + func TestReadGoldenUsesToolchainVersion(t *testing.T) { dir := t.TempDir() file := filepath.Join(dir, "expect.txt") diff --git a/cl/compile.go b/cl/compile.go index 6cb94df500..094c423a25 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -1618,10 +1618,7 @@ func (p *context) jumpTo(v *ssa.Jump) llssa.BasicBlock { } func (p *context) getDebugLocScope(v *ssa.Function, pos token.Pos) *types.Scope { - if v.Object() == nil { - return nil - } - funcScope := v.Object().(*types.Func).Scope() + funcScope := debugFunctionScope(v) if funcScope == nil { return nil } diff --git a/cl/compile_test.go b/cl/compile_test.go index 108cbb7353..32429171b7 100644 --- a/cl/compile_test.go +++ b/cl/compile_test.go @@ -21,6 +21,8 @@ package cl_test import ( "os" + "os/exec" + "path/filepath" "runtime" "strings" "testing" @@ -28,8 +30,10 @@ import ( "github.com/goplus/llgo/cl/cltest" "github.com/goplus/llgo/internal/build" "github.com/goplus/llgo/internal/buildenv" + "github.com/goplus/llgo/internal/cabi" "github.com/goplus/llgo/internal/llgen" "github.com/goplus/llgo/internal/lto" + llvmenv "github.com/goplus/llgo/xtool/env/llvm" ) func testCompile(t *testing.T, src, expected string) { @@ -187,6 +191,7 @@ func TestRunAndTestFromTestlto(t *testing.T) { "./_testlto/globaldce_reflect_method_by_name_ltoplugin_param", "./_testlto/globaldce_reflect_method_by_name_ltoplugin_range_literal", "./_testlto/globaldce_reflect_method_by_name_ltoplugin_slice", + "./_testlto/globaldce_reflect_method_by_name_ltoplugin_string_abi", "./_testlto/globaldce_reflect_method_by_name_ltoplugin_switch", } if !buildenv.Dev { @@ -206,6 +211,24 @@ func TestRunAndTestFromTestlto(t *testing.T) { cltest.RunAndTestFromDir(t, "", "./_testlto", ignore, cltest.WithRunConfig(conf)) } +func runTestltoDWARF(t *testing.T, test string) { + t.Helper() + t.Setenv("LLGO_BUILD_CACHE", "off") + conf := build.NewDefaultConf(build.ModeRun) + conf.LTO = lto.Full + conf.LinkOptions.DWARF = build.DWARFPreserve + cltest.RunAndTestFromDir(t, test, "./_testlto", nil, + cltest.WithRunConfig(conf), cltest.WithIRCheck(false)) +} + +func TestRunAndTestFromTestltoDWARF(t *testing.T) { + runTestltoDWARF(t, "reflectmk_runtime") +} + +func TestRunAndTestFromTestltoDeferDWARF(t *testing.T) { + runTestltoDWARF(t, "defer_dwarf") +} + var testltoSymbolChecks = []string{ "globaldce_interface_matrix", "globaldce_interface_slots", @@ -225,9 +248,17 @@ var testltoLTOPluginTests = []string{ "globaldce_reflect_method_by_name_ltoplugin_param", "globaldce_reflect_method_by_name_ltoplugin_range_literal", "globaldce_reflect_method_by_name_ltoplugin_slice", + "globaldce_reflect_method_by_name_ltoplugin_string_abi", "globaldce_reflect_method_by_name_ltoplugin_switch", } +var testltoLTOPluginDWARFTests = []string{ + "globaldce_reflect_method_by_name_ltoplugin_concat", + "globaldce_reflect_method_by_name_ltoplugin_global_slice", + "globaldce_reflect_method_by_name_ltoplugin_param", + "globaldce_reflect_method_by_name_ltoplugin_slice", +} + func TestBuildAndCheckSymbolsFromTestlto(t *testing.T) { if !buildenv.Dev { t.Skip("globaldce symbol checks require dev build") @@ -260,6 +291,14 @@ func TestRunAndTestFromTestltoLTOPlugin(t *testing.T) { ) } +func TestRunAndTestFromTestltoLTOPluginDWARF(t *testing.T) { + t.Setenv("LLGO_BUILD_CACHE", "off") + conf := testltoLTOPluginConf(t, build.ModeRun) + conf.LinkOptions.DWARF = build.DWARFPreserve + cltest.RunAndTestFromDir(t, "ltoplugin_switch", "./_testlto", nil, + cltest.WithRunConfig(conf), cltest.WithIRCheck(false)) +} + func TestBuildAndCheckSymbolsFromTestltoLTOPlugin(t *testing.T) { buildConf := testltoLTOPluginConf(t, build.ModeBuild) cltest.BuildAndCheckSymbolsFromDir(t, "", "./_testlto", testltoLTOPluginTests, @@ -267,6 +306,69 @@ func TestBuildAndCheckSymbolsFromTestltoLTOPlugin(t *testing.T) { ) } +func runTestltoLTOPluginAggregateABI(t *testing.T, fixture string) string { + t.Helper() + conf := testltoLTOPluginConf(t, build.ModeGen) + // Apply a fixed LP64 ABI below so the aggregate form is tested on every host. + conf.AbiMode = cabi.ModeNone + plugin := conf.LTOPlugin.Path + pkgs, err := build.Do([]string{fixture}, conf) + if err != nil { + t.Fatalf("generate aggregate string module: %v", err) + } + if len(pkgs) != 1 { + t.Fatalf("generate aggregate string module: got %d packages", len(pkgs)) + } + cabi.NewTransformer(pkgs[0].LPkg.Prog, "arm64-unknown-linux", "", cabi.ModeAllFunc, true). + TransformModule(pkgs[0].PkgPath, pkgs[0].LPkg.Module()) + aggregateIR := pkgs[0].LPkg.String() + pkgs[0].LPkg.Prog.Dispose() + if !strings.Contains(aggregateIR, `runtime.String" "llgo.reflect.methodbyname.name"`) { + t.Fatalf("MethodByName string argument was not captured in aggregate form:\n%s", aggregateIR) + } + + opt := filepath.Join(llvmenv.New("").BinDir(), "opt") + cmd := exec.Command(opt, "-load-pass-plugin="+plugin, + "-passes=llgo-lto-pre-globaldce", "-S", "-o", "-") + cmd.Stdin = strings.NewReader(aggregateIR) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("run LTO plugin for aggregate ABI: %v\n%s", err, out) + } + return string(out) +} + +func TestBuildAndCheckSymbolsFromTestltoLTOPluginAggregateABI(t *testing.T) { + result := runTestltoLTOPluginAggregateABI(t, + "./_testlto/globaldce_reflect_method_by_name_ltoplugin_string_abi") + for _, name := range []string{"Direct", "Concat", "Slice", "Forward"} { + marker := `metadata !"go.method.value.reflect.` + name + `"` + if !strings.Contains(result, marker) { + t.Fatalf("aggregate ABI output missing %s\n%s", marker, result) + } + } + if strings.Contains(result, `metadata !"go.method.value.reflect"`) { + t.Fatalf("aggregate ABI output retained the generic value marker\n%s", result) + } + + // A generic marker conservatively retains every matching method, so test + // unknown-name fallback in a separate module from the Drop symbol check. + unknownResult := runTestltoLTOPluginAggregateABI(t, + "./_testlto/_globaldce_reflect_method_by_name_ltoplugin_string_abi_unknown") + if !strings.Contains(unknownResult, `metadata !"go.method.type.reflect"`) { + t.Fatalf("aggregate ABI output lost the unknown-name type marker\n%s", unknownResult) + } +} + +func TestBuildAndCheckSymbolsFromTestltoLTOPluginDWARF(t *testing.T) { + t.Setenv("LLGO_BUILD_CACHE", "off") + buildConf := testltoLTOPluginConf(t, build.ModeBuild) + buildConf.LinkOptions.DWARF = build.DWARFPreserve + cltest.BuildAndCheckSymbolsFromDir(t, "", "./_testlto", testltoLTOPluginDWARFTests, + cltest.WithRunConfig(buildConf), + ) +} + func TestFilterEmulatorOutput(t *testing.T) { tests := []struct { name string @@ -412,6 +514,13 @@ func TestRunAndTestFromTestpy(t *testing.T) { cltest.RunAndTestFromDir(t, "", "./_testpy", nil) } +func TestRunAndTestFromTestpyDWARF(t *testing.T) { + conf := build.NewDefaultConf(build.ModeRun) + conf.LinkOptions.DWARF = build.DWARFPreserve + cltest.RunAndTestFromDir(t, "", "./_testpy", nil, + cltest.WithRunConfig(conf), cltest.WithIRCheck(false)) +} + func TestRunAndTestFromTestlibgo(t *testing.T) { cltest.RunAndTestFromDir(t, "", "./_testlibgo", nil) } diff --git a/cl/debug_compile_test.go b/cl/debug_compile_test.go index 2f177005b2..40987bf41f 100644 --- a/cl/debug_compile_test.go +++ b/cl/debug_compile_test.go @@ -19,26 +19,8 @@ import ( "golang.org/x/tools/go/ssa/ssautil" ) -func TestCompileDebugMetadata(t *testing.T) { - const source = `package debugcompile - -type item struct { value int } - -func inspect(items [2]item, seed int) int { - x := seed + 1 - var local [1]item - if x > 0 { - items[0].value = x - local[0].value = x - } - return items[0].value + local[0].value -} - -var anonymous = func(seed int) int { - value := seed + 1 - return value -} -` +func compileDebugSource(t *testing.T, source string, level optlevel.Level) llssa.Package { + t.Helper() fset := token.NewFileSet() file, err := parser.ParseFile(fset, "debug_compile.go", source, 0) if err != nil { @@ -58,21 +40,45 @@ var anonymous = func(seed int) int { oldDebug, oldDebugSyms := enableDbg, enableDbgSyms EnableDebug(true) EnableDbgSyms(true) - defer func() { + t.Cleanup(func() { EnableDebug(oldDebug) EnableDbgSyms(oldDebugSyms) - }() + }) prog := newLLSSAProgForTarget(t, &llssa.Target{ GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, - OptLevel: optlevel.O0, + OptLevel: level, }) - defer prog.Dispose() + t.Cleanup(prog.Dispose) pkg, err := NewPackage(prog, ssaPkg, []*ast.File{file}) if err != nil { t.Fatal(err) } + return pkg +} + +func TestCompileDebugMetadata(t *testing.T) { + const source = `package debugcompile + +type item struct { value int } + +func inspect(items [2]item, seed int) int { + x := seed + 1 + var local [1]item + if x > 0 { + items[0].value = x + local[0].value = x + } + return items[0].value + local[0].value +} + +var anonymous = func(seed int) int { + value := seed + 1 + return value +} +` + pkg := compileDebugSource(t, source, optlevel.O0) if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { t.Fatalf("debug module is invalid: %v\n%s", err, pkg.Module().String()) } @@ -93,3 +99,27 @@ var anonymous = func(seed int) int { } } } + +func TestCompileNestedRangeFuncDeferDebugMetadata(t *testing.T) { + const source = `package debugcompile + +func outer(yield func(int) bool) { _ = yield(1) } + +func inner(base int) func(func(int) bool) { + return func(yield func(int) bool) { _ = yield(base + 10) } +} + +func f() { + for i := range outer { + defer func() { _ = i }() + for j := range inner(i) { + defer func(v int) { _ = v }(j) + } + } +} +` + pkg := compileDebugSource(t, source, optlevel.O2) + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatalf("nested range defer debug module is invalid: %v\n%s", err, pkg.Module().String()) + } +} diff --git a/cl/debug_scope_test.go b/cl/debug_scope_test.go index 6b2b854c3a..6bbf937abc 100644 --- a/cl/debug_scope_test.go +++ b/cl/debug_scope_test.go @@ -73,4 +73,17 @@ var anonymous = func() int { if root.Pos() < anonymous.Syntax().Pos() || root.End() > anonymous.Syntax().End() { t.Fatalf("anonymous function scope %s is outside function %s", root, anonymous.Syntax()) } + lit, ok := anonymous.Syntax().(*ast.FuncLit) + if !ok || len(lit.Body.List) == 0 { + t.Fatal("anonymous function syntax not found") + } + ifStmt, ok := lit.Body.List[0].(*ast.IfStmt) + if !ok || len(ifStmt.Body.List) == 0 { + t.Fatal("anonymous function inner scope not found") + } + sourcePos := ifStmt.Body.List[0].Pos() + want := root.Innermost(sourcePos) + if got := (&context{}).getDebugLocScope(anonymous, sourcePos); got != want || got == nil { + t.Fatalf("anonymous instruction scope = %p, want %p", got, want) + } } diff --git a/cl/instr.go b/cl/instr.go index ddbcb58806..09268b61bd 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -1590,7 +1590,7 @@ func (p *context) emitPCLineLabel(b llssa.Builder, pos token.Pos) { pushSection = ".pushsection __DATA,__llgo_pcl,regular,live_support" recordSymbol = "l_llgo_pcline_rec_${:uid}:\n" } - b.InlineAsm( + b.InlineAsmNoDebug( asmLabel + ":\n" + pushSection + "\n" + ".p2align " + align + "\n" + diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index 325f062078..ae09d45065 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -26,6 +26,10 @@ func init() { } func compileWithRewrites(t *testing.T, src string, rewrites map[string]string) string { + return compileWithRewritesMode(t, src, rewrites, ssa.SanityCheckFunctions|ssa.InstantiateGenerics) +} + +func compileWithRewritesMode(t *testing.T, src string, rewrites map[string]string, mode ssa.BuilderMode) string { t.Helper() fset := token.NewFileSet() file, err := parser.ParseFile(fset, "rewrite.go", src, parser.ParseComments) @@ -33,7 +37,6 @@ func compileWithRewrites(t *testing.T, src string, rewrites map[string]string) s t.Fatalf("parse failed: %v", err) } importer := gpackages.NewImporter(fset) - mode := ssa.SanityCheckFunctions | ssa.InstantiateGenerics pkg, _, err := ssautil.BuildPackage(&types.Config{Importer: importer}, fset, types.NewPackage(file.Name.Name, file.Name.Name), []*ast.File{file}, mode) if err != nil { @@ -154,6 +157,75 @@ func Use() callbackType { return CallbackTypes[1] } } } +func TestStaticGlobalSliceLiteralInitWithDebugRefs(t *testing.T) { + const src = `package staticinit + +var CallbackTypes = []string{"BeforeCreate", "AfterCreate"} + +func Use() string { return CallbackTypes[1] } +` + ir := compileWithRewritesMode(t, src, nil, + ssa.SanityCheckFunctions|ssa.InstantiateGenerics|ssa.GlobalDebug) + for _, want := range []string{ + `@"staticinit.CallbackTypes$data" = global [2 x %"github.com/goplus/llgo/runtime/internal/runtime.String"]`, + `@staticinit.CallbackTypes = global %"github.com/goplus/llgo/runtime/internal/runtime.Slice" { ptr @"staticinit.CallbackTypes$data", i64 2, i64 2 }`, + `c"BeforeCreate"`, + `c"AfterCreate"`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("missing static slice initializer %q with debug refs:\n%s", want, ir) + } + } + assertNoStoreToGlobal(t, ir, "@staticinit.CallbackTypes") + if strings.Contains(ir, "runtime.AllocZ") { + t.Fatalf("static slice initializer allocates at runtime with debug refs:\n%s", ir) + } +} + +func TestStaticSliceInitRejectsExecutableReferrers(t *testing.T) { + const src = `package foo + +var Values []int + +func useSlice([]int) {} +func usePointer(*int) {} + +func sliceUser() { + backing := [2]int{1, 2} + values := backing[:] + Values = values + useSlice(values) +} + +func elementUser() { + var backing [2]int + elem := &backing[0] + *elem = 1 + usePointer(elem) + Values = backing[:] +} +` + ssapkg := buildSSAPackage(t, src) + global := ssapkg.Members["Values"].(*ssa.Global) + for _, name := range []string{"sliceUser", "elementUser"} { + fn := ssapkg.Func(name) + var globalStore *ssa.Store + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + if store, ok := instr.(*ssa.Store); ok && store.Addr == global { + globalStore = store + } + } + } + if globalStore == nil { + t.Fatalf("%s: store to Values not found", name) + } + if _, ok := staticSliceInitOf(globalStore); ok { + t.Fatalf("%s: static slice init accepted an executable referrer", name) + } + } +} + func TestStaticGlobalZeroSizedSliceLiteralFallsBack(t *testing.T) { const src = `package staticinit diff --git a/cl/static_init.go b/cl/static_init.go index bbbe94cfb4..7877f591e6 100644 --- a/cl/static_init.go +++ b/cl/static_init.go @@ -204,16 +204,16 @@ func staticSliceInitOf(store *ssa.Store) (*staticSliceInit, bool) { values: make(map[int]*ssa.Const), instrs: []ssa.Instruction{alloc, slice, store}, } - sliceRefs := slice.Referrers() - if sliceRefs == nil || len(*sliceRefs) != 1 || (*sliceRefs)[0] != store { + sliceRefs, ok := nonDebugReferrers(slice) + if !ok || len(sliceRefs) != 1 || sliceRefs[0] != store { return nil, false } - refs := alloc.Referrers() - if refs == nil { + refs, ok := nonDebugReferrers(alloc) + if !ok { return nil, false } seenSlice := false - for _, ref := range *refs { + for _, ref := range refs { switch ref := ref.(type) { case *ssa.Slice: if ref != slice || seenSlice { @@ -228,11 +228,11 @@ func staticSliceInitOf(store *ssa.Store) (*staticSliceInit, bool) { if !ok || index >= int(array.Len()) { return nil, false } - indexRefs := ref.Referrers() - if indexRefs == nil || len(*indexRefs) != 1 { + indexRefs, ok := nonDebugReferrers(ref) + if !ok || len(indexRefs) != 1 { return nil, false } - elemStore, ok := (*indexRefs)[0].(*ssa.Store) + elemStore, ok := indexRefs[0].(*ssa.Store) if !ok || elemStore.Addr != ref { return nil, false } diff --git a/internal/build/build.go b/internal/build/build.go index 2de1f96536..cd0ae715af 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -168,10 +168,7 @@ type Config struct { // linker semantics into typed Config fields before calling Do. GoBuildFlags []string LinkOptions LinkOptions - // OmitDWARFByDefault controls linked builds only when -w was not - // explicitly specified. Explicit -w and -w=false always win. - OmitDWARFByDefault bool - PCLNMode PCLNMode + PCLNMode PCLNMode // PCLNModeSet marks PCLNMode as authoritative. Command flags set it for // explicit requests; Do sets it after resolving the legacy environment // default. @@ -217,14 +214,13 @@ func NewDefaultConf(mode Mode) *Config { goarch = runtime.GOARCH } conf := &Config{ - Goos: goos, - Goarch: goarch, - BinPath: bin, - Mode: mode, - BuildMode: BuildModeExe, - AbiMode: cabi.ModeAllFunc, - OmitDWARFByDefault: mode != ModeGen, - PCLNMode: PCLNEmbedded, + Goos: goos, + Goarch: goarch, + BinPath: bin, + Mode: mode, + BuildMode: BuildModeExe, + AbiMode: cabi.ModeAllFunc, + PCLNMode: PCLNEmbedded, } return conf } @@ -381,12 +377,10 @@ func Do(args []string, conf *Config) ([]Package, error) { prog.EnableLTOPluginMarkers(conf.LTOPlugin.Enabled()) funcInfo := conf.Mode != ModeGen && conf.PCLNMode != PCLNNone prog.EnableFuncInfoMetadata(funcInfo) - // Site records are inline-asm fragments inside function bodies. Darwin - // DWARF builds avoid them because they disturb LLDB lexical scopes; Linux - // still needs them because its restricted dynamic symbol table cannot - // reconstruct every Go entry PC through dlsym. External mode always needs - // final-PC sites for sidecar construction. - prog.EnableFuncInfoSites(shouldEnablePCLNSites(conf, funcInfo, emitDebugInfo)) + // PC-line sites stay enabled with DWARF so runtime source locations remain + // precise. The link table emitter separately suppresses Darwin address sites + // when their entry-block assembly would disturb LLDB lexical scopes. + prog.EnableFuncInfoSites(shouldEnablePCLNSites(conf, funcInfo)) sizes := func(sizes types.Sizes, compiler, arch string) types.Sizes { if arch == "wasm" { sizes = &types.StdSizes{WordSize: 4, MaxAlign: 4} @@ -466,10 +460,7 @@ func Do(args []string, conf *Config) ([]Package, error) { buildMode := ssaBuildMode cabiOptimize := true - passOpt := true - if emitDebugInfo || mode == ModeGen { - passOpt = false - } + passOpt := shouldRunLLVMPasses(mode) if emitDebugInfo { buildMode |= ssa.GlobalDebug cabiOptimize = false @@ -477,6 +468,7 @@ func Do(args []string, conf *Config) ([]Package, error) { if !IsOptimizeEnabled() { buildMode |= ssa.NaiveForm } + prog.SetDebugInfoOptimized(passOpt && conf.OptLevel != optlevel.O0) progSSA := ssa.NewProgram(initial[0].Fset, buildMode) patches := make(cl.Patches, len(altPkgPaths)) altSSAPkgs(progSSA, patches, altPkgs[1:], conf, verbose) @@ -1101,7 +1093,7 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { // internal/pclnpost and doc/design/pclntab-linkphase.md). Any failure leaves // the binary fully functional on the first-use construction fallback. func rewritePrebuiltFuncTab(ctx *context, out string, verbose bool) { - if ctx == nil || ctx.prog == nil || !ctx.prog.FuncInfoSitesEnabled() || !shouldEmitRuntimeSites(ctx) { + if !shouldEmitRuntimeAddressSites(ctx) { return } if ctx.buildConf.BuildMode != BuildModeExe { @@ -2240,6 +2232,10 @@ func llvmPassPipeline(level optlevel.Level, ltoMode lto.Mode) string { } } +func shouldRunLLVMPasses(mode Mode) bool { + return mode != ModeGen +} + func IsWasiThreadsEnabled() bool { return isEnvOn(llgoWasiThreads, true) } diff --git a/internal/build/build_test.go b/internal/build/build_test.go index c26241cc20..eff5615936 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -304,8 +304,10 @@ func TestLinkOptionsControlELFDWARF(t *testing.T) { options LinkOptions wantDWARF bool }{ + {name: "default", wantDWARF: true}, {name: "omit", options: LinkOptions{DWARF: DWARFOmit}}, - {name: "preserve", options: LinkOptions{DWARF: DWARFPreserve}, wantDWARF: true}, + {name: "s", options: LinkOptions{OmitSymbolTable: true}}, + {name: "s_w_false", options: LinkOptions{OmitSymbolTable: true, DWARF: DWARFPreserve}, wantDWARF: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -331,8 +333,10 @@ func TestLinkOptionsControlDarwinDebugSymbols(t *testing.T) { options LinkOptions wantSTABS bool }{ + {name: "default", wantSTABS: true}, {name: "omit", options: LinkOptions{DWARF: DWARFOmit}}, - {name: "preserve", options: LinkOptions{DWARF: DWARFPreserve}, wantSTABS: true}, + {name: "s", options: LinkOptions{OmitSymbolTable: true}}, + {name: "s_w_false", options: LinkOptions{OmitSymbolTable: true, DWARF: DWARFPreserve}, wantSTABS: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/build/funcinfo_table.go b/internal/build/funcinfo_table.go index 30089f1c7a..79f36e7109 100644 --- a/internal/build/funcinfo_table.go +++ b/internal/build/funcinfo_table.go @@ -481,7 +481,7 @@ func emitFuncInfoTable(ctx *context, pkg llssa.Package, records []funcInfoRecord llvm.ConstInt(countType, 0, false), })) pcLineCount.SetInitializer(llvm.ConstInt(countType, uint64(len(encoded.PCLines)), false)) - if shouldEmitRuntimeSites(ctx) { + if shouldEmitRuntimePCLineSites(ctx) { startName, endName := pcLineSiteSectionInfo.boundary(shouldEmitRuntimeMachOSites(ctx)) pcSiteStart := llvm.AddGlobal(mod, pcSiteRecordType, startName) pcSiteEnd := llvm.AddGlobal(mod, pcSiteRecordType, endName) @@ -493,9 +493,10 @@ func emitFuncInfoTable(ctx *context, pkg llssa.Package, records []funcInfoRecord } } machOSites := shouldEmitRuntimeMachOSites(ctx) - emitSites := shouldEmitRuntimeSites(ctx) - emitEntrySites := shouldEmitRuntimeEntryELFSites(ctx) && len(encoded.Records) != 0 - emitStubSites := shouldEmitRuntimeStubELFSites(ctx) + emitSites := shouldEmitRuntimePCLineSites(ctx) + emitAddressSites := shouldEmitRuntimeAddressSites(ctx) + emitEntrySites := emitAddressSites && len(encoded.Records) != 0 + emitStubSites := emitAddressSites emitRuntimeFuncInfoSites(mod, ctx.prog.PointerSize(), machOSites, emitSites && len(pcLineValues) != 0, emitEntrySites, emitStubSites && len(stubRecords) != 0) if emitEntrySites { startName, endName := entrySiteSectionInfo.boundary(machOSites) @@ -712,10 +713,11 @@ func emitExternalFuncInfoTable(ctx *context, mod llvm.Module, records []funcInfo used.SetSection("llvm.metadata") machO := shouldEmitRuntimeMachOSites(ctx) - emitSites := shouldEmitRuntimeSites(ctx) + emitSites := shouldEmitRuntimePCLineSites(ctx) + emitAddressSites := shouldEmitRuntimeAddressSites(ctx) emitPCSites := emitSites && len(encoded.PCLines) != 0 - emitEntrySites := shouldEmitRuntimeEntryELFSites(ctx) && len(encoded.Records) != 0 - emitStubSites := shouldEmitRuntimeStubELFSites(ctx) && len(stubRecords) != 0 + emitEntrySites := emitAddressSites && len(encoded.Records) != 0 + emitStubSites := emitAddressSites && len(stubRecords) != 0 emitRuntimeFuncInfoSites(mod, ctx.prog.PointerSize(), machO, emitPCSites, emitEntrySites, emitStubSites) if emitPCSites { start, end := pcLineSiteSectionInfo.boundary(machO) @@ -754,29 +756,34 @@ func shouldEmitRuntimeMachOSites(ctx *context) bool { ctx.buildConf.Target == "" } -// shouldEmitRuntimeSites reports whether the target object format has a +// shouldEmitRuntimePCLineSites reports whether the target object format has a // DCE-safe section story for metadata site records. ELF uses SHF_LINK_ORDER // associated sections (honored by --gc-sections). The ELF sections are also // writable because shared libraries need dynamic relative relocations for the // absolute pointers stored in each record. Mach-O uses live_support sections: // under ld64/lld -dead_strip a live_support atom survives only if the atom it // references (the anchor inside the function body) is live, which is the same -// records-follow-function semantics. Sites are additionally gated per Program: -// debug builds keep the funcinfo tables but drop the body-embedded site records -// (see Program.EnableFuncInfoSites). -func shouldEmitRuntimeSites(ctx *context) bool { +// records-follow-function semantics. Sites are additionally gated per Program. +func shouldEmitRuntimePCLineSites(ctx *context) bool { if ctx == nil || ctx.prog == nil || !ctx.prog.FuncInfoSitesEnabled() { return false } return shouldEmitRuntimeELFSites(ctx) || shouldEmitRuntimeMachOSites(ctx) } -func shouldEmitRuntimeStubELFSites(ctx *context) bool { - return shouldEmitRuntimeSites(ctx) -} - -func shouldEmitRuntimeEntryELFSites(ctx *context) bool { - return shouldEmitRuntimeSites(ctx) +// shouldEmitRuntimeAddressSites controls function-entry and closure-stub +// address records. Their entry-block inline assembly changes Darwin's initial +// line row and makes LLDB expose inner lexical scopes too early. PC-line sites +// do not have that problem when emitted without a debug location, so embedded +// Darwin DWARF builds suppress only address sites. External PCLN still needs +// final address records to construct its sidecar. +func shouldEmitRuntimeAddressSites(ctx *context) bool { + if !shouldEmitRuntimePCLineSites(ctx) { + return false + } + conf := ctx.buildConf + return conf.Goos != "darwin" || conf.PCLNMode == PCLNExternal || + !shouldEmitDebugInfo(conf, &ctx.crossCompile) } // siteSectionInfo names one metadata site section in both object formats. @@ -851,7 +858,7 @@ func siteAnchorLabel(machO bool, kind string) string { } func emitFuncInfoEntrySites(ctx *context, pkg llssa.Package) { - if !shouldEmitRuntimeEntryELFSites(ctx) || pkg == nil || !ctx.prog.FuncInfoMetadataEnabled() { + if !shouldEmitRuntimeAddressSites(ctx) || pkg == nil || !ctx.prog.FuncInfoMetadataEnabled() { return } mod := pkg.Module() @@ -931,7 +938,7 @@ func emitFuncInfoEntrySites(ctx *context, pkg llssa.Package) { } func emitFuncInfoStubSites(ctx *context, pkg llssa.Package) { - if !shouldEmitRuntimeStubELFSites(ctx) || pkg == nil || !ctx.prog.FuncInfoMetadataEnabled() { + if !shouldEmitRuntimeAddressSites(ctx) || pkg == nil || !ctx.prog.FuncInfoMetadataEnabled() { return } machO := shouldEmitRuntimeMachOSites(ctx) diff --git a/internal/build/funcinfo_table_test.go b/internal/build/funcinfo_table_test.go index 15de1a7682..551cc648bd 100644 --- a/internal/build/funcinfo_table_test.go +++ b/internal/build/funcinfo_table_test.go @@ -499,6 +499,74 @@ func TestFuncInfoTableIgnoresInvalidMetadata(t *testing.T) { } } +func TestRuntimeSitePolicy(t *testing.T) { + tests := []struct { + name string + conf Config + enableSites bool + wantPCLine bool + wantAddress bool + }{ + { + name: "linux embedded dwarf", + conf: Config{Goos: "linux", PCLNMode: PCLNEmbedded}, + enableSites: true, + wantPCLine: true, + wantAddress: true, + }, + { + name: "darwin embedded dwarf", + conf: Config{Goos: "darwin", PCLNMode: PCLNEmbedded}, + enableSites: true, + wantPCLine: true, + }, + { + name: "darwin embedded without dwarf", + conf: Config{ + Goos: "darwin", + PCLNMode: PCLNEmbedded, + LinkOptions: LinkOptions{DWARF: DWARFOmit}, + }, + enableSites: true, + wantPCLine: true, + wantAddress: true, + }, + { + name: "darwin external dwarf", + conf: Config{Goos: "darwin", PCLNMode: PCLNExternal}, + enableSites: true, + wantPCLine: true, + wantAddress: true, + }, + { + name: "fixed target", + conf: Config{Goos: "darwin", Target: "rp2040", PCLNMode: PCLNEmbedded}, + enableSites: true, + }, + { + name: "program sites disabled", + conf: Config{Goos: "linux", PCLNMode: PCLNEmbedded}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prog := llssa.NewProgram(nil) + defer prog.Dispose() + prog.EnableFuncInfoSites(tt.enableSites) + ctx := &context{prog: prog, buildConf: &tt.conf} + if got := shouldEmitRuntimePCLineSites(ctx); got != tt.wantPCLine { + t.Fatalf("shouldEmitRuntimePCLineSites() = %v, want %v", got, tt.wantPCLine) + } + if got := shouldEmitRuntimeAddressSites(ctx); got != tt.wantAddress { + t.Fatalf("shouldEmitRuntimeAddressSites() = %v, want %v", got, tt.wantAddress) + } + }) + } + if shouldEmitRuntimePCLineSites(nil) || shouldEmitRuntimeAddressSites(nil) { + t.Fatal("nil context enabled runtime sites") + } +} + // TestFuncInfoTableEmissionMatrix sweeps the OS / pointer-size / content // combinations so both the ELF and Mach-O directive branches, the 32-bit // pointer directives, and the empty-table initializers stay covered on every @@ -539,6 +607,9 @@ func TestFuncInfoTableEmissionMatrix(t *testing.T) { BuildMode: BuildModeExe, Goos: c.goos, Goarch: c.goarch, + LinkOptions: LinkOptions{ + DWARF: DWARFOmit, + }, }, } records := collectFuncInfo([]Package{{LPkg: src}}) diff --git a/internal/build/link_options.go b/internal/build/link_options.go index 1a7c0140d8..050dcae053 100644 --- a/internal/build/link_options.go +++ b/internal/build/link_options.go @@ -66,10 +66,12 @@ func (o LinkOptions) EffectiveOmitDWARF() bool { } } -// omitDWARFRequested combines explicit Go linker flags with LLGo's typed -// default. The default never overrides an explicit -w value. +// omitDWARFRequested applies cmd/link's -w semantics. DWARF is preserved by +// default, and -s implies -w unless -w was explicitly set. Darwin c-shared +// builds default to -w; an explicit -w=false still overrides that default. func omitDWARFRequested(conf *Config) bool { - if conf.LinkOptions.DWARF == DWARFDefault && conf.OmitDWARFByDefault { + if conf.LinkOptions.DWARF == DWARFDefault && + conf.Goos == "darwin" && conf.BuildMode == BuildModeCShared { return true } return conf.LinkOptions.EffectiveOmitDWARF() @@ -83,9 +85,8 @@ func effectiveOmitDWARF(conf *Config, target *crosscompile.Export) bool { } // shouldEmitDebugInfo reports whether this compilation should produce DWARF. -// Linked modes use the typed LLGo default and target/linker constraints, with -// an explicit -w value taking precedence. ModeGen has no linker, so it emits -// only on an explicit preserve request. +// Linked modes use cmd/link defaults and target constraints. ModeGen has no +// linker, so it emits only on an explicit preserve request. func shouldEmitDebugInfo(conf *Config, target *crosscompile.Export) bool { if effectiveOmitDWARF(conf, target) { return false diff --git a/internal/build/link_options_test.go b/internal/build/link_options_test.go index b903487dfe..bf316c3ae8 100644 --- a/internal/build/link_options_test.go +++ b/internal/build/link_options_test.go @@ -40,9 +40,8 @@ func TestDwarfLinkerArgs(t *testing.T) { want []string }{ {name: "default"}, - {name: "safe default", conf: Config{BuildMode: BuildModeExe, OmitDWARFByDefault: true}, target: configurableDebugInfo(), want: []string{"-Wl,-S"}}, - {name: "safe default c-shared", conf: Config{BuildMode: BuildModeCShared, OmitDWARFByDefault: true}, target: configurableDebugInfo(), want: []string{"-Wl,-S"}}, - {name: "safe default c-archive", conf: Config{BuildMode: BuildModeCArchive, OmitDWARFByDefault: true}, target: configurableDebugInfo()}, + {name: "darwin c-shared default", conf: Config{Goos: "darwin", BuildMode: BuildModeCShared}, target: configurableDebugInfo(), want: []string{"-Wl,-S"}}, + {name: "darwin c-shared explicit w false", conf: Config{Goos: "darwin", BuildMode: BuildModeCShared, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, target: configurableDebugInfo()}, {name: "explicit c-shared w", conf: Config{BuildMode: BuildModeCShared, LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: configurableDebugInfo(), want: []string{"-Wl,-S"}}, {name: "explicit c-archive w", conf: Config{BuildMode: BuildModeCArchive, LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: configurableDebugInfo()}, {name: "w", conf: Config{LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: configurableDebugInfo(), want: []string{"-Wl,-S"}}, @@ -67,13 +66,13 @@ func TestEffectiveOmitDWARF(t *testing.T) { want bool }{ {name: "default"}, - {name: "safe default", conf: Config{BuildMode: BuildModeExe, OmitDWARFByDefault: true}, want: true}, - {name: "safe default c-shared", conf: Config{BuildMode: BuildModeCShared, OmitDWARFByDefault: true}, want: true}, - {name: "safe default c-archive", conf: Config{BuildMode: BuildModeCArchive, OmitDWARFByDefault: true}, want: true}, + {name: "s implies w", conf: Config{LinkOptions: LinkOptions{OmitSymbolTable: true}}, want: true}, + {name: "darwin c-shared default", conf: Config{Goos: "darwin", BuildMode: BuildModeCShared}, want: true}, + {name: "linux c-shared default", conf: Config{Goos: "linux", BuildMode: BuildModeCShared}}, {name: "requested", conf: Config{LinkOptions: LinkOptions{DWARF: DWARFOmit}}, want: true}, {name: "target baseline", target: alwaysOmitDebugInfo(), want: true}, {name: "explicit preserve", conf: Config{LinkOptions: LinkOptions{DWARF: DWARFPreserve}}}, - {name: "explicit preserve overrides safe default", conf: Config{OmitDWARFByDefault: true, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}}, + {name: "explicit preserve overrides darwin c-shared default", conf: Config{Goos: "darwin", BuildMode: BuildModeCShared, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -92,9 +91,10 @@ func TestShouldEmitDebugInfo(t *testing.T) { want bool }{ {name: "linked default", conf: Config{Mode: ModeBuild}, want: true}, - {name: "linked safe default", conf: Config{Mode: ModeBuild, BuildMode: BuildModeExe, OmitDWARFByDefault: true}}, - {name: "c-shared safe default", conf: Config{Mode: ModeBuild, BuildMode: BuildModeCShared, OmitDWARFByDefault: true}}, - {name: "c-archive safe default", conf: Config{Mode: ModeBuild, BuildMode: BuildModeCArchive, OmitDWARFByDefault: true}}, + {name: "linux c-shared default", conf: Config{Mode: ModeBuild, Goos: "linux", BuildMode: BuildModeCShared}, want: true}, + {name: "c-archive default", conf: Config{Mode: ModeBuild, BuildMode: BuildModeCArchive}, want: true}, + {name: "darwin c-shared default", conf: Config{Mode: ModeBuild, Goos: "darwin", BuildMode: BuildModeCShared}}, + {name: "darwin c-shared w false", conf: Config{Mode: ModeBuild, Goos: "darwin", BuildMode: BuildModeCShared, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, want: true}, {name: "linked w", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFOmit}}}, {name: "linked s", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{OmitSymbolTable: true}}}, {name: "linked s w false", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{OmitSymbolTable: true, DWARF: DWARFPreserve}}, want: true}, @@ -121,10 +121,8 @@ func TestValidateLinkOptions(t *testing.T) { wantErr bool }{ {name: "linux executable", conf: Config{Goos: "linux", BuildMode: BuildModeExe, LinkOptions: w}, target: configurableDebugInfo()}, - {name: "linux executable safe default", conf: Config{Goos: "linux", BuildMode: BuildModeExe, OmitDWARFByDefault: true}, target: configurableDebugInfo()}, {name: "darwin executable", conf: Config{Goos: "darwin", BuildMode: BuildModeExe, LinkOptions: w}, target: configurableDebugInfo()}, - {name: "c-shared safe default", conf: Config{Goos: "linux", BuildMode: BuildModeCShared, OmitDWARFByDefault: true}, target: configurableDebugInfo()}, - {name: "c-archive safe default", conf: Config{Goos: "linux", BuildMode: BuildModeCArchive, OmitDWARFByDefault: true}, target: configurableDebugInfo()}, + {name: "darwin c-shared default", conf: Config{Goos: "darwin", BuildMode: BuildModeCShared}, target: configurableDebugInfo()}, {name: "unsupported native OS", conf: Config{Goos: "windows", BuildMode: BuildModeExe, LinkOptions: w}, wantErr: true}, {name: "c-shared omit", conf: Config{Goos: "linux", BuildMode: BuildModeCShared, LinkOptions: w}, target: configurableDebugInfo()}, {name: "c-archive omit", conf: Config{Goos: "linux", BuildMode: BuildModeCArchive, LinkOptions: w}, target: configurableDebugInfo()}, diff --git a/internal/build/optlevel_test.go b/internal/build/optlevel_test.go index ce6d1e7791..857f0a92c5 100644 --- a/internal/build/optlevel_test.go +++ b/internal/build/optlevel_test.go @@ -71,3 +71,14 @@ func TestLLVMPassPipeline(t *testing.T) { } } } + +func TestShouldRunLLVMPasses(t *testing.T) { + for _, mode := range []Mode{ModeBuild, ModeInstall, ModeRun, ModeTest, ModeCmpTest} { + if !shouldRunLLVMPasses(mode) { + t.Errorf("shouldRunLLVMPasses(%v) = false, want true", mode) + } + } + if shouldRunLLVMPasses(ModeGen) { + t.Fatal("shouldRunLLVMPasses(ModeGen) = true, want false") + } +} diff --git a/internal/build/pcln_mode.go b/internal/build/pcln_mode.go index 56682058fe..8ae0e39e42 100644 --- a/internal/build/pcln_mode.go +++ b/internal/build/pcln_mode.go @@ -70,15 +70,12 @@ func effectivePCLNMode(conf *Config) PCLNMode { } // shouldEnablePCLNSites reports whether compiler-emitted PC anchor records are -// required for this build. Darwin DWARF builds keep the historical site-free -// path because inline anchors disturb LLDB lexical scopes there. ELF cannot -// reconstruct all Go entry PCs with dlsym: most Go symbols are intentionally -// absent from .dynsym, so Linux keeps sites even when it emits DWARF. -func shouldEnablePCLNSites(conf *Config, funcInfo, emitDebugInfo bool) bool { +// required for this build. +func shouldEnablePCLNSites(conf *Config, funcInfo bool) bool { if conf == nil || !funcInfo || !IsFuncInfoSitesEnabled() { return false } - return !emitDebugInfo || conf.Goos == "linux" || conf.PCLNMode == PCLNExternal + return true } // validatePCLNMode checks whether the selected build can produce the requested diff --git a/internal/build/pcln_mode_test.go b/internal/build/pcln_mode_test.go index 0b14102d59..483ba7d7f7 100644 --- a/internal/build/pcln_mode_test.go +++ b/internal/build/pcln_mode_test.go @@ -26,6 +26,7 @@ import ( "testing" buildfuncinfo "github.com/goplus/llgo/internal/build/funcinfo" + "github.com/goplus/llgo/internal/crosscompile" "github.com/goplus/llgo/internal/pclnmap" ) @@ -121,27 +122,26 @@ func TestEffectivePCLNModeLegacyPrecedence(t *testing.T) { func TestShouldEnablePCLNSites(t *testing.T) { t.Setenv(llgoFuncInfoSites, "1") tests := []struct { - name string - conf Config - funcInfo bool - debugInfo bool - want bool + name string + conf Config + funcInfo bool + want bool }{ {name: "embedded without debug", conf: Config{Goos: "darwin", PCLNMode: PCLNEmbedded}, funcInfo: true, want: true}, - {name: "darwin embedded debug", conf: Config{Goos: "darwin", PCLNMode: PCLNEmbedded}, funcInfo: true, debugInfo: true}, - {name: "linux embedded debug", conf: Config{Goos: "linux", PCLNMode: PCLNEmbedded}, funcInfo: true, debugInfo: true, want: true}, - {name: "external debug", conf: Config{Goos: "darwin", PCLNMode: PCLNExternal}, funcInfo: true, debugInfo: true, want: true}, - {name: "metadata disabled", conf: Config{Goos: "linux", PCLNMode: PCLNEmbedded}, debugInfo: true}, + {name: "darwin embedded debug", conf: Config{Goos: "darwin", PCLNMode: PCLNEmbedded}, funcInfo: true, want: true}, + {name: "linux embedded debug", conf: Config{Goos: "linux", PCLNMode: PCLNEmbedded}, funcInfo: true, want: true}, + {name: "external debug", conf: Config{Goos: "darwin", PCLNMode: PCLNExternal}, funcInfo: true, want: true}, + {name: "metadata disabled", conf: Config{Goos: "linux", PCLNMode: PCLNEmbedded}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := shouldEnablePCLNSites(&tt.conf, tt.funcInfo, tt.debugInfo); got != tt.want { + if got := shouldEnablePCLNSites(&tt.conf, tt.funcInfo); got != tt.want { t.Fatalf("shouldEnablePCLNSites() = %v, want %v", got, tt.want) } }) } t.Setenv(llgoFuncInfoSites, "0") - if shouldEnablePCLNSites(&Config{Goos: "linux", PCLNMode: PCLNExternal}, true, true) { + if shouldEnablePCLNSites(&Config{Goos: "linux", PCLNMode: PCLNExternal}, true) { t.Fatal("LLGO_FUNCINFO_SITES=0 did not disable sites") } } @@ -156,12 +156,13 @@ func TestNewDefaultConfMetadataDefaults(t *testing.T) { if conf.PCLNModeSet { t.Fatal("NewDefaultConf().PCLNModeSet = true, want unresolved legacy default") } - if !conf.OmitDWARFByDefault { - t.Fatal("NewDefaultConf().OmitDWARFByDefault = false, want safe provisional-DWARF default") + target := crosscompile.Export{} + if !shouldEmitDebugInfo(conf, &target) { + t.Fatal("NewDefaultConf() omits DWARF, want cmd/link default") } genConf := NewDefaultConf(ModeGen) - if genConf.OmitDWARFByDefault { - t.Fatal("NewDefaultConf(ModeGen).OmitDWARFByDefault = true, want no linked-build policy") + if shouldEmitDebugInfo(genConf, &target) { + t.Fatal("NewDefaultConf(ModeGen) emits DWARF without an explicit request") } } diff --git a/internal/build/ssa_order_fix.go b/internal/build/ssa_order_fix.go index 57235740c4..1efd9e46b4 100644 --- a/internal/build/ssa_order_fix.go +++ b/internal/build/ssa_order_fix.go @@ -186,6 +186,9 @@ func moveAssignDepsAfterRecv(b *ssa.BasicBlock, roots []ssa.Value, recv ssa.Valu if len(move) == 0 { return false } + // Metadata uses must follow the definitions they describe rather than + // blocking an otherwise safe source-order repair. + includeDebugRefsForMovedValues(b.Instrs, move, recvIdx) if moveWouldBreakSSA(b.Instrs, move, recvIdx) { return false } @@ -205,6 +208,30 @@ func moveAssignDepsAfterRecv(b *ssa.BasicBlock, roots []ssa.Value, recv ssa.Valu return true } +func includeDebugRefsForMovedValues(instrs []ssa.Instruction, move map[int]struct{}, through int) { + moved := make(map[ssa.Value]struct{}, len(move)) + for i := range move { + if v, ok := instrs[i].(ssa.Value); ok && v != nil { + moved[v] = struct{}{} + } + } + for i := 0; i <= through && i < len(instrs); i++ { + if _, moving := move[i]; moving { + continue + } + ref, ok := instrs[i].(*ssa.DebugRef) + if !ok { + continue + } + for v := range moved { + if instrUsesValue(ref, v) { + move[i] = struct{}{} + break + } + } + } +} + func moveWouldBreakSSA(instrs []ssa.Instruction, move map[int]struct{}, recvIdx int) bool { moved := make(map[ssa.Value]struct{}, len(move)) for i := range move { @@ -302,11 +329,15 @@ func fixSSAOrderBlock(b *ssa.BasicBlock) { continue } - // If the loaded value is used by any instruction between its current - // position and the return (excluding return itself), moving it may place - // its definition after one of those uses and break SSA form. + // DebugRefs are metadata-only and move with the value they describe. Any + // executable use before Return still makes reordering unsafe. + moving := map[ssa.Instruction]struct{}{u: {}} usedBeforeReturn := false for i := loadIdx + 1; i < retIdx; i++ { + if ref, ok := b.Instrs[i].(*ssa.DebugRef); ok && instrUsesValue(ref, u) { + moving[ref] = struct{}{} + continue + } if instrUsesValue(b.Instrs[i], u) { usedBeforeReturn = true break @@ -316,9 +347,7 @@ func fixSSAOrderBlock(b *ssa.BasicBlock) { continue } - // Move the load right after the last call (but before Return). - b.Instrs = moveInstr(b.Instrs, loadIdx, lastCallIdx+1) - // Adjust retIdx for subsequent moves in this block. + b.Instrs = moveInstrsAfter(b.Instrs, moving, b.Instrs[lastCallIdx]) retIdx = indexOfInstr(b.Instrs, ret) } } @@ -391,41 +420,29 @@ func valueDependsOn(v, target ssa.Value, seen map[ssa.Value]struct{}) bool { return false } -// moveInstr moves instrs[from] to position to (like inserting before to), -// preserving relative order of other elements. -func moveInstr(instrs []ssa.Instruction, from, to int) []ssa.Instruction { - if from < 0 || from >= len(instrs) { - return instrs - } - if to < 0 { - to = 0 - } - if to > len(instrs) { - to = len(instrs) - } - if from == to || from+1 == to { +// moveInstrsAfter moves selected instructions as a stable group immediately +// after anchor. +func moveInstrsAfter(instrs []ssa.Instruction, moving map[ssa.Instruction]struct{}, anchor ssa.Instruction) []ssa.Instruction { + if len(moving) == 0 || anchor == nil { return instrs } - - ins := instrs[from] - // Remove. - copy(instrs[from:], instrs[from+1:]) - instrs = instrs[:len(instrs)-1] - - // Recompute insertion index after removal. - if to > from { - to-- - } - if to < 0 { - to = 0 - } - if to > len(instrs) { - to = len(instrs) + moved := make([]ssa.Instruction, 0, len(moving)) + remaining := make([]ssa.Instruction, 0, len(instrs)) + for _, instr := range instrs { + if _, ok := moving[instr]; ok { + moved = append(moved, instr) + continue + } + remaining = append(remaining, instr) + } + for i, instr := range remaining { + if instr == anchor { + ret := make([]ssa.Instruction, 0, len(instrs)) + ret = append(ret, remaining[:i+1]...) + ret = append(ret, moved...) + ret = append(ret, remaining[i+1:]...) + return ret + } } - - // Insert. - instrs = append(instrs, nil) - copy(instrs[to+1:], instrs[to:]) - instrs[to] = ins return instrs } diff --git a/internal/build/ssa_order_fix_test.go b/internal/build/ssa_order_fix_test.go index 7408ae261c..33ff26041f 100644 --- a/internal/build/ssa_order_fix_test.go +++ b/internal/build/ssa_order_fix_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" ) @@ -28,11 +29,12 @@ func f() { case *fp(&x, 100) = <-fc(c, 1): } }` - fn := buildSSAOrderTestPackage(t, src) - got := instrOrder(fn, "fc(", "<-", "fp(", "*t") - if !inOrder(got, "fc(", "<-", "fp(") { - t.Fatalf("single-case select receive assignment order = %v, want fc/receive before fp", got) - } + testSSAOrderModes(t, src, func(t *testing.T, fn *ssa.Function) { + got := instrOrder(fn, "fc(", "<-", "fp(", "*t") + if !inOrder(got, "fc(", "<-", "fp(") { + t.Fatalf("single-case select receive assignment order = %v, want fc/receive before fp", got) + } + }) } func TestFixSSAOrderPlainRecvAssignKeepsLeftToRight(t *testing.T) { @@ -66,11 +68,12 @@ func f() { case m[fn(13, 100)] = <-fc(c, 1): } }` - fn := buildSSAOrderTestPackage(t, src) - got := instrOrder(fn, "fc(", "<-", "fn(") - if !inOrder(got, "fc(", "<-", "fn(") { - t.Fatalf("single-case select map receive assignment order = %v, want fc/receive before fn", got) - } + testSSAOrderModes(t, src, func(t *testing.T, fn *ssa.Function) { + got := instrOrder(fn, "fc(", "<-", "fn(") + if !inOrder(got, "fc(", "<-", "fn(") { + t.Fatalf("single-case select map receive assignment order = %v, want fc/receive before fn", got) + } + }) } func TestFixSSAOrderSingleCaseSelectTwoValueRecv(t *testing.T) { @@ -87,11 +90,12 @@ func f() { case *fp(&x, 100), ok = <-fc(c, 1): } }` - fn := buildSSAOrderTestPackage(t, src) - got := instrOrder(fn, "fc(", "<-", "fp(", "*t") - if !inOrder(got, "fc(", "<-", "fp(") { - t.Fatalf("single-case select two-value receive assignment order = %v, want fc/receive before fp", got) - } + testSSAOrderModes(t, src, func(t *testing.T, fn *ssa.Function) { + got := instrOrder(fn, "fc(", "<-", "fp(", "*t") + if !inOrder(got, "fc(", "<-", "fp(") { + t.Fatalf("single-case select two-value receive assignment order = %v, want fc/receive before fp", got) + } + }) } func TestFixSSAOrderMultiCaseSelectKeepsLeftToRight(t *testing.T) { @@ -115,7 +119,161 @@ func f() { } } +func TestFixSSAOrderReturnLoadWithDebugRefs(t *testing.T) { + const src = `package p +type value struct { n int } +func (v *value) mutate() bool { v.n = 1; return true } +func f() (value, bool) { + var v value + return v, v.mutate() +}` + base := ssa.SanityCheckFunctions | ssa.InstantiateGenerics + for _, test := range []struct { + name string + mode ssa.BuilderMode + }{ + {name: "default", mode: base}, + {name: "global-debug", mode: base | ssa.GlobalDebug}, + } { + t.Run(test.name, func(t *testing.T) { + fn := buildSSAOrderTestPackageMode(t, src, test.mode) + checkReturnLoadAfterMutation(t, fn, "mutate") + }) + } +} + +func TestFixSSAOrderCryptoX509ParseOID(t *testing.T) { + fset := token.NewFileSet() + loaded, err := packages.Load(&packages.Config{ + Mode: packages.LoadSyntax, + Fset: fset, + }, "crypto/x509") + if err != nil { + t.Fatal(err) + } + if packages.PrintErrors(loaded) != 0 || len(loaded) != 1 { + t.Fatal("failed to load crypto/x509") + } + mode := ssa.SanityCheckFunctions | ssa.InstantiateGenerics | ssa.GlobalDebug + prog, ssaPackages := ssautil.Packages(loaded, mode) + prog.Build() + pkg := ssaPackages[0] + fixSSAOrder(pkg, loaded[0].Syntax) + checkReturnLoadAfterMutation(t, pkg.Func("ParseOID"), "unmarshalOIDText") +} + +func TestMoveInstrsAfter(t *testing.T) { + const src = `package p +func f() { + println(1) + println(2) + println(3) +}` + fn := buildSSAOrderTestPackage(t, src) + instrs := fn.Blocks[0].Instrs + if len(instrs) < 4 { + t.Fatalf("instructions = %d, want at least 4", len(instrs)) + } + + assertOrder := func(t *testing.T, got, want []ssa.Instruction) { + t.Helper() + if len(got) != len(want) { + t.Fatalf("instruction count = %d, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("instruction %d = %v, want %v", i, got[i], want[i]) + } + } + } + + t.Run("empty", func(t *testing.T) { + assertOrder(t, moveInstrsAfter(instrs, nil, instrs[1]), instrs) + }) + t.Run("nil-anchor", func(t *testing.T) { + moving := map[ssa.Instruction]struct{}{instrs[0]: {}} + assertOrder(t, moveInstrsAfter(instrs, moving, nil), instrs) + }) + t.Run("missing-anchor", func(t *testing.T) { + moving := map[ssa.Instruction]struct{}{instrs[0]: {}} + assertOrder(t, moveInstrsAfter(instrs, moving, &ssa.Return{}), instrs) + }) + t.Run("stable", func(t *testing.T) { + moving := map[ssa.Instruction]struct{}{ + instrs[0]: {}, + instrs[2]: {}, + } + want := []ssa.Instruction{instrs[1], instrs[0], instrs[2]} + want = append(want, instrs[3:]...) + assertOrder(t, moveInstrsAfter(instrs, moving, instrs[1]), want) + }) +} + +func checkReturnLoadAfterMutation(t *testing.T, fn *ssa.Function, mutation string) { + t.Helper() + var ret *ssa.Return + var mutationCall ssa.CallInstruction + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + if candidate, ok := instr.(*ssa.Return); ok { + ret = candidate + } + if call, ok := instr.(ssa.CallInstruction); ok { + callee := call.Common().StaticCallee() + if callee != nil && callee.Name() == mutation { + mutationCall = call + } + } + } + } + if ret == nil || len(ret.Results) != 2 || mutationCall == nil { + t.Fatalf("unexpected return instruction: %v", ret) + } + callInstr := mutationCall.(ssa.Instruction) + callIdx := indexOfInstr(callInstr.Block().Instrs, callInstr) + loadIdx := -1 + debugRefIdx := -1 + var resultLoad *ssa.UnOp + for i, instr := range callInstr.Block().Instrs { + if load, ok := instr.(*ssa.UnOp); ok && load.Op == token.MUL { + if alloc, ok := load.X.(*ssa.Alloc); ok && callUsesValue(mutationCall, alloc) { + loadIdx = i + resultLoad = load + } + } + if ref, ok := instr.(*ssa.DebugRef); ok && resultLoad != nil && instrUsesValue(ref, resultLoad) { + debugRefIdx = i + } + } + if callIdx < 0 || loadIdx <= callIdx { + t.Fatalf("return load index = %d, mutation call index = %d; want load after call\n%s", loadIdx, callIdx, fn) + } + if debugRefIdx >= 0 && debugRefIdx <= loadIdx { + t.Fatalf("DebugRef index = %d, load index = %d; want DebugRef after its load\n%s", debugRefIdx, loadIdx, fn) + } +} + func buildSSAOrderTestPackage(t *testing.T, src string) *ssa.Function { + return buildSSAOrderTestPackageMode(t, src, ssa.SanityCheckFunctions|ssa.InstantiateGenerics) +} + +func testSSAOrderModes(t *testing.T, src string, check func(*testing.T, *ssa.Function)) { + t.Helper() + base := ssa.SanityCheckFunctions | ssa.InstantiateGenerics + for _, test := range []struct { + name string + mode ssa.BuilderMode + }{ + {name: "default", mode: base}, + {name: "global-debug", mode: base | ssa.GlobalDebug}, + } { + t.Run(test.name, func(t *testing.T) { + check(t, buildSSAOrderTestPackageMode(t, src, test.mode)) + }) + } +} + +func buildSSAOrderTestPackageMode(t *testing.T, src string, mode ssa.BuilderMode) *ssa.Function { t.Helper() fset := token.NewFileSet() file, err := parser.ParseFile(fset, "p.go", src, 0) @@ -129,7 +287,7 @@ func buildSSAOrderTestPackage(t *testing.T, src string) *ssa.Function { fset, pkg, files, - ssa.SanityCheckFunctions|ssa.InstantiateGenerics, + mode, ) if err != nil { t.Fatalf("BuildPackage: %v", err) diff --git a/ltoplugin/LLGOReflectMethodByNamePass.cpp b/ltoplugin/LLGOReflectMethodByNamePass.cpp index 68c95283f5..57440ba617 100644 --- a/ltoplugin/LLGOReflectMethodByNamePass.cpp +++ b/ltoplugin/LLGOReflectMethodByNamePass.cpp @@ -173,20 +173,40 @@ bool isRuntimeStringSlice2(CallBase *CB) { return Callee && Callee->getName().ends_with(RuntimeStringSlice2Suffix); } +bool consumeStringCallArgument(CallBase *CB, unsigned &NextArgIndex, + const DataLayout &DL, + SmallVectorImpl &Names, + unsigned Depth) { + if (NextArgIndex >= CB->arg_size()) + return false; + + Value *Arg = CB->getArgOperand(NextArgIndex); + if (Arg->getType()->isStructTy()) { + ++NextArgIndex; + return collectStringSetFromStringValue(Arg, DL, Names, Depth + 1); + } + + if (NextArgIndex + 1 >= CB->arg_size()) + return false; + Value *Len = CB->getArgOperand(NextArgIndex + 1); + if (!Arg->getType()->isPointerTy() || !Len->getType()->isIntegerTy()) + return false; + NextArgIndex += 2; + return collectStringSet(Arg, Len, DL, Names, Depth + 1); +} + bool collectStringSetFromStringCat(CallBase *CB, const DataLayout &DL, SmallVectorImpl &Names, unsigned Depth) { if (!isRuntimeStringCat(CB)) return false; - if (CB->arg_size() != 4) - return false; SmallVector Prefixes; SmallVector Suffixes; - if (!collectStringSet(CB->getArgOperand(0), CB->getArgOperand(1), DL, - Prefixes, Depth + 1) || - !collectStringSet(CB->getArgOperand(2), CB->getArgOperand(3), DL, - Suffixes, Depth + 1)) + unsigned NextArgIndex = 0; + if (!consumeStringCallArgument(CB, NextArgIndex, DL, Prefixes, Depth) || + !consumeStringCallArgument(CB, NextArgIndex, DL, Suffixes, Depth) || + NextArgIndex != CB->arg_size()) return false; return addConcatenatedNames(Names, Prefixes, Suffixes); } @@ -196,18 +216,19 @@ bool collectStringSetFromStringSlice2(CallBase *CB, const DataLayout &DL, unsigned Depth) { if (!isRuntimeStringSlice2(CB)) return false; - if (CB->arg_size() != 6) - return false; SmallVector Bases; - if (!collectStringSet(CB->getArgOperand(0), CB->getArgOperand(1), DL, Bases, - Depth + 1)) + unsigned NextArgIndex = 0; + if (!consumeStringCallArgument(CB, NextArgIndex, DL, Bases, Depth) || + CB->arg_size() != NextArgIndex + 4) return false; SmallVector Lows; SmallVector Highs; - if (!collectIntegerChoices(CB->getArgOperand(2), Lows, Depth + 1) || - !collectIntegerChoices(CB->getArgOperand(3), Highs, Depth + 1)) + if (!collectIntegerChoices(CB->getArgOperand(NextArgIndex), Lows, + Depth + 1) || + !collectIntegerChoices(CB->getArgOperand(NextArgIndex + 1), Highs, + Depth + 1)) return false; for (const std::string &Base : Bases) { @@ -886,6 +907,37 @@ bool collectStringSetFromFunctionStringArg(Value *Ptr, Value *Len, return true; } +bool collectStringSetFromFunctionStringValueArg( + Value *StringValue, const DataLayout &DL, + SmallVectorImpl &Names, unsigned Depth) { + auto *Arg = dyn_cast(StringValue); + if (!Arg || !Arg->getType()->isStructTy()) + return false; + + Function *F = Arg->getParent(); + if (!F) + return false; + + unsigned ArgNo = Arg->getArgNo(); + SmallVector Callers; + for (User *U : F->users()) { + auto *CB = dyn_cast(U); + if (!CB || CB->getCalledFunction() != F || ArgNo >= CB->arg_size()) + return false; + Callers.push_back(CB); + } + if (Callers.empty()) + return false; + + for (CallBase *CB : Callers) { + Value *CallValue = CB->getArgOperand(ArgNo); + if (!CallValue->getType()->isStructTy() || + !collectStringSetFromStringValue(CallValue, DL, Names, Depth + 1)) + return false; + } + return true; +} + bool collectStringSetFromStringValue(Value *StringValue, const DataLayout &DL, SmallVectorImpl &Names, unsigned Depth) { @@ -907,6 +959,9 @@ bool collectStringSetFromStringValue(Value *StringValue, const DataLayout &DL, return collectStringSetFromStringConstant(Folded, DL, Names, Depth + 1); } + if (collectStringSetFromFunctionStringValueArg(StringValue, DL, Names, Depth)) + return true; + if (auto *Insert = dyn_cast(StringValue)) { Value *Ptr = findInsertedValue(Insert, 0); Value *Len = findInsertedValue(Insert, 1); @@ -1039,23 +1094,18 @@ std::optional findReflectMethodNameArg(CallBase *ReflectCall) { return std::nullopt; } -// The plugin runs during LTO, after LLGo has applied C ABI lowering to every -// module that participates in the link. At this point a Go string argument is -// always split into (ptr, len). LLGo marks the string pointer parameter, and -// the length is the following parameter produced by the same lowering step. +// Optimized C ABI lowering splits a Go string into (ptr, len), with the name +// attribute on the pointer. Debug-preserving builds can retain the original +// aggregate string argument instead. Accept both representations so enabling +// DWARF does not change MethodByName reachability. bool collectStringSetFromCallArgs(CallBase *ReflectCall, const DataLayout &DL, SmallVectorImpl &Names) { std::optional NameArg = findReflectMethodNameArg(ReflectCall); if (!NameArg) return false; - if (*NameArg + 1 >= ReflectCall->arg_size()) - return false; - Value *Ptr = ReflectCall->getArgOperand(*NameArg); - Value *Len = ReflectCall->getArgOperand(*NameArg + 1); - if (!Ptr->getType()->isPointerTy() || !Len->getType()->isIntegerTy()) - return false; - return collectStringSet(Ptr, Len, DL, Names); + unsigned NextArgIndex = *NameArg; + return consumeStringCallArgument(ReflectCall, NextArgIndex, DL, Names, 0); } bool isTypeCheckedLoad(CallBase *CB) { diff --git a/ssa/di.go b/ssa/di.go index 18058eeebe..f046d8601a 100644 --- a/ssa/di.go +++ b/ssa/di.go @@ -769,6 +769,11 @@ const ( ) func (b Builder) DIGlobal(v Expr, name string, pos token.Position) { + // Frontend pseudo-variables, such as Python module attributes, have no + // native storage to describe or attach metadata to. + if v.impl.IsNil() { + return + } if _, ok := b.Pkg.glbDbgVars[v]; ok { return } diff --git a/ssa/di_debug_test.go b/ssa/di_debug_test.go index aebb9d016f..d5aaf5cc61 100644 --- a/ssa/di_debug_test.go +++ b/ssa/di_debug_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "github.com/goplus/gogen/packages" "github.com/goplus/llgo/internal/optlevel" "github.com/xgo-dev/llvm" ) @@ -166,6 +167,206 @@ type Shape struct { } } +func TestDeferInitBuilderInheritsDebugLocation(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "defer.go", `package p +func f() {} +func child() {} +`, 0) + if err != nil { + t.Fatal(err) + } + typesPkg, err := (&types.Config{}).Check("example.com/p", fset, []*ast.File{file}, nil) + if err != nil { + t.Fatal(err) + } + + prog := NewProgram(&Target{OptLevel: optlevel.O0}) + defer prog.Dispose() + prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) + pkg := prog.NewPackage("p", "example.com/p") + pkg.InitDebug("p", "example.com/p", fset) + decl := file.Decls[0].(*ast.FuncDecl) + object := typesPkg.Scope().Lookup("f").(*types.Func) + fn := pkg.NewFunc("example.com/p.f", object.Type().(*types.Signature), InGo) + builder := fn.MakeBody(1) + defer builder.Dispose() + bodyPos := fset.Position(decl.Body.Lbrace) + builder.DebugFunction(fn, object.Scope(), fset.Position(object.Pos()), bodyPos) + builder.DISetCurrentDebugLocation(fn, bodyPos) + builder.Return() + + deferBuilder, next := fn.deferInitBuilder(builder) + defer deferBuilder.Dispose() + loc := deferBuilder.impl.GetCurrentDebugLocation() + if loc.Line != uint(bodyPos.Line) || loc.Col != uint(bodyPos.Column) || loc.Scope != fn.diFunc.ll { + t.Fatalf("defer debug location = %+v, want %s:%d:%d", loc, bodyPos.Filename, bodyPos.Line, bodyPos.Column) + } + deferBuilder.Jump(next) + + childDecl := file.Decls[1].(*ast.FuncDecl) + childObject := typesPkg.Scope().Lookup("child").(*types.Func) + child := pkg.NewFunc("example.com/p.child", childObject.Type().(*types.Signature), InGo) + childBuilder := child.MakeBody(1) + defer childBuilder.Dispose() + childPos := fset.Position(childDecl.Body.Lbrace) + childBuilder.DebugFunction(child, childObject.Scope(), fset.Position(childObject.Pos()), childPos) + childBuilder.DISetCurrentDebugLocation(child, childPos) + childBuilder.Return() + + sameLoc := childBuilder.deferDebugLocationFor(child) + if sameLoc.Scope != child.diFunc.ll { + t.Fatal("same-function defer location changed scope") + } + ownerLoc := childBuilder.deferDebugLocationFor(fn) + if ownerLoc.Line != uint(childPos.Line) || ownerLoc.Col != uint(childPos.Column) || ownerLoc.Scope != fn.diFunc.ll || !ownerLoc.InlinedAt.IsNil() { + t.Fatalf("cross-function defer location = %+v, want owner scope at %s:%d:%d", ownerLoc, childPos.Filename, childPos.Line, childPos.Column) + } + plain := pkg.NewFunc("example.com/p.plain", NoArgsNoRet, InGo) + plainBuilder := plain.MakeBody(1) + defer plainBuilder.Dispose() + plainBuilder.Return() + if loc := childBuilder.deferDebugLocationFor(plain); !loc.Scope.IsNil() { + t.Fatalf("defer location for non-debug owner has scope %+v", loc.Scope) + } + + pkg.FinalizeDebug() + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatalf("defer debug metadata is invalid: %v\n%s", err, pkg.Module().String()) + } +} + +func TestDIGlobalIgnoresStorageLessFrontendVariable(t *testing.T) { + var builder Builder + builder.DIGlobal(pyVarExpr(Nil, "attribute"), "module.attribute", token.Position{}) +} + +func TestDeferredCallsKeepDebugLocations(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "defer.go", `package p +func cleanup(int) {} +func f() { + defer cleanup(1) + defer cleanup(2) +} +`, 0) + if err != nil { + t.Fatal(err) + } + typesPkg, err := (&types.Config{}).Check("example.com/p", fset, []*ast.File{file}, nil) + if err != nil { + t.Fatal(err) + } + + prog := NewProgram(&Target{OptLevel: optlevel.O0}) + defer prog.Dispose() + prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) + imp := packages.NewImporter(fset) + prog.SetRuntime(func() *types.Package { + pkg, err := imp.Import(PkgRuntime) + if err != nil { + t.Fatal(err) + } + return pkg + }) + pkg := prog.NewPackage("p", "example.com/p") + pkg.InitDebug("p", "example.com/p", fset) + + cleanupObject := typesPkg.Scope().Lookup("cleanup").(*types.Func) + cleanup := pkg.NewFunc("example.com/p.cleanup", cleanupObject.Type().(*types.Signature), InGo) + cleanupBuilder := cleanup.MakeBody(1) + defer cleanupBuilder.Dispose() + cleanupBuilder.Return() + cleanupBuilder.EndBuild() + + decl := file.Decls[1].(*ast.FuncDecl) + object := typesPkg.Scope().Lookup("f").(*types.Func) + fn := pkg.NewFunc("example.com/p.f", object.Type().(*types.Signature), InGo) + b := fn.MakeBody(1) + defer b.Dispose() + bodyPos := fset.Position(decl.Body.Lbrace) + b.DebugFunction(fn, object.Scope(), fset.Position(object.Pos()), bodyPos) + recoverBlock := fn.MakeBlock() + fn.SetRecover(recoverBlock) + b.SetBlock(recoverBlock).Return() + b.SetBlock(fn.Block(0)) + + firstDefer := decl.Body.List[0].(*ast.DeferStmt) + b.DISetCurrentDebugLocation(fn, fset.Position(firstDefer.Defer)) + b.Defer(DeferAlways, cleanup.Expr, Builder.Call, prog.Val(1)) + secondDefer := decl.Body.List[1].(*ast.DeferStmt) + b.DISetCurrentDebugLocation(fn, fset.Position(secondDefer.Defer)) + b.Defer(DeferInLoop, cleanup.Expr, Builder.Call, prog.Val(2)) + b.RunDefers() + b.Return() + b.EndBuild() + pkg.FinalizeDebug() + + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatalf("defer debug metadata is invalid: %v\n%s", err, pkg.Module().String()) + } + ir := pkg.Module().String() + for _, target := range []string{"FreeDeferNode", "SetThreadDefer", "example.com/p.cleanup"} { + found := false + for _, line := range strings.Split(ir, "\n") { + if !strings.Contains(line, " call ") || !strings.Contains(line, target) { + continue + } + found = true + if !strings.Contains(line, "!dbg !") { + t.Errorf("call to %s is missing a debug location: %s", target, line) + } + } + if !found { + t.Errorf("no call to %s found in IR", target) + } + } +} + +func TestInlineAsmNoDebugPreservesBuilderLocation(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "asm.go", `package p +func f() {} +`, 0) + if err != nil { + t.Fatal(err) + } + typesPkg, err := (&types.Config{}).Check("example.com/p", fset, []*ast.File{file}, nil) + if err != nil { + t.Fatal(err) + } + + prog := NewProgram(&Target{OptLevel: optlevel.O0}) + defer prog.Dispose() + prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) + pkg := prog.NewPackage("p", "example.com/p") + pkg.InitDebug("p", "example.com/p", fset) + decl := file.Decls[0].(*ast.FuncDecl) + object := typesPkg.Scope().Lookup("f").(*types.Func) + fn := pkg.NewFunc("example.com/p.f", object.Type().(*types.Signature), InGo) + b := fn.MakeBody(1) + defer b.Dispose() + pos := fset.Position(decl.Body.Lbrace) + b.DebugFunction(fn, object.Scope(), fset.Position(object.Pos()), pos) + b.DISetCurrentDebugLocation(fn, pos) + b.InlineAsmNoDebug("nop") + b.Return() + b.EndBuild() + pkg.FinalizeDebug() + + asm := fn.impl.EntryBasicBlock().FirstInstruction() + if !asm.InstructionDebugLoc().IsNil() { + t.Fatal("inline assembly retained the current debug location") + } + ret := llvm.NextInstruction(asm) + if ret.IsNil() || ret.InstructionDebugLoc().IsNil() { + t.Fatal("inline assembly cleared the builder debug location") + } + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatalf("inline assembly debug metadata is invalid: %v\n%s", err, pkg.Module().String()) + } +} + func newDebugRuntimePackage() *types.Package { pkg := types.NewPackage(PkgRuntime, "runtime") unsafePointer := types.Typ[types.UnsafePointer] diff --git a/ssa/eh.go b/ssa/eh.go index b8ead4eb64..7678f94c38 100644 --- a/ssa/eh.go +++ b/ssa/eh.go @@ -136,13 +136,45 @@ func (b Builder) Longjmp(jb, retval Expr) { // ----------------------------------------------------------------------------- -func (p Function) deferInitBuilder() (b Builder, next BasicBlock) { +func (p Function) deferInitBuilder(from Builder) (b Builder, next BasicBlock) { b = p.NewBuilder() + b.setDeferDebugLocation(from.deferDebugLocation()) next = b.setBlockMoveLast(p.blks[0]) p.blks[0].last = next.last return } +func (b Builder) deferDebugLocation() llvm.DebugLoc { + if b.Func.diFunc == nil { + return llvm.DebugLoc{} + } + return b.impl.GetCurrentDebugLocation() +} + +func (b Builder) deferDebugLocationFor(owner Function) llvm.DebugLoc { + loc := b.deferDebugLocation() + if b.Func == owner || loc.Scope.IsNil() { + return loc + } + if owner.diFunc == nil { + return llvm.DebugLoc{} + } + loc.Scope = owner.diFunc.ll + loc.InlinedAt = llvm.Metadata{} + return loc +} + +func (b Builder) setDeferDebugLocation(loc llvm.DebugLoc) { + if !loc.Scope.IsNil() { + b.impl.SetCurrentDebugLocation(loc.Line, loc.Col, loc.Scope, loc.InlinedAt) + } +} + +type deferStmt struct { + loc llvm.DebugLoc + emit func(bits Expr) +} + type aDefer struct { nextBit int // next defer bit data Expr // pointer to runtime.Defer @@ -158,7 +190,7 @@ type aDefer struct { // walking defers in reverse order in endDefer). loopDrainerGenerated bool loopCases []loopDeferCase - stmts []func(bits Expr) + stmts []deferStmt } // loopDeferCase represents a defer statement inside a loop. @@ -170,6 +202,7 @@ type loopDeferCase struct { fn Expr args []Expr buildCall func(Builder, Expr, ...Expr) Expr + loc llvm.DebugLoc } const ( @@ -197,7 +230,7 @@ func (b Builder) getDefer(kind DoAction) *aDefer { // TODO(xsw): check if in pkg.init var next, panicBlk BasicBlock if kind != DeferAlways { - b, next = self.deferInitBuilder() + b, next = self.deferInitBuilder(b) } blks := self.MakeBlocks(2) @@ -346,7 +379,7 @@ func (b Builder) Defer(kind DoAction, fn Expr, buildCall func(Builder, Expr, ... } typ := b.saveDeferArgs(self, kind, id, fn, args) if kind == DeferInLoop { - loopCase := loopDeferCase{id: id, typ: typ, fn: fn, args: args, buildCall: buildCall} + loopCase := loopDeferCase{id: id, typ: typ, fn: fn, args: args, buildCall: buildCall, loc: b.deferDebugLocation()} self.loopCases = append(self.loopCases, loopCase) } b.appendDeferStmt(self, kind, typ, buildCall, fn, args, nextbit) @@ -372,6 +405,7 @@ func (b Builder) DeferTo(owner Function, stack Expr, fn Expr, buildCall func(Bui fn: fn, args: args, buildCall: buildCall, + loc: b.deferDebugLocationFor(owner), } if self == nil { owner.pendingLoopCases = append(owner.pendingLoopCases, loopCase) @@ -381,7 +415,7 @@ func (b Builder) DeferTo(owner Function, stack Expr, fn Expr, buildCall func(Bui } func (b Builder) appendDeferStmt(self *aDefer, kind DoAction, typ Type, buildCall func(Builder, Expr, ...Expr) Expr, fn Expr, args []Expr, nextbit Expr) { - self.stmts = append(self.stmts, func(bits Expr) { + self.stmts = append(self.stmts, deferStmt{loc: b.deferDebugLocation(), emit: func(bits Expr) { switch kind { case DeferInCond: // Leaving a run of loop defers; allow the next loop-defer statement @@ -401,13 +435,13 @@ func (b Builder) appendDeferStmt(self *aDefer, kind DoAction, typ Type, buildCal case DeferInLoop: b.loopDeferDrainer(self) } - }) + }}) } func (b Builder) appendLoopDeferDrainer(self *aDefer) { - self.stmts = append(self.stmts, func(Expr) { + self.stmts = append(self.stmts, deferStmt{loc: b.deferDebugLocation(), emit: func(Expr) { b.loopDeferDrainer(self) - }) + }}) } func (b Builder) loopDeferDrainer(self *aDefer) { @@ -456,6 +490,7 @@ func (b Builder) loopDeferDrainer(self *aDefer) { b.If(match, caseBlks[i], nextBlk) b.SetBlockEx(caseBlks[i], AtEnd, true) + b.setDeferDebugLocation(c.loc) b.Store(self.rethPtr, drainEntryAddr) b.callDefer(self, c.typ, c.buildCall, c.fn, c.args) b.Jump(condBlk) @@ -584,9 +619,11 @@ func (p Function) endDefer(b Builder) { for i := n - 1; i >= 0; i-- { rethNext := rethsNext[i] + stmt := stmts[i] b.SetBlockEx(rethsNext[i+1], AtEnd, true) + b.setDeferDebugLocation(stmt.loc) b.Store(rethPtr, rethNext.Addr()) - stmts[i](b.Load(bitsPtr)) + stmt.emit(b.Load(bitsPtr)) if i != 0 { b.Jump(rethNext) } diff --git a/ssa/expr.go b/ssa/expr.go index caee097c06..8f42af45cf 100644 --- a/ssa/expr.go +++ b/ssa/expr.go @@ -296,6 +296,17 @@ func (b Builder) InlineAsm(instruction string) { b.impl.CreateCall(typ, asm, nil, "") } +// InlineAsmNoDebug emits inline assembly without attaching the builder's +// current source location. The builder location itself is left unchanged. +func (b Builder) InlineAsmNoDebug(instruction string) { + dbgInstrf("InlineAsm %s\n", instruction) + + typ := llvm.FunctionType(b.Prog.tyVoid(), nil, false) + asm := llvm.InlineAsm(typ, instruction, "", true, false, llvm.InlineAsmDialectATT, false) + call := b.impl.CreateCall(typ, asm, nil, "") + call.InstructionSetDebugLoc(llvm.Metadata{}) +} + func (b Builder) InlineAsmFull(instruction, constraints string, retType Type, exprs []Expr) Expr { typs := make([]llvm.Type, len(exprs)) vals := make([]llvm.Value, len(exprs)) diff --git a/ssa/funcinfo.go b/ssa/funcinfo.go index 25e792b6c3..468b75c46e 100644 --- a/ssa/funcinfo.go +++ b/ssa/funcinfo.go @@ -37,12 +37,9 @@ func (p Program) FuncInfoMetadataEnabled() bool { return p.enableFuncInfoMetadata } -// EnableFuncInfoSites controls emission of the per-function site records -// (entry/stub/pc-line inline-asm fragments inside function bodies). They are -// gated separately from the funcinfo metadata tables because the -// body-embedded anchors shift instruction/scope layout enough to confuse -// debuggers; debug builds keep the tables (FuncForPC name/FileLine fidelity -// via the dlsym path) but drop the sites. +// EnableFuncInfoSites controls emission of per-function site records. The build +// layer may apply a narrower target policy to entry/stub address records while +// retaining PC-line records. func (p Program) EnableFuncInfoSites(enable bool) { p.enableFuncInfoSites = enable } diff --git a/ssa/package.go b/ssa/package.go index 8282a676d6..4aa785080b 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -363,6 +363,12 @@ func (p Program) EnableLTOPluginMarkers(enable bool) { p.enableLTOPluginMarker = enable } +// SetDebugInfoOptimized records whether the LLVM IR optimization pipeline runs. +// It only controls DWARF's optimized marker and never selects compiler passes. +func (p Program) SetDebugInfoOptimized(enable bool) { + p.debugInfoOptimized = enable +} + func (p Program) SetNoInterfaceMethod(fullName string) { p.noInterface[fullName] = none{} } diff --git a/test/go/dwarf_pcln_acceptance_test.go b/test/go/dwarf_pcln_acceptance_test.go new file mode 100644 index 0000000000..f535118918 --- /dev/null +++ b/test/go/dwarf_pcln_acceptance_test.go @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gotest + +import ( + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" +) + +const dwarfPCLNProbe = `package main + +import ( + "log" + "os" + "runtime" + "strconv" + "strings" +) + +func checkCaller() { + _, file, line, ok := runtime.Caller(0) // CALLER_MARK + if !ok || !strings.HasSuffix(file, "main.go") || line != CALLER_LINE { + panic("bad caller: " + file + ":" + strconv.Itoa(line)) + } +} + +func checkFrames() { + var pcs [8]uintptr + n := runtime.Callers(0, pcs[:]) // FRAMES_MARK + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function == "main.checkFrames" { + if !strings.HasSuffix(frame.File, "main.go") || frame.Line != FRAMES_LINE { + panic("bad frame: " + frame.File + ":" + strconv.Itoa(frame.Line)) + } + break + } + if !more { + panic("checkFrames frame missing") + } + } +} + +func main() { + var out strings.Builder + logger := log.New(&out, "", log.Lshortfile) + logger.Print("site") // LOG_MARK + want := "main.go:" + strconv.Itoa(LOG_LINE) + ":" + if !strings.HasPrefix(out.String(), want) { + panic("bad log site: " + out.String()) + } + + checkCaller() + checkFrames() + os.Stdout.WriteString("PCLN_DWARF_OK\n") +} +` + +func TestDWARFPCLNLineSites(t *testing.T) { + source := dwarfPCLNProbe + for _, marker := range []string{"CALLER", "FRAMES", "LOG"} { + source = strings.ReplaceAll(source, marker+"_LINE", strconv.Itoa(markerLine(source, marker+"_MARK"))) + } + dir := t.TempDir() + file := filepath.Join(dir, "main.go") + if err := os.WriteFile(file, []byte(source), 0o644); err != nil { + t.Fatal(err) + } + + repoRoot := findStringConversionRepoRoot(t) + t.Setenv("LLGO_ROOT", repoRoot) + cmd := exec.Command("go", "run", "-p=1", "./cmd/llgo", "run", "-a", "-ldflags=-w=false", file) + cmd.Dir = repoRoot + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("DWARF PCLN probe failed: %v\n%s", err, out) + } + if !strings.Contains(string(out), "PCLN_DWARF_OK") { + t.Fatalf("DWARF PCLN probe is missing its success marker:\n%s", out) + } +} diff --git a/test/go/dwarf_semantics_acceptance_test.go b/test/go/dwarf_semantics_acceptance_test.go new file mode 100644 index 0000000000..29a8c34fc8 --- /dev/null +++ b/test/go/dwarf_semantics_acceptance_test.go @@ -0,0 +1,62 @@ +//go:build !llgo + +package gotest + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +const dwarfReturnOrderProbe = `package main + +type value struct { + n int +} + +func (v *value) mutate() bool { + v.n = 1 + return true +} + +func result() (value, bool) { + var v value + return v, v.mutate() +} + +func main() { + v, ok := result() + if !ok || v.n != 1 { + panic("return value was loaded before mutation") + } + println("RETURN_ORDER_OK") +} +` + +func TestDWARFReturnOrderSemantics(t *testing.T) { + repoRoot := findStringConversionRepoRoot(t) + source := filepath.Join(t.TempDir(), "main.go") + if err := os.WriteFile(source, []byte(dwarfReturnOrderProbe), 0o600); err != nil { + t.Fatal(err) + } + cmd := exec.Command( + "go", "run", "./cmd/llgo", "run", "-ldflags=-w=false", source, + ) + cmd.Dir = repoRoot + cmd.Env = append(os.Environ(), + "LLGO_ROOT="+repoRoot, + "LLGO_BUILD_CACHE=off", + "GOMAXPROCS=2", + "GOMEMLIMIT=6GiB", + "GOFLAGS=-p=1", + ) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("LLGo DWARF return-order acceptance failed: %v\n%s", err, out) + } + if !strings.Contains(string(out), "RETURN_ORDER_OK") { + t.Fatalf("LLGo DWARF return-order acceptance did not report success:\n%s", out) + } +}