diff --git a/.github/workflows/browser-debug.yml b/.github/workflows/browser-debug.yml index fecada2510..ec6db7fe6f 100644 --- a/.github/workflows/browser-debug.yml +++ b/.github/workflows/browser-debug.yml @@ -91,9 +91,9 @@ jobs: run: | set -euo pipefail package=./internal/debugabi/testdata/fixture - GOOS=js GOARCH=wasm llgo build -debug-artifact=embedded \ + GOOS=js GOARCH=wasm llgo build -O0 -debug-artifact=embedded \ -o "${RUNNER_TEMP}/browser-embedded.wasm" "${package}" - GOOS=js GOARCH=wasm llgo build -debug-artifact=external \ + GOOS=js GOARCH=wasm llgo build -O0 -debug-artifact=external \ -o "${RUNNER_TEMP}/browser-external.wasm" "${package}" test -s "${RUNNER_TEMP}/browser-embedded.wasm" test -s "${RUNNER_TEMP}/browser-external.wasm" diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 346ba76e99..4898c04aec 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -140,7 +140,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: ${{ startsWith(matrix.os, 'macos') && 45 || 30 }} + timeout-minutes: ${{ (startsWith(matrix.os, 'macos') || matrix.lane == 'primary') && 45 || 30 }} strategy: matrix: # Keep compatibility and primary toolchains pinned to exact patches. 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/cltest/cltest.go b/cl/cltest/cltest.go index 4a1fc60bef..ff6f46f730 100644 --- a/cl/cltest/cltest.go +++ b/cl/cltest/cltest.go @@ -433,6 +433,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 var meta string prevHook := localConf.ModuleHook @@ -480,6 +485,9 @@ func withMetaCaptures(conf *build.Config, pkgDirs []string) (*build.Config, map[ conf = build.NewDefaultConf(build.ModeRun) } localConf := *conf + if localConf.LinkOptions.DWARF == build.DWARFDefault { + localConf.LinkOptions.DWARF = build.DWARFOmit + } localConf.ForceRebuild = true metas := make(map[string]*string, len(pkgDirs)) for _, pkgDir := range pkgDirs { diff --git a/cl/cltest/cltest_test.go b/cl/cltest/cltest_test.go index 636295b984..6da3fc2be8 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 9032deb02b..1803f4192f 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -1663,10 +1663,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 2e61753a50..009b2a3361 100644 --- a/cl/compile_test.go +++ b/cl/compile_test.go @@ -222,6 +222,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", @@ -245,6 +263,13 @@ var testltoLTOPluginTests = []string{ "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") @@ -316,6 +341,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) // See TestBuildAndCheckSymbolsFromTestlto: dynamic main.* exports retain @@ -380,6 +413,19 @@ func TestBuildAndCheckSymbolsFromTestltoLTOPluginAggregateABI(t *testing.T) { } } +func TestBuildAndCheckSymbolsFromTestltoLTOPluginDWARF(t *testing.T) { + t.Setenv("LLGO_BUILD_CACHE", "off") + buildConf := testltoLTOPluginConf(t, build.ModeBuild) + buildConf.LinkOptions.DWARF = build.DWARFPreserve + // Match the non-DWARF symbol gate: Linux's embedded pclntab exports main.* + // dynamically, which retains the very methods this test expects LTO to + // remove and is independent of whether DWARF is enabled. + buildConf.PCLNMode = build.PCLNNone + cltest.BuildAndCheckSymbolsFromDir(t, "", "./_testlto", testltoLTOPluginDWARFTests, + cltest.WithRunConfig(buildConf), + ) +} + func TestFilterEmulatorOutput(t *testing.T) { tests := []struct { name string diff --git a/cl/debug_compile_test.go b/cl/debug_compile_test.go index 1338ba715c..03874d0e5f 100644 --- a/cl/debug_compile_test.go +++ b/cl/debug_compile_test.go @@ -58,26 +58,8 @@ func TestFrontendOptions(t *testing.T) { } } -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 { @@ -97,9 +79,9 @@ var anonymous = func(seed int) int { 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 := newPackageEx(prog, nil, nil, nil, ssaPkg, []*ast.File{file}, nil, false, Options{ Debug: true, DebugSymbols: true, @@ -107,6 +89,30 @@ var anonymous = func(seed int) int { 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()) } @@ -127,3 +133,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 7e091eee82..00d52eb493 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -1582,7 +1582,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 ce5d370d33..f328ef3ab1 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -27,6 +27,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) @@ -34,7 +38,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 { @@ -155,6 +158,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 9f8fe7ad8d..d89fd45256 100644 --- a/cl/static_init.go +++ b/cl/static_init.go @@ -209,16 +209,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 { @@ -233,11 +233,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/cmd/internal/debug/debug.go b/cmd/internal/debug/debug.go index a7beb055f4..cad9e5a31a 100644 --- a/cmd/internal/debug/debug.go +++ b/cmd/internal/debug/debug.go @@ -131,7 +131,6 @@ func run(packageArgs, debuggerArgs []string, opts options, stdin io.Reader, stdo return fmt.Errorf("llgo debug: %w", err) } conf.BuildMode = build.BuildModeExe - conf.OmitDWARFByDefault = false if conf.LinkOptions.EffectiveOmitDWARF() || (conf.DebugArtifactModeSet && conf.DebugArtifactMode == build.DebugArtifactNone) { return errors.New("llgo debug: debug information is required; remove -ldflags=-w or -debug-artifact=none") diff --git a/cmd/llgo/debugtest/ACCEPTANCE.md b/cmd/llgo/debugtest/ACCEPTANCE.md index 73fc3ef702..02182506d5 100644 --- a/cmd/llgo/debugtest/ACCEPTANCE.md +++ b/cmd/llgo/debugtest/ACCEPTANCE.md @@ -60,8 +60,14 @@ Platform-specific limits remain explicit: section symbolication but does not get Go runtime presentation. - The current Asyncify-transformed browser runtime is accepted by Go's DWARF reader and Chrome, but LLVM's final `llvm-dwarfdump --verify` still reports - overlapping/range-containment diagnostics. This is a final-artifact blocker, - not a condition that the acceptance lane may suppress. + overlapping/range-containment diagnostics. With Emscripten 4.0.21 and + Binaryen 125, relinking the exact same objects without Asyncify passes the + verifier across all compile units; Asyncify alone introduces invalid ranges + in both LLGo and Emscripten C-library DIEs. This matches Binaryen issue + [#6406](https://github.com/WebAssembly/binaryen/issues/6406). A source map + can recover source lines but cannot repair Wasm-local variable locations, so + it is not a substitute for this gate. This is a final-artifact blocker, not a + condition that the acceptance lane may suppress. Panic/trap source stops, optimized inline stepping, Go/host boundary frames, and clean final DWARF after every LTO/post-link transform remain required diff --git a/cmd/llgo/lldbtest/README.md b/cmd/llgo/lldbtest/README.md index ae63ed5f49..5de80d4411 100644 --- a/cmd/llgo/lldbtest/README.md +++ b/cmd/llgo/lldbtest/README.md @@ -6,14 +6,12 @@ llgo build -O0 -ldflags=-w=false -o cl/_testdata/debug/out ./cl/_testdata/debug ``` -LLGo temporarily omits DWARF when `-w` is absent because the current debug -information path is not yet safe for broad use. The native executable build -above explicitly uses `-ldflags=-w=false` to enable DWARF. Use -`-ldflags=-w` to explicitly omit it. `-O0` is recommended for the most -complete local variable inspection in LLDB. The former `LLGO_DEBUG` and -`LLGO_DEBUG_SYMBOLS` environment variables are no longer read. This uses -LLGo's existing runnable DWARF path; improving its metadata quality and making -it optimization-independent are separate follow-up work. +LLGo linked builds preserve DWARF by default, matching `cmd/link`. The native +fixture above explicitly uses `-ldflags=-w=false` so its requirement remains +self-documenting; use `-ldflags=-w` to omit DWARF. `-O0` is recommended for +the most complete local variable inspection in LLDB, while optimized builds +run the same LLVM pipeline whether or not DWARF is retained. The former +`LLGO_DEBUG` and `LLGO_DEBUG_SYMBOLS` environment variables are no longer read. LLGo currently marks compile units as `DW_LANG_C` because stock LLDB does not provide a Go language plugin and otherwise hides valid frame variables. diff --git a/doc/design/pclntab-packaging.md b/doc/design/pclntab-packaging.md index 8fea3325be..60a19fe79a 100644 --- a/doc/design/pclntab-packaging.md +++ b/doc/design/pclntab-packaging.md @@ -25,43 +25,33 @@ without `-target` on Darwin/Linux amd64/arm64. `ModeGen`, `c-archive`, The policy is deliberately separate from Go's `-ldflags=-s` and `-w`: -- LLGo temporarily omits DWARF by default because the existing DI path is not - yet safe for broad use. `-w=false` explicitly enables it; `-w` explicitly - omits it. +- Linked LLGo builds preserve DWARF by default, matching `cmd/link`. + `-w=false` explicitly preserves it and `-w` explicitly omits it. Darwin + `c-shared` keeps the platform-specific `cmd/link` default of omitting DWARF. - `-s` records native symbol-table omission intent and implies `-w` unless `-w` is explicitly set; the explicit `-w` value wins regardless of argument order. Native symbol-table deletion remains a later phase. - `-pclntab` controls LLGo runtime symbolization metadata. -The explicit `-w` semantics above are Go-compatible, but the temporary -DWARF-free default is not: Go behaves as if `-w=false` when the flag is absent. -LLGo currently behaves as if `-w=true`; explicit `-w=false` overrides that -default. Once the dedicated DI fixes make broad DWARF use safe, the default can -be changed without changing flag parsing or backend precedence. - `ModeGen` remains DWARF-free by default and emits DWARF only when `-w=false` -is explicitly requested and supported. -Effective DWARF omission (`-w`, or bare `-s`) is rejected for `c-archive` and -`c-shared`. Fixed targets that always omit DWARF reject `-w=false`, while -backends without an omit capability reject `-w`. Because native `-s` -stripping is not implemented yet, `-s -w=false` does not strip shared or -archive outputs. +is explicitly requested and supported. Target builds derive `embedded`, +`external`, or host-side DWARF packaging from explicit policy and linker +capabilities. A backend that cannot retain DWARF rejects an explicit +`-w=false`; a backend that cannot omit retained input DWARF rejects `-w`. +Because native `-s` stripping is not implemented yet, `-s -w=false` records +the symbol-table intent without deleting native symbols. The former `LLGO_DEBUG` and `LLGO_DEBUG_SYMBOLS` environment switches are not part of this policy. Use `-w=false` to retain DWARF, `-w` to omit it, and `-O0` when a debugger-friendly unoptimized build is required. -This change deliberately reuses LLGo's existing runnable DWARF path; it does -not repair or redesign the debug metadata implementation. That path currently -disables LLGo's LLVM and C ABI optimization steps while emitting DWARF, so -`-w` can still affect optimization in addition to artifact metadata. Go's -optimization-independent DWARF behavior requires a separate implementation -PR and is explicitly out of scope here. - -Broad CI uses the DWARF-free default until the corresponding DI limitations -are fixed. Targeted native integration, the debug IR fixture, and the full -LLDB suite explicitly select `-w=false` to protect the currently supported -runnable path. +DWARF no longer changes whether LLGo runs its normal LLVM optimization +pipeline or C ABI lowering. Debug references are treated as metadata by SSA +compatibility rewrites, C ABI lowering preserves variable homes, and the +DWARF optimized marker follows the selected optimization level. Broad CI +therefore exercises the same DWARF-preserving default as ordinary linked +builds; focused debugger fixtures may still select `-w=false` explicitly to +make their artifact requirement self-documenting. Consequently all combinations are meaningful. For example, `-ldflags=-s -pclntab=embedded` keeps Go-compatible runtime symbolization, @@ -135,11 +125,13 @@ a later native-strip mutation is added. sites are not produced. `-pclntab=embedded` keeps the existing post-link prebuilt-table rewrite. -Darwin embedded builds that emit DWARF keep the historical site-free path so -inline PC anchors do not disturb LLDB lexical scopes. Linux retains sites with -DWARF because most Go symbols are intentionally absent from ELF `.dynsym`, so -`dlsym` alone cannot reconstruct every function entry. External mode needs -the final-PC sites on both platforms before it can construct the sidecar. +Darwin embedded builds that emit DWARF retain PC-line sites, emitted without a +debug location, but suppress function-entry and closure-stub address sites so +inline anchors do not make LLDB expose inner lexical scopes too early. Linux +retains both classes because most Go symbols are intentionally absent from ELF +`.dynsym`, so `dlsym` alone cannot reconstruct every function entry. External +mode needs the final-address sites on both platforms before it can construct +the sidecar. ## Sidecar identity and addressing diff --git a/internal/build/build.go b/internal/build/build.go index 1388f55f65..39a43279bd 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -182,10 +182,7 @@ type Config struct { // DebugArtifactModeSet distinguishes an explicit command request from the // effective mode derived from -w and the current build default. DebugArtifactModeSet bool - // 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. @@ -304,14 +301,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 } @@ -474,12 +470,10 @@ func Build(inv Invocation) ([]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} @@ -593,18 +587,14 @@ func Build(inv Invocation) ([]Package, error) { } buildMode := ssaBuildMode - cabiOptimize := true - passOpt := true - if emitDebugInfo || mode == ModeGen { - passOpt = false - } + passOpt := shouldRunLLVMPasses(mode) if emitDebugInfo { buildMode |= ssa.GlobalDebug - cabiOptimize = false } 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) @@ -622,7 +612,7 @@ func Build(inv Invocation) ([]Package, error) { crossCompile: export, commands: commands, frontendOptions: frontendOptions, - cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize), + cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, true), } defer ctx.closePackageMetas() @@ -1276,7 +1266,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 { @@ -2534,6 +2524,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 a69cd0a9db..6bf391a4ad 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -752,8 +752,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) { @@ -779,8 +781,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) { @@ -800,6 +804,56 @@ func TestLinkOptionsControlDarwinDebugSymbols(t *testing.T) { } } +func TestDarwinDWARFCacheUsesStableArchivePaths(t *testing.T) { + if runtime.GOOS != "darwin" { + t.Skip("Mach-O debug-map integration test") + } + + cacheDir := t.TempDir() + oldCacheRootFunc := cacheRootFunc + cacheRootFunc = func() string { return cacheDir } + t.Cleanup(func() { cacheRootFunc = oldCacheRootFunc }) + + binPath := filepath.Join(t.TempDir(), "ldflagsstrip") + buildCachedArchivePaths := func() []string { + cfg := &Config{ + Mode: ModeBuild, + OutFile: binPath, + LinkOptions: LinkOptions{DWARF: DWARFPreserve}, + } + if _, err := Do([]string{"./testdata/ldflagsstrip"}, cfg); err != nil { + t.Fatalf("ModeBuild with DWARF failed: %v", err) + } + f, err := macho.Open(binPath) + if err != nil { + t.Fatal(err) + } + defer f.Close() + if f.Symtab == nil { + t.Fatal("Mach-O has no symbol table") + } + + const nOSO = 0x66 + cachePrefix := filepath.Join(cacheDir, cacheBuildDirName) + string(os.PathSeparator) + var paths []string + for _, sym := range f.Symtab.Syms { + if sym.Type == nOSO && strings.HasPrefix(sym.Name, cachePrefix) { + paths = append(paths, sym.Name) + } + } + return paths + } + + coldPaths := buildCachedArchivePaths() + warmPaths := buildCachedArchivePaths() + if len(coldPaths) == 0 { + t.Fatal("cold build has no cache-backed N_OSO paths") + } + if !slices.Equal(coldPaths, warmPaths) { + t.Fatalf("cold and warm N_OSO paths differ:\ncold: %q\nwarm: %q", coldPaths, warmPaths) + } +} + func machoHasStabs(t *testing.T, path string) bool { t.Helper() f, err := macho.Open(path) diff --git a/internal/build/collect.go b/internal/build/collect.go index 66da887c21..6f95d2bcda 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -523,6 +523,9 @@ func (c *context) saveToCache(pkg *aPackage) error { return err } + // Link a cache miss from the same stable archive path used by a cache hit. + // Darwin records this path in its N_OSO debug map. + pkg.ArchiveFile = paths.Archive return nil } diff --git a/internal/build/collect_test.go b/internal/build/collect_test.go index 94b2601c57..a6425bfc42 100644 --- a/internal/build/collect_test.go +++ b/internal/build/collect_test.go @@ -1001,6 +1001,9 @@ func TestSaveToCache_Success(t *testing.T) { // Check cache was created cm := ctx.ensureCacheManager() paths := cm.PackagePaths("arm64-apple-darwin", "example.com/lib", "def456") + if pkg.ArchiveFile != paths.Archive { + t.Fatalf("ArchiveFile = %q, want stable cache path %q", pkg.ArchiveFile, paths.Archive) + } // Check manifest contains original content and metadata in Package section content, err := readManifest(paths.Manifest) diff --git a/internal/build/debug_artifact.go b/internal/build/debug_artifact.go index e52c9ff649..877313db5e 100644 --- a/internal/build/debug_artifact.go +++ b/internal/build/debug_artifact.go @@ -75,9 +75,9 @@ func isWasmDebugTarget(conf *Config, target *crosscompile.Export) bool { } // resolveDebugArtifactMode validates an explicit artifact request, translates -// it into typed -w intent, and records the effective packaging mode. The -// existing safe DWARF default remains authoritative when no mode was supplied; -// restoring Go's default is owned by the optimized-DWARF dependency chain. +// it into typed -w intent, and records the effective packaging mode. When no +// mode is supplied, linked builds follow Go's DWARF-preserving default and +// target builds select host or embedded packaging from target capabilities. func resolveDebugArtifactMode(conf *Config, target *crosscompile.Export) error { if !conf.DebugArtifactMode.IsValid() { return fmt.Errorf("invalid debug artifact mode %d", conf.DebugArtifactMode) diff --git a/internal/build/debug_artifact_test.go b/internal/build/debug_artifact_test.go index a6f5f5e487..c4f11e4fcd 100644 --- a/internal/build/debug_artifact_test.go +++ b/internal/build/debug_artifact_test.go @@ -46,11 +46,11 @@ func TestResolveDebugArtifactMode(t *testing.T) { wantDWARF DWARFMode wantErr bool }{ - {name: "safe default", conf: Config{Mode: ModeBuild, BuildMode: BuildModeExe, OmitDWARFByDefault: true}, target: native, wantMode: DebugArtifactNone}, - {name: "native explicit preserve overrides safe default", conf: Config{Mode: ModeBuild, BuildMode: BuildModeExe, OmitDWARFByDefault: true, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, target: native, wantMode: DebugArtifactEmbedded, wantDWARF: DWARFPreserve}, + {name: "s implies no artifact", conf: Config{Mode: ModeBuild, BuildMode: BuildModeExe, LinkOptions: LinkOptions{OmitSymbolTable: true}}, target: native, wantMode: DebugArtifactNone}, + {name: "native explicit preserve overrides s", conf: Config{Mode: ModeBuild, BuildMode: BuildModeExe, LinkOptions: LinkOptions{OmitSymbolTable: true, DWARF: DWARFPreserve}}, target: native, wantMode: DebugArtifactEmbedded, wantDWARF: DWARFPreserve}, {name: "native default with DWARF", conf: base(), target: native, wantMode: DebugArtifactEmbedded}, {name: "fixed default with DWARF", conf: Config{Mode: ModeBuild, BuildMode: BuildModeExe, Target: "rp2040"}, target: fixed, wantMode: DebugArtifactHost}, - {name: "fixed explicit preserve overrides safe default", conf: Config{Mode: ModeBuild, BuildMode: BuildModeExe, Target: "rp2040", OmitDWARFByDefault: true, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, target: fixed, wantMode: DebugArtifactHost, wantDWARF: DWARFPreserve}, + {name: "fixed explicit preserve overrides s", conf: Config{Mode: ModeBuild, BuildMode: BuildModeExe, Target: "rp2040", LinkOptions: LinkOptions{OmitSymbolTable: true, DWARF: DWARFPreserve}}, target: fixed, wantMode: DebugArtifactHost, wantDWARF: DWARFPreserve}, {name: "wasm default with DWARF", conf: Config{Mode: ModeBuild, BuildMode: BuildModeExe, Target: "wasi", Goarch: "wasm"}, target: wasm, wantMode: DebugArtifactEmbedded}, {name: "explicit none", conf: Config{Mode: ModeBuild, BuildMode: BuildModeExe, DebugArtifactMode: DebugArtifactNone, DebugArtifactModeSet: true}, target: native, wantMode: DebugArtifactNone, wantDWARF: DWARFOmit}, {name: "none conflicts preserve", conf: Config{Mode: ModeBuild, BuildMode: BuildModeExe, DebugArtifactMode: DebugArtifactNone, DebugArtifactModeSet: true, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, target: native, wantErr: true}, diff --git a/internal/build/funcinfo_table.go b/internal/build/funcinfo_table.go index b36aa29654..6e51282a72 100644 --- a/internal/build/funcinfo_table.go +++ b/internal/build/funcinfo_table.go @@ -483,7 +483,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) @@ -495,10 +495,11 @@ 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) - emitRuntimeFuncInfoSites(mod, ctx.prog.PointerSize(), machOSites, emitSites && len(pcLineValues) != 0, emitEntrySites, emitStubSites && len(stubRecords) != 0) + emitPCLineSites := shouldEmitRuntimePCLineSites(ctx) + emitAddressSites := shouldEmitRuntimeAddressSites(ctx) + emitEntrySites := emitAddressSites && len(encoded.Records) != 0 + emitStubSites := emitAddressSites + emitRuntimeFuncInfoSites(mod, ctx.prog.PointerSize(), machOSites, emitPCLineSites && len(pcLineValues) != 0, emitEntrySites, emitStubSites && len(stubRecords) != 0) if emitEntrySites { startName, endName := entrySiteSectionInfo.boundary(machOSites) entryStart := llvm.AddGlobal(mod, funcEntryRecordType, startName) @@ -714,10 +715,11 @@ func emitExternalFuncInfoTable(ctx *context, mod llvm.Module, records []funcInfo used.SetSection("llvm.metadata") machO := shouldEmitRuntimeMachOSites(ctx) - emitSites := shouldEmitRuntimeSites(ctx) - emitPCSites := emitSites && len(encoded.PCLines) != 0 - emitEntrySites := shouldEmitRuntimeEntryELFSites(ctx) && len(encoded.Records) != 0 - emitStubSites := shouldEmitRuntimeStubELFSites(ctx) && len(stubRecords) != 0 + emitPCLineSites := shouldEmitRuntimePCLineSites(ctx) + emitAddressSites := shouldEmitRuntimeAddressSites(ctx) + emitPCSites := emitPCLineSites && len(encoded.PCLines) != 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) @@ -756,29 +758,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. @@ -853,7 +860,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() @@ -933,7 +940,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 d606f0549f..9c7584080e 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 8d1370920a..68220b1c85 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 d5ec1254b1..735bb1f630 100644 --- a/internal/build/link_options_test.go +++ b/internal/build/link_options_test.go @@ -42,9 +42,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"}}, @@ -92,13 +91,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: unavailableDebugInfo(), 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) { @@ -117,9 +116,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}, @@ -146,10 +146,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..8bf84b9acf 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 { +// enabled after the global funcinfo and site controls are applied. +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 2f6f939906..b53115ff10 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..3a577bb12e 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,13 +208,39 @@ func moveAssignDepsAfterRecv(b *ssa.BasicBlock, roots []ssa.Value, recv ssa.Valu return true } -func moveWouldBreakSSA(instrs []ssa.Instruction, move map[int]struct{}, recvIdx int) bool { +func movedValues(instrs []ssa.Instruction, move map[int]struct{}) map[ssa.Value]struct{} { 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{}{} } } + return moved +} + +func includeDebugRefsForMovedValues(instrs []ssa.Instruction, move map[int]struct{}, recvIdx int) { + moved := movedValues(instrs, move) + // DebugRef is metadata, not an ssa.Value, so adding one cannot introduce + // another value whose DebugRefs would require a second scan. + for i := 0; i < recvIdx && 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 := movedValues(instrs, move) for i := 0; i <= recvIdx && i < len(instrs); i++ { if _, moving := move[i]; moving { continue @@ -302,11 +331,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 +349,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 +422,34 @@ 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) { +// moveInstrsAfter reinserts selected instructions as an ordered group directly +// after anchor, preserving the relative order of all other instructions. It +// returns instrs unchanged when moving is empty or anchor is nil, absent, or +// itself selected for moving. +func moveInstrsAfter(instrs []ssa.Instruction, moving map[ssa.Instruction]struct{}, anchor ssa.Instruction) []ssa.Instruction { + if len(moving) == 0 || anchor == nil { return instrs } - if to < 0 { - to = 0 - } - if to > len(instrs) { - to = len(instrs) - } - if from == to || from+1 == to { + if _, movingAnchor := moving[anchor]; movingAnchor { 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..89adce6a82 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,168 @@ 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("moving-anchor", func(t *testing.T) { + moving := map[ssa.Instruction]struct{}{ + instrs[0]: {}, + instrs[1]: {}, + } + assertOrder(t, moveInstrsAfter(instrs, moving, instrs[1]), 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 +294,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/internal/cabi/cabi.go b/internal/cabi/cabi.go index 2e37b6bc99..740f3d0fa0 100644 --- a/internal/cabi/cabi.go +++ b/internal/cabi/cabi.go @@ -411,6 +411,7 @@ func (p *Transformer) transformFuncBody(m llvm.Module, ctx llvm.Context, info *F b.SetInsertPointBefore(nfn.EntryBasicBlock().FirstInstruction()) params := nfn.Params() + preserveDebugHomes := !nfn.Subprogram().IsNil() index := 0 if info.Return.Kind == AttrPointer { index++ @@ -439,7 +440,7 @@ func (p *Transformer) transformFuncBody(m llvm.Module, ctx llvm.Context, info *F nv = b.CreateLoad(ti.Type, params[index], "") // replace %0 to %2 if p.optimize { - replaceAllocaInstrs(fn.Param(i), params[index]) + replaceAllocaInstrs(fn.Param(i), params[index], preserveDebugHomes) } case AttrWidthType: iptr := llvm.CreateAlloca(b, ti.Type1) @@ -447,7 +448,7 @@ func (p *Transformer) transformFuncBody(m llvm.Module, ctx llvm.Context, info *F ptr := b.CreateBitCast(iptr, llvm.PointerType(ti.Type, 0), "") nv = b.CreateLoad(ti.Type, ptr, "") if p.optimize { - replaceAllocaInstrs(fn.Param(i), ptr) + replaceAllocaInstrs(fn.Param(i), ptr, preserveDebugHomes) } case AttrWidthType2: typ := ctx.StructType([]llvm.Type{ti.Type1, ti.Type2}, false) @@ -458,7 +459,7 @@ func (p *Transformer) transformFuncBody(m llvm.Module, ctx llvm.Context, info *F ptr := b.CreateBitCast(iptr, llvm.PointerType(ti.Type, 0), "") nv = b.CreateLoad(ti.Type, ptr, "") if p.optimize { - replaceAllocaInstrs(fn.Param(i), ptr) + replaceAllocaInstrs(fn.Param(i), ptr, preserveDebugHomes) } case AttrExtract: nsubs := ti.Type.StructElementTypesCount() @@ -765,7 +766,15 @@ func (p *Transformer) callMemcpy(_ llvm.Module, ctx llvm.Context, b llvm.Builder }, "") } -func replaceAllocaInstrs(param llvm.Value, nv llvm.Value) { +func replaceAllocaInstrs(param llvm.Value, nv llvm.Value, preserveDebugHome bool) { + // A debug home must remain the single source of truth for both reads and + // writes. Moving only executable uses would leave the declared location + // stale after an assignment, while moving dbg.declare to a byval parameter + // makes LLVM emit an extra dereference for the aggregate. + if preserveDebugHome { + return + } + u := param.FirstUse() var storeInstrs []llvm.Value for !u.IsNil() { @@ -776,27 +785,26 @@ func replaceAllocaInstrs(param llvm.Value, nv llvm.Value) { } for _, instr := range storeInstrs { if alloc := instr.Operand(1).IsAAllocaInst(); !alloc.IsNil() { - skips := make(map[llvm.Value]bool) + type operandUse struct { + instr llvm.Value + index int + } + var preserved []operandUse next := llvm.NextInstruction(alloc) for !next.IsNil() && next != instr { - skips[next] = true - next = llvm.NextInstruction(next) - } - var uses []llvm.Value - u := alloc.FirstUse() - for !u.IsNil() { - if v := u.User(); !skips[v] { - uses = append(uses, v) - } - u = u.NextUse() - } - for _, use := range uses { - n := use.OperandsCount() - for i := 0; i < n; i++ { - if use.Operand(i) == alloc { - use.SetOperand(i, nv) + for i := 0; i < next.OperandsCount(); i++ { + if next.Operand(i) == alloc { + preserved = append(preserved, operandUse{next, i}) } } + next = llvm.NextInstruction(next) + } + + // RAUW updates instruction operands and LLVM debug records. Restore + // setup instructions before the parameter store to the original alloca. + alloc.ReplaceAllUsesWith(nv) + for _, use := range preserved { + use.instr.SetOperand(use.index, alloc) } } } diff --git a/internal/cabi/cabi_debug_test.go b/internal/cabi/cabi_debug_test.go new file mode 100644 index 0000000000..c26cdf679b --- /dev/null +++ b/internal/cabi/cabi_debug_test.go @@ -0,0 +1,87 @@ +//go:build !llgo + +package cabi + +import ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/debuginfo" + "github.com/xgo-dev/llvm" +) + +func TestReplaceAllocaInstrsPreservesInitializedDebugHome(t *testing.T) { + ctx := llvm.NewContext() + defer ctx.Dispose() + mod := ctx.NewModule("cabi-debug") + defer mod.Dispose() + + di := debuginfo.New(mod, debuginfo.Config{Producer: "LLGo"}) + cu := di.CompileUnit("cabi.go", "/src") + file := di.File("/src/cabi.go") + intType := di.CreateBasicType(llvm.DIBasicType{Name: "int", SizeInBits: 64, Encoding: 5}) + subroutine := di.CreateSubroutineType(llvm.DISubroutineType{File: file}) + subprogram := di.CreateFunction(cu, llvm.DIFunction{ + Name: "cabi", + LinkageName: "cabi", + File: file, + Line: 1, + ScopeLine: 1, + Type: subroutine, + IsDefinition: true, + }) + variable := di.CreateAutoVariable(subprogram, llvm.DIAutoVariable{ + Name: "value", + File: file, + Line: 1, + Type: intType, + AlwaysPreserve: true, + }) + + int64Type := ctx.Int64Type() + fnType := llvm.FunctionType(ctx.VoidType(), []llvm.Type{int64Type, llvm.PointerType(int64Type, 0)}, false) + fn := llvm.AddFunction(mod, "cabi", fnType) + fn.SetSubprogram(subprogram) + param := fn.Param(0) + param.SetName("param") + replacement := fn.Param(1) + replacement.SetName("replacement") + builder := ctx.NewBuilder() + defer builder.Dispose() + block := llvm.AddBasicBlock(fn, "entry") + builder.SetInsertPointAtEnd(block) + home := builder.CreateAlloca(int64Type, "home") + di.InsertDeclareAtEnd(home, variable, di.CreateExpression(nil), llvm.DebugLoc{Line: 1, Scope: subprogram}, block) + zero := llvm.ConstInt(ctx.Int32Type(), 0, false) + setup := builder.CreateGEP(int64Type, home, []llvm.Value{zero}, "setup") + builder.CreateStore(param, home) + loaded := builder.CreateLoad(int64Type, home, "loaded") + builder.CreateStore(loaded, replacement) + builder.CreateStore(llvm.ConstInt(int64Type, 42, false), home) + updated := builder.CreateLoad(int64Type, home, "updated") + builder.CreateStore(updated, replacement) + builder.CreateRetVoid() + + replaceAllocaInstrs(param, replacement, true) + di.Finalize() + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatalf("rewritten module is invalid: %v\n%s", err, mod.String()) + } + if setup.Operand(0) != home { + t.Fatalf("setup operand was rewritten to the ABI home:\n%s", mod.String()) + } + ir := mod.String() + if !strings.Contains(ir, "#dbg_declare(ptr %home") { + t.Fatalf("dbg.declare did not retain the local home:\n%s", ir) + } + if !strings.Contains(ir, "store i64 %param, ptr %home") { + t.Fatalf("debug home is not initialized by the parameter store:\n%s", ir) + } + if !strings.Contains(ir, "%loaded = load i64, ptr %home") { + t.Fatalf("debug home is not authoritative for executable uses:\n%s", ir) + } + if !strings.Contains(ir, "store i64 42, ptr %home") || + !strings.Contains(ir, "%updated = load i64, ptr %home") { + t.Fatalf("debug home does not track later assignments:\n%s", ir) + } +} diff --git a/ssa/di_debug_test.go b/ssa/di_debug_test.go index 861ab7a23d..7980986179 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" ) @@ -180,6 +181,201 @@ func TestDIGlobalIgnoresStorageLessFrontendVariable(t *testing.T) { builder.DIGlobal(pyVarExpr(Nil, "attribute"), "module.attribute", token.Position{}) } +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 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 6f476c9eac..93781719fa 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 db41bae4ab..aa53e79483 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -381,6 +381,12 @@ func (p Program) EnableLTOPluginMarkers(enable bool) { p.enableLTOPluginMarker = enable } +// SetDebugInfoOptimized records whether DWARF should mark generated code as +// optimized. It 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/large_array_return_test.go b/test/go/large_array_return_test.go index 0cb38c2cc7..b06048e152 100644 --- a/test/go/large_array_return_test.go +++ b/test/go/large_array_return_test.go @@ -83,6 +83,15 @@ func largeArrayLLGo(t *testing.T) string { } func TestLargeArrayReturnAllABIModes(t *testing.T) { + testLargeArrayReturn(t, nil, 0, 1, 2) +} + +func TestLargeArrayReturnDWARF(t *testing.T) { + testLargeArrayReturn(t, []string{"-ldflags=-w=false"}, 2) +} + +func testLargeArrayReturn(t *testing.T, buildFlags []string, modes ...int) { + t.Helper() dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module largearray\n\ngo 1.21\n"), 0o644); err != nil { t.Fatal(err) @@ -91,9 +100,12 @@ func TestLargeArrayReturnAllABIModes(t *testing.T) { t.Fatal(err) } bin := largeArrayLLGo(t) - for mode := 0; mode <= 2; mode++ { + for _, mode := range modes { t.Run(fmt.Sprintf("abi%d", mode), func(t *testing.T) { - cmd := exec.Command(bin, "run", fmt.Sprintf("-abi=%d", mode), ".") + args := []string{"run", fmt.Sprintf("-abi=%d", mode)} + args = append(args, buildFlags...) + args = append(args, ".") + cmd := exec.Command(bin, args...) cmd.Dir = dir out, err := cmd.CombinedOutput() if err != nil { diff --git a/test/llgo/dwarf_semantics_acceptance_test.go b/test/llgo/dwarf_semantics_acceptance_test.go new file mode 100644 index 0000000000..19ca4db452 --- /dev/null +++ b/test/llgo/dwarf_semantics_acceptance_test.go @@ -0,0 +1,80 @@ +//go:build !llgo + +package llgotest + +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 := findRepoRoot(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) + } +} + +func findRepoRoot(t *testing.T) string { + t.Helper() + dir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + t.Fatal("repo root not found") + } + dir = parent + } +}