From f33786d4fa86df07caf47c6fbf1ca61725f9a27b Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 22 Jul 2026 06:30:32 +0800 Subject: [PATCH 01/25] fix(build): keep DebugRefs with reordered return loads ssa.GlobalDebug inserts metadata-only DebugRefs between a return load and its Return. Treating those references as executable users prevents the existing gc-compatible order repair from running, while moving only the load would invalidate SSA order. Move each related DebugRef with the load. This preserves LLGo compatibility for the behavior discussed in golang/go#77938; it does not make the unspecified Go expression order normative. --- internal/build/ssa_order_fix.go | 68 ++++----- internal/build/ssa_order_fix_test.go | 141 ++++++++++++++++++- test/llgo/dwarf_semantics_acceptance_test.go | 80 +++++++++++ 3 files changed, 249 insertions(+), 40 deletions(-) create mode 100644 test/llgo/dwarf_semantics_acceptance_test.go diff --git a/internal/build/ssa_order_fix.go b/internal/build/ssa_order_fix.go index 57235740c4..94a4958b87 100644 --- a/internal/build/ssa_order_fix.go +++ b/internal/build/ssa_order_fix.go @@ -302,11 +302,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 +320,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 +393,29 @@ func valueDependsOn(v, target ssa.Value, seen map[ssa.Value]struct{}) bool { return false } -// moveInstr moves instrs[from] to position to (like inserting before to), -// preserving relative order of other elements. -func moveInstr(instrs []ssa.Instruction, from, to int) []ssa.Instruction { - if from < 0 || from >= len(instrs) { +// moveInstrsAfter reinserts selected instructions as an ordered group directly +// after anchor, preserving the relative order of all other instructions. +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 { - 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..0cd4c1b514 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" ) @@ -115,7 +116,145 @@ func f() { } } +func TestFixSSAOrderReturnLoadWithDebugRefs(t *testing.T) { + const src = `package p +type value struct { n int } +func (v *value) mutate() bool { v.n = 1; return true } +func f() (value, bool) { + var v value + return v, v.mutate() +}` + base := ssa.SanityCheckFunctions | ssa.InstantiateGenerics + for _, test := range []struct { + name string + mode ssa.BuilderMode + }{ + {name: "default", mode: base}, + {name: "global-debug", mode: base | ssa.GlobalDebug}, + } { + t.Run(test.name, func(t *testing.T) { + fn := buildSSAOrderTestPackageMode(t, src, test.mode) + checkReturnLoadAfterMutation(t, fn, "mutate") + }) + } +} + +func TestFixSSAOrderCryptoX509ParseOID(t *testing.T) { + fset := token.NewFileSet() + loaded, err := packages.Load(&packages.Config{ + Mode: packages.LoadSyntax, + Fset: fset, + }, "crypto/x509") + if err != nil { + t.Fatal(err) + } + if packages.PrintErrors(loaded) != 0 || len(loaded) != 1 { + t.Fatal("failed to load crypto/x509") + } + mode := ssa.SanityCheckFunctions | ssa.InstantiateGenerics | ssa.GlobalDebug + prog, ssaPackages := ssautil.Packages(loaded, mode) + prog.Build() + pkg := ssaPackages[0] + fixSSAOrder(pkg, loaded[0].Syntax) + checkReturnLoadAfterMutation(t, pkg.Func("ParseOID"), "unmarshalOIDText") +} + +func TestMoveInstrsAfter(t *testing.T) { + const src = `package p +func f() { + println(1) + println(2) + println(3) +}` + fn := buildSSAOrderTestPackage(t, src) + instrs := fn.Blocks[0].Instrs + if len(instrs) < 4 { + t.Fatalf("instructions = %d, want at least 4", len(instrs)) + } + + assertOrder := func(t *testing.T, got, want []ssa.Instruction) { + t.Helper() + if len(got) != len(want) { + t.Fatalf("instruction count = %d, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("instruction %d = %v, want %v", i, got[i], want[i]) + } + } + } + + t.Run("empty", func(t *testing.T) { + assertOrder(t, moveInstrsAfter(instrs, nil, instrs[1]), instrs) + }) + t.Run("nil-anchor", func(t *testing.T) { + moving := map[ssa.Instruction]struct{}{instrs[0]: {}} + assertOrder(t, moveInstrsAfter(instrs, moving, nil), instrs) + }) + t.Run("missing-anchor", func(t *testing.T) { + moving := map[ssa.Instruction]struct{}{instrs[0]: {}} + assertOrder(t, moveInstrsAfter(instrs, moving, &ssa.Return{}), instrs) + }) + t.Run("stable", func(t *testing.T) { + moving := map[ssa.Instruction]struct{}{ + instrs[0]: {}, + instrs[2]: {}, + } + want := []ssa.Instruction{instrs[1], instrs[0], instrs[2]} + want = append(want, instrs[3:]...) + assertOrder(t, moveInstrsAfter(instrs, moving, instrs[1]), want) + }) +} + +func checkReturnLoadAfterMutation(t *testing.T, fn *ssa.Function, mutation string) { + t.Helper() + var ret *ssa.Return + var mutationCall ssa.CallInstruction + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + if candidate, ok := instr.(*ssa.Return); ok { + ret = candidate + } + if call, ok := instr.(ssa.CallInstruction); ok { + callee := call.Common().StaticCallee() + if callee != nil && callee.Name() == mutation { + mutationCall = call + } + } + } + } + if ret == nil || len(ret.Results) != 2 || mutationCall == nil { + t.Fatalf("unexpected return instruction: %v", ret) + } + callInstr := mutationCall.(ssa.Instruction) + callIdx := indexOfInstr(callInstr.Block().Instrs, callInstr) + loadIdx := -1 + debugRefIdx := -1 + var resultLoad *ssa.UnOp + for i, instr := range callInstr.Block().Instrs { + if load, ok := instr.(*ssa.UnOp); ok && load.Op == token.MUL { + if alloc, ok := load.X.(*ssa.Alloc); ok && callUsesValue(mutationCall, alloc) { + loadIdx = i + resultLoad = load + } + } + if ref, ok := instr.(*ssa.DebugRef); ok && resultLoad != nil && instrUsesValue(ref, resultLoad) { + debugRefIdx = i + } + } + if callIdx < 0 || loadIdx <= callIdx { + t.Fatalf("return load index = %d, mutation call index = %d; want load after call\n%s", loadIdx, callIdx, fn) + } + if debugRefIdx >= 0 && debugRefIdx <= loadIdx { + t.Fatalf("DebugRef index = %d, load index = %d; want DebugRef after its load\n%s", debugRefIdx, loadIdx, fn) + } +} + func buildSSAOrderTestPackage(t *testing.T, src string) *ssa.Function { + return buildSSAOrderTestPackageMode(t, src, ssa.SanityCheckFunctions|ssa.InstantiateGenerics) +} + +func 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 +268,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/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 + } +} From b7f864f588cdba9e74e366236ba838acb749a868 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 28 Jul 2026 21:18:29 +0800 Subject: [PATCH 02/25] docs(build): document instruction move fallback --- internal/build/ssa_order_fix.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/build/ssa_order_fix.go b/internal/build/ssa_order_fix.go index 94a4958b87..0bcb785552 100644 --- a/internal/build/ssa_order_fix.go +++ b/internal/build/ssa_order_fix.go @@ -394,7 +394,8 @@ func valueDependsOn(v, target ssa.Value, seen map[ssa.Value]struct{}) bool { } // moveInstrsAfter reinserts selected instructions as an ordered group directly -// after anchor, preserving the relative order of all other instructions. +// after anchor, preserving the relative order of all other instructions. It +// returns instrs unchanged when moving is empty or anchor is nil or absent. func moveInstrsAfter(instrs []ssa.Instruction, moving map[ssa.Instruction]struct{}, anchor ssa.Instruction) []ssa.Instruction { if len(moving) == 0 || anchor == nil { return instrs From 1644341218c0379ae327da151740e9b629d76624 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 25 Jul 2026 12:38:37 +0800 Subject: [PATCH 03/25] fix(build): preserve DebugRefs in select order repair The single-case select repair moves receive-assignment LHS dependencies after the receive, as required by select evaluation order. Under ssa.GlobalDebug, metadata-only DebugRefs looked like pre-receive users and blocked that repair. Include those DebugRefs in the same stable move set so default and GlobalDebug SSA retain identical runtime ordering. --- internal/build/ssa_order_fix.go | 31 +++++++++++++++++- internal/build/ssa_order_fix_test.go | 49 +++++++++++++++++++--------- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/internal/build/ssa_order_fix.go b/internal/build/ssa_order_fix.go index 0bcb785552..e063f85808 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 diff --git a/internal/build/ssa_order_fix_test.go b/internal/build/ssa_order_fix_test.go index 0cd4c1b514..33ff26041f 100644 --- a/internal/build/ssa_order_fix_test.go +++ b/internal/build/ssa_order_fix_test.go @@ -29,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) { @@ -67,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) { @@ -88,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) { @@ -254,6 +257,22 @@ 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() From ea9f0051085d26dcb388285bfad4d67a56837873 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 29 Jul 2026 23:50:26 +0800 Subject: [PATCH 04/25] fix(build): stop DebugRef scan before receive --- internal/build/ssa_order_fix.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/build/ssa_order_fix.go b/internal/build/ssa_order_fix.go index e063f85808..d8a8eb6452 100644 --- a/internal/build/ssa_order_fix.go +++ b/internal/build/ssa_order_fix.go @@ -222,7 +222,7 @@ func includeDebugRefsForMovedValues(instrs []ssa.Instruction, move map[int]struc 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++ { + for i := 0; i < recvIdx && i < len(instrs); i++ { if _, moving := move[i]; moving { continue } From cb22f32b7be23d23470732d3bc331b4d6dd19768 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 22 Jul 2026 05:42:13 +0800 Subject: [PATCH 05/25] build(debug): run LLVM passes with DWARF --- internal/build/build.go | 10 ++++++---- internal/build/optlevel_test.go | 11 +++++++++++ ssa/package.go | 6 ++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index 1388f55f65..9f750506e9 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -594,10 +594,7 @@ 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 @@ -605,6 +602,7 @@ func Build(inv Invocation) ([]Package, error) { if !IsOptimizeEnabled() { buildMode |= ssa.NaiveForm } + prog.SetDebugInfoOptimized(passOpt && conf.OptLevel != optlevel.O0) progSSA := ssa.NewProgram(initial[0].Fset, buildMode) patches := make(cl.Patches, len(altPkgPaths)) altSSAPkgs(progSSA, patches, altPkgs[1:], conf, verbose) @@ -2534,6 +2532,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/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/ssa/package.go b/ssa/package.go index db41bae4ab..181698e73c 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 the LLVM IR optimization pipeline runs. +// It only controls DWARF's optimized marker and never selects compiler passes. +func (p Program) SetDebugInfoOptimized(enable bool) { + p.debugInfoOptimized = enable +} + func (p Program) SetNoInterfaceMethod(fullName string) { p.noInterface[fullName] = none{} } From a3a231d8485b2f5607b98562830e1ed247915bbb Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 22 Jul 2026 23:19:05 +0800 Subject: [PATCH 06/25] fix(debug): ignore DebugRefs during static init --- cl/rewrite_internal_test.go | 74 ++++++++++++++++++++++++++++++++++++- cl/static_init.go | 16 ++++---- 2 files changed, 81 insertions(+), 9 deletions(-) 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 } From 6491c947f89062002fcf55cdcd30e1fac48a6f88 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 22 Jul 2026 07:05:54 +0800 Subject: [PATCH 07/25] fix(debug): preserve defer locations under LTO --- cl/compile_test.go | 17 +++++++++++++++++ ssa/di_debug_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ ssa/eh.go | 10 ++++++++-- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/cl/compile_test.go b/cl/compile_test.go index 2e61753a50..983990bae2 100644 --- a/cl/compile_test.go +++ b/cl/compile_test.go @@ -222,6 +222,15 @@ func TestRunAndTestFromTestlto(t *testing.T) { cltest.RunAndTestFromDir(t, "", "./_testlto", ignore, cltest.WithRunConfig(conf)) } +func TestRunAndTestFromTestltoDWARF(t *testing.T) { + t.Setenv("LLGO_BUILD_CACHE", "off") + conf := build.NewDefaultConf(build.ModeRun) + conf.LTO = lto.Full + conf.LinkOptions.DWARF = build.DWARFPreserve + cltest.RunAndTestFromDir(t, "reflectmk_runtime", "./_testlto", nil, + cltest.WithRunConfig(conf), cltest.WithIRCheck(false)) +} + var testltoSymbolChecks = []string{ "globaldce_interface_matrix", "globaldce_interface_slots", @@ -316,6 +325,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 diff --git a/ssa/di_debug_test.go b/ssa/di_debug_test.go index 861ab7a23d..0f65ef4300 100644 --- a/ssa/di_debug_test.go +++ b/ssa/di_debug_test.go @@ -180,6 +180,48 @@ 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() {} +`, 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) + + 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 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..e18149ebc3 100644 --- a/ssa/eh.go +++ b/ssa/eh.go @@ -136,8 +136,14 @@ 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() + if p.diFunc != nil { + loc := from.impl.GetCurrentDebugLocation() + if !loc.Scope.IsNil() { + b.impl.SetCurrentDebugLocation(loc.Line, loc.Col, loc.Scope, loc.InlinedAt) + } + } next = b.setBlockMoveLast(p.blks[0]) p.blks[0].last = next.last return @@ -197,7 +203,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) From 102550cf413bc0ee4484d0f6b71187cfd22339d5 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 22 Jul 2026 23:21:47 +0800 Subject: [PATCH 08/25] test(lto): cover DWARF-preserved string values --- cl/compile_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cl/compile_test.go b/cl/compile_test.go index 983990bae2..005f87f4ad 100644 --- a/cl/compile_test.go +++ b/cl/compile_test.go @@ -254,6 +254,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") @@ -397,6 +404,15 @@ 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 + cltest.BuildAndCheckSymbolsFromDir(t, "", "./_testlto", testltoLTOPluginDWARFTests, + cltest.WithRunConfig(buildConf), + ) +} + func TestFilterEmulatorOutput(t *testing.T) { tests := []struct { name string From 6cb53890cf40c6445b74e3c3b79fc803ed441bf6 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 28 Jul 2026 21:19:50 +0800 Subject: [PATCH 09/25] docs(ssa): clarify DWARF optimized marker --- ssa/package.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ssa/package.go b/ssa/package.go index 181698e73c..aa53e79483 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -381,8 +381,8 @@ func (p Program) EnableLTOPluginMarkers(enable bool) { p.enableLTOPluginMarker = enable } -// SetDebugInfoOptimized records whether the LLVM IR optimization pipeline runs. -// It only controls DWARF's optimized marker and never selects compiler passes. +// SetDebugInfoOptimized records whether DWARF should mark generated code as +// optimized. It never selects compiler passes. func (p Program) SetDebugInfoOptimized(enable bool) { p.debugInfoOptimized = enable } From 18e9edc8b418832a4acbd3419632396840eada54 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 22 Jul 2026 05:48:49 +0800 Subject: [PATCH 10/25] cabi: preserve debug variable homes during lowering --- internal/build/build.go | 4 +- internal/cabi/cabi.go | 33 +++++++------ internal/cabi/cabi_debug_test.go | 77 ++++++++++++++++++++++++++++++ test/go/large_array_return_test.go | 16 ++++++- 4 files changed, 108 insertions(+), 22 deletions(-) create mode 100644 internal/cabi/cabi_debug_test.go diff --git a/internal/build/build.go b/internal/build/build.go index 9f750506e9..b4d977690b 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -593,11 +593,9 @@ func Build(inv Invocation) ([]Package, error) { } buildMode := ssaBuildMode - cabiOptimize := true passOpt := shouldRunLLVMPasses(mode) if emitDebugInfo { buildMode |= ssa.GlobalDebug - cabiOptimize = false } if !IsOptimizeEnabled() { buildMode |= ssa.NaiveForm @@ -620,7 +618,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() diff --git a/internal/cabi/cabi.go b/internal/cabi/cabi.go index 2e37b6bc99..822620f43e 100644 --- a/internal/cabi/cabi.go +++ b/internal/cabi/cabi.go @@ -776,27 +776,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..1e647a864a --- /dev/null +++ b/internal/cabi/cabi_debug_test.go @@ -0,0 +1,77 @@ +//go:build !llgo + +package cabi + +import ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/debuginfo" + "github.com/xgo-dev/llvm" +) + +func TestReplaceAllocaInstrsUpdatesDebugDeclare(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.CreateRetVoid() + + replaceAllocaInstrs(param, replacement) + 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 %replacement") { + t.Fatalf("dbg.declare did not follow the ABI home:\n%s", ir) + } + if !strings.Contains(ir, "%loaded = load i64, ptr %replacement") { + t.Fatalf("executable alloca use did not follow the ABI home:\n%s", ir) + } +} 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 { From efbfa723a552a4638a120c948ba9ffca551f6f14 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 22 Jul 2026 23:41:16 +0800 Subject: [PATCH 11/25] fix(pclntab): preserve Darwin line sites with DWARF --- cl/instr.go | 2 +- internal/build/build.go | 12 ++--- internal/build/funcinfo_table.go | 47 ++++++++++-------- internal/build/funcinfo_table_test.go | 71 +++++++++++++++++++++++++++ internal/build/pcln_mode.go | 9 ++-- internal/build/pcln_mode_test.go | 21 ++++---- ssa/di_debug_test.go | 44 +++++++++++++++++ ssa/expr.go | 11 +++++ ssa/funcinfo.go | 9 ++-- 9 files changed, 175 insertions(+), 51 deletions(-) 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/internal/build/build.go b/internal/build/build.go index b4d977690b..e089ec0ec8 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -474,12 +474,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} @@ -1272,7 +1270,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 { diff --git a/internal/build/funcinfo_table.go b/internal/build/funcinfo_table.go index b36aa29654..3c9be57fb2 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,9 +495,10 @@ func emitFuncInfoTable(ctx *context, pkg llssa.Package, records []funcInfoRecord } } machOSites := shouldEmitRuntimeMachOSites(ctx) - emitSites := shouldEmitRuntimeSites(ctx) - emitEntrySites := shouldEmitRuntimeEntryELFSites(ctx) && len(encoded.Records) != 0 - emitStubSites := shouldEmitRuntimeStubELFSites(ctx) + emitSites := shouldEmitRuntimePCLineSites(ctx) + emitAddressSites := shouldEmitRuntimeAddressSites(ctx) + emitEntrySites := emitAddressSites && len(encoded.Records) != 0 + emitStubSites := emitAddressSites emitRuntimeFuncInfoSites(mod, ctx.prog.PointerSize(), machOSites, emitSites && len(pcLineValues) != 0, emitEntrySites, emitStubSites && len(stubRecords) != 0) if emitEntrySites { startName, endName := entrySiteSectionInfo.boundary(machOSites) @@ -714,10 +715,11 @@ func emitExternalFuncInfoTable(ctx *context, mod llvm.Module, records []funcInfo used.SetSection("llvm.metadata") machO := shouldEmitRuntimeMachOSites(ctx) - emitSites := shouldEmitRuntimeSites(ctx) + emitSites := shouldEmitRuntimePCLineSites(ctx) + emitAddressSites := shouldEmitRuntimeAddressSites(ctx) emitPCSites := emitSites && len(encoded.PCLines) != 0 - emitEntrySites := shouldEmitRuntimeEntryELFSites(ctx) && len(encoded.Records) != 0 - emitStubSites := shouldEmitRuntimeStubELFSites(ctx) && len(stubRecords) != 0 + emitEntrySites := emitAddressSites && len(encoded.Records) != 0 + emitStubSites := emitAddressSites && len(stubRecords) != 0 emitRuntimeFuncInfoSites(mod, ctx.prog.PointerSize(), machO, emitPCSites, emitEntrySites, emitStubSites) if emitPCSites { start, end := pcLineSiteSectionInfo.boundary(machO) @@ -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/pcln_mode.go b/internal/build/pcln_mode.go index 56682058fe..8ae0e39e42 100644 --- a/internal/build/pcln_mode.go +++ b/internal/build/pcln_mode.go @@ -70,15 +70,12 @@ func effectivePCLNMode(conf *Config) PCLNMode { } // shouldEnablePCLNSites reports whether compiler-emitted PC anchor records are -// required for this build. Darwin DWARF builds keep the historical site-free -// path because inline anchors disturb LLDB lexical scopes there. ELF cannot -// reconstruct all Go entry PCs with dlsym: most Go symbols are intentionally -// absent from .dynsym, so Linux keeps sites even when it emits DWARF. -func shouldEnablePCLNSites(conf *Config, funcInfo, emitDebugInfo bool) bool { +// required for this build. +func shouldEnablePCLNSites(conf *Config, funcInfo bool) bool { if conf == nil || !funcInfo || !IsFuncInfoSitesEnabled() { return false } - return !emitDebugInfo || conf.Goos == "linux" || conf.PCLNMode == PCLNExternal + return true } // validatePCLNMode checks whether the selected build can produce the requested diff --git a/internal/build/pcln_mode_test.go b/internal/build/pcln_mode_test.go index 2f6f939906..6ebcf49809 100644 --- a/internal/build/pcln_mode_test.go +++ b/internal/build/pcln_mode_test.go @@ -121,27 +121,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") } } diff --git a/ssa/di_debug_test.go b/ssa/di_debug_test.go index 0f65ef4300..a36bcb0f08 100644 --- a/ssa/di_debug_test.go +++ b/ssa/di_debug_test.go @@ -222,6 +222,50 @@ func f() {} } } +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/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 } From bee8834b3f96c562ed169399f2359c34e595f7eb Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 22 Jul 2026 23:46:15 +0800 Subject: [PATCH 12/25] test(pclntab): cover DWARF runtime line sites --- test/go/dwarf_pcln_acceptance_test.go | 100 ++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 test/go/dwarf_pcln_acceptance_test.go 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) + } +} From 1ef373f0d933f39a524afc47a13adc75ab58de86 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 22 Jul 2026 05:53:13 +0800 Subject: [PATCH 13/25] build(debug): match cmd/link DWARF defaults --- internal/build/build.go | 20 ++++++++------------ internal/build/build_test.go | 8 ++++++-- internal/build/link_options.go | 13 +++++++------ internal/build/link_options_test.go | 24 +++++++++++------------- internal/build/pcln_mode_test.go | 10 ++++++---- 5 files changed, 38 insertions(+), 37 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index e089ec0ec8..5bedce95b0 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 } diff --git a/internal/build/build_test.go b/internal/build/build_test.go index a69cd0a9db..293b68a1be 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) { 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/pcln_mode_test.go b/internal/build/pcln_mode_test.go index 6ebcf49809..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" ) @@ -155,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") } } From c2e59748044621d6214f37141319fbd44900c313 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 22 Jul 2026 23:47:42 +0800 Subject: [PATCH 14/25] test(cl): keep IR fixtures stable under DWARF defaults --- cl/cltest/cltest.go | 5 +++++ cl/cltest/cltest_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/cl/cltest/cltest.go b/cl/cltest/cltest.go index 4a1fc60bef..1bf7415e1c 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 diff --git a/cl/cltest/cltest_test.go b/cl/cltest/cltest_test.go index 636295b984..7d94e8e168 100644 --- a/cl/cltest/cltest_test.go +++ b/cl/cltest/cltest_test.go @@ -5,8 +5,34 @@ import ( "path/filepath" "runtime" "testing" + + "github.com/goplus/llgo/internal/build" ) +func TestWithModuleCaptureDWARFMode(t *testing.T) { + for _, test := range []struct { + name string + mode build.DWARFMode + want build.DWARFMode + }{ + {name: "default", mode: build.DWARFDefault, want: build.DWARFOmit}, + {name: "preserve", mode: build.DWARFPreserve, want: build.DWARFPreserve}, + {name: "omit", mode: build.DWARFOmit, want: build.DWARFOmit}, + } { + t.Run(test.name, func(t *testing.T) { + conf := build.NewDefaultConf(build.ModeRun) + conf.LinkOptions.DWARF = test.mode + got, _ := withModuleCapture(conf, t.TempDir()) + if got.LinkOptions.DWARF != test.want { + t.Fatalf("DWARF mode = %v, want %v", got.LinkOptions.DWARF, test.want) + } + if conf.LinkOptions.DWARF != test.mode { + t.Fatalf("input DWARF mode = %v, want %v", conf.LinkOptions.DWARF, test.mode) + } + }) + } +} + func TestReadGoldenUsesToolchainVersion(t *testing.T) { dir := t.TempDir() file := filepath.Join(dir, "expect.txt") From 6ebec084cfcfc9a645b6d68fed0a37641efa2bc1 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 22 Jul 2026 23:47:42 +0800 Subject: [PATCH 15/25] fix(debug): resolve anonymous function lexical scopes --- cl/compile.go | 5 +---- cl/debug_scope_test.go | 13 +++++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) 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/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) + } } From cb1db8146540b2f5eab15ae18ad879fb1b2a134e Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 23 Jul 2026 02:02:50 +0800 Subject: [PATCH 16/25] fix(debug): preserve deferred call locations under LTO --- cl/_testlto/defer_dwarf/expect.txt | 1 + cl/_testlto/defer_dwarf/in.go | 19 +++++++ cl/compile_test.go | 13 ++++- ssa/di_debug_test.go | 83 ++++++++++++++++++++++++++++++ ssa/eh.go | 44 +++++++++++----- 5 files changed, 145 insertions(+), 15 deletions(-) create mode 100644 cl/_testlto/defer_dwarf/expect.txt create mode 100644 cl/_testlto/defer_dwarf/in.go 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/compile_test.go b/cl/compile_test.go index 005f87f4ad..3fce0b27c3 100644 --- a/cl/compile_test.go +++ b/cl/compile_test.go @@ -222,15 +222,24 @@ func TestRunAndTestFromTestlto(t *testing.T) { cltest.RunAndTestFromDir(t, "", "./_testlto", ignore, cltest.WithRunConfig(conf)) } -func TestRunAndTestFromTestltoDWARF(t *testing.T) { +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, "reflectmk_runtime", "./_testlto", nil, + 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", diff --git a/ssa/di_debug_test.go b/ssa/di_debug_test.go index a36bcb0f08..2949f3c138 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" ) @@ -222,6 +223,88 @@ func f() {} } } +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 diff --git a/ssa/eh.go b/ssa/eh.go index e18149ebc3..1e0b01a7e0 100644 --- a/ssa/eh.go +++ b/ssa/eh.go @@ -138,17 +138,30 @@ func (b Builder) Longjmp(jb, retval Expr) { func (p Function) deferInitBuilder(from Builder) (b Builder, next BasicBlock) { b = p.NewBuilder() - if p.diFunc != nil { - loc := from.impl.GetCurrentDebugLocation() - if !loc.Scope.IsNil() { - b.impl.SetCurrentDebugLocation(loc.Line, loc.Col, loc.Scope, loc.InlinedAt) - } - } + 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) 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 @@ -164,7 +177,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. @@ -176,6 +189,7 @@ type loopDeferCase struct { fn Expr args []Expr buildCall func(Builder, Expr, ...Expr) Expr + loc llvm.DebugLoc } const ( @@ -352,7 +366,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) @@ -378,6 +392,7 @@ func (b Builder) DeferTo(owner Function, stack Expr, fn Expr, buildCall func(Bui fn: fn, args: args, buildCall: buildCall, + loc: b.deferDebugLocation(), } if self == nil { owner.pendingLoopCases = append(owner.pendingLoopCases, loopCase) @@ -387,7 +402,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 @@ -407,13 +422,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) { @@ -462,6 +477,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) @@ -590,9 +606,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) } From 2359379e96c382553f0e8bfe2e337e5ab177a86a Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 23 Jul 2026 02:28:43 +0800 Subject: [PATCH 17/25] fix(debug): reanchor cross-function defer locations --- cl/debug_compile_test.go | 74 ++++++++++++++++++++++++++++------------ ssa/di_debug_test.go | 27 +++++++++++++++ ssa/eh.go | 15 +++++++- 3 files changed, 93 insertions(+), 23 deletions(-) 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/ssa/di_debug_test.go b/ssa/di_debug_test.go index 2949f3c138..7980986179 100644 --- a/ssa/di_debug_test.go +++ b/ssa/di_debug_test.go @@ -185,6 +185,7 @@ 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) @@ -217,6 +218,32 @@ func f() {} } 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()) diff --git a/ssa/eh.go b/ssa/eh.go index 1e0b01a7e0..7678f94c38 100644 --- a/ssa/eh.go +++ b/ssa/eh.go @@ -151,6 +151,19 @@ func (b Builder) deferDebugLocation() 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) @@ -392,7 +405,7 @@ func (b Builder) DeferTo(owner Function, stack Expr, fn Expr, buildCall func(Bui fn: fn, args: args, buildCall: buildCall, - loc: b.deferDebugLocation(), + loc: b.deferDebugLocationFor(owner), } if self == nil { owner.pendingLoopCases = append(owner.pendingLoopCases, loopCase) From 79eae660a855ebc874102b71a9650e346d414d00 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 27 Jul 2026 11:57:21 +0800 Subject: [PATCH 18/25] test(cl): keep metadata captures DWARF-free --- cl/cltest/cltest.go | 3 +++ cl/cltest/cltest_test.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cl/cltest/cltest.go b/cl/cltest/cltest.go index 1bf7415e1c..ff6f46f730 100644 --- a/cl/cltest/cltest.go +++ b/cl/cltest/cltest.go @@ -485,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 7d94e8e168..6da3fc2be8 100644 --- a/cl/cltest/cltest_test.go +++ b/cl/cltest/cltest_test.go @@ -22,7 +22,7 @@ func TestWithModuleCaptureDWARFMode(t *testing.T) { t.Run(test.name, func(t *testing.T) { conf := build.NewDefaultConf(build.ModeRun) conf.LinkOptions.DWARF = test.mode - got, _ := withModuleCapture(conf, t.TempDir()) + got, _, _ := withModuleCapture(conf, t.TempDir()) if got.LinkOptions.DWARF != test.want { t.Fatalf("DWARF mode = %v, want %v", got.LinkOptions.DWARF, test.want) } From e528e8a7cc4afb103c411be9767ae55f779baf4f Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 08:00:15 +0800 Subject: [PATCH 19/25] ci: allow primary DWARF shards more time --- .github/workflows/llgo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 86d7de009a2b15c4749618291112a33386acec7f Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 28 Jul 2026 21:43:36 +0800 Subject: [PATCH 20/25] fix(build): stabilize Darwin debug-map archive paths --- internal/build/build_test.go | 50 ++++++++++++++++++++++++++++++++++ internal/build/collect.go | 3 ++ internal/build/collect_test.go | 3 ++ 3 files changed, 56 insertions(+) diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 293b68a1be..6bf391a4ad 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -804,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) From e511f3c7c2755dc4e741fd1f7c1f8015639a4e04 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 2 Aug 2026 18:33:19 +0800 Subject: [PATCH 21/25] debug: integrate Go-compatible DWARF defaults --- cmd/internal/debug/debug.go | 1 - doc/design/pclntab-packaging.md | 54 ++++++++++++--------------- internal/build/build.go | 4 +- internal/build/debug_artifact.go | 6 +-- internal/build/debug_artifact_test.go | 6 +-- internal/build/funcinfo_table.go | 8 ++-- internal/build/pcln_mode.go | 2 +- internal/build/ssa_order_fix.go | 6 ++- internal/build/ssa_order_fix_test.go | 7 ++++ 9 files changed, 48 insertions(+), 46 deletions(-) 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/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 5bedce95b0..39a43279bd 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -182,7 +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 - 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. @@ -612,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, true), + cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, true), } defer ctx.closePackageMetas() 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 3c9be57fb2..6e51282a72 100644 --- a/internal/build/funcinfo_table.go +++ b/internal/build/funcinfo_table.go @@ -495,11 +495,11 @@ func emitFuncInfoTable(ctx *context, pkg llssa.Package, records []funcInfoRecord } } machOSites := shouldEmitRuntimeMachOSites(ctx) - emitSites := shouldEmitRuntimePCLineSites(ctx) + emitPCLineSites := shouldEmitRuntimePCLineSites(ctx) emitAddressSites := shouldEmitRuntimeAddressSites(ctx) emitEntrySites := emitAddressSites && len(encoded.Records) != 0 emitStubSites := emitAddressSites - emitRuntimeFuncInfoSites(mod, ctx.prog.PointerSize(), machOSites, emitSites && len(pcLineValues) != 0, emitEntrySites, emitStubSites && len(stubRecords) != 0) + 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) @@ -715,9 +715,9 @@ func emitExternalFuncInfoTable(ctx *context, mod llvm.Module, records []funcInfo used.SetSection("llvm.metadata") machO := shouldEmitRuntimeMachOSites(ctx) - emitSites := shouldEmitRuntimePCLineSites(ctx) + emitPCLineSites := shouldEmitRuntimePCLineSites(ctx) emitAddressSites := shouldEmitRuntimeAddressSites(ctx) - emitPCSites := emitSites && len(encoded.PCLines) != 0 + 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) diff --git a/internal/build/pcln_mode.go b/internal/build/pcln_mode.go index 8ae0e39e42..8bf84b9acf 100644 --- a/internal/build/pcln_mode.go +++ b/internal/build/pcln_mode.go @@ -70,7 +70,7 @@ func effectivePCLNMode(conf *Config) PCLNMode { } // shouldEnablePCLNSites reports whether compiler-emitted PC anchor records are -// required for this build. +// enabled after the global funcinfo and site controls are applied. func shouldEnablePCLNSites(conf *Config, funcInfo bool) bool { if conf == nil || !funcInfo || !IsFuncInfoSitesEnabled() { return false diff --git a/internal/build/ssa_order_fix.go b/internal/build/ssa_order_fix.go index d8a8eb6452..3a577bb12e 100644 --- a/internal/build/ssa_order_fix.go +++ b/internal/build/ssa_order_fix.go @@ -424,11 +424,15 @@ func valueDependsOn(v, target ssa.Value, seen map[ssa.Value]struct{}) bool { // 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 or absent. +// 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 _, movingAnchor := moving[anchor]; movingAnchor { + return instrs + } moved := make([]ssa.Instruction, 0, len(moving)) remaining := make([]ssa.Instruction, 0, len(instrs)) for _, instr := range instrs { diff --git a/internal/build/ssa_order_fix_test.go b/internal/build/ssa_order_fix_test.go index 33ff26041f..89adce6a82 100644 --- a/internal/build/ssa_order_fix_test.go +++ b/internal/build/ssa_order_fix_test.go @@ -198,6 +198,13 @@ func f() { 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]: {}, From 3329ad8f9ed97bd5ec1c9447aaee15482209f374 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 2 Aug 2026 18:44:28 +0800 Subject: [PATCH 22/25] docs(debug): isolate the Asyncify DWARF blocker --- cmd/llgo/debugtest/ACCEPTANCE.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 From 3fd2c661a78e5304a7b2d5680c51e77cfd6055b9 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 2 Aug 2026 18:46:18 +0800 Subject: [PATCH 23/25] docs(debug): update native DWARF defaults --- cmd/llgo/lldbtest/README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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. From 25a8eeffd02081e290046561b57c19f3f4abebb0 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 2 Aug 2026 19:06:35 +0800 Subject: [PATCH 24/25] ci(debug): stabilize integrated DWARF gates --- .github/workflows/browser-debug.yml | 4 ++-- cl/compile_test.go | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) 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/cl/compile_test.go b/cl/compile_test.go index 3fce0b27c3..009b2a3361 100644 --- a/cl/compile_test.go +++ b/cl/compile_test.go @@ -417,6 +417,10 @@ 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), ) From 5336d7074219df332fb6301e88816f636d5c9bc0 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 2 Aug 2026 20:46:47 +0800 Subject: [PATCH 25/25] fix(debug): keep aggregate parameter homes authoritative --- internal/cabi/cabi.go | 17 +++++++++++++---- internal/cabi/cabi_debug_test.go | 22 ++++++++++++++++------ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/internal/cabi/cabi.go b/internal/cabi/cabi.go index 822620f43e..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() { diff --git a/internal/cabi/cabi_debug_test.go b/internal/cabi/cabi_debug_test.go index 1e647a864a..c26cdf679b 100644 --- a/internal/cabi/cabi_debug_test.go +++ b/internal/cabi/cabi_debug_test.go @@ -10,7 +10,7 @@ import ( "github.com/xgo-dev/llvm" ) -func TestReplaceAllocaInstrsUpdatesDebugDeclare(t *testing.T) { +func TestReplaceAllocaInstrsPreservesInitializedDebugHome(t *testing.T) { ctx := llvm.NewContext() defer ctx.Dispose() mod := ctx.NewModule("cabi-debug") @@ -57,9 +57,12 @@ func TestReplaceAllocaInstrsUpdatesDebugDeclare(t *testing.T) { 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) + 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()) @@ -68,10 +71,17 @@ func TestReplaceAllocaInstrsUpdatesDebugDeclare(t *testing.T) { t.Fatalf("setup operand was rewritten to the ABI home:\n%s", mod.String()) } ir := mod.String() - if !strings.Contains(ir, "#dbg_declare(ptr %replacement") { - t.Fatalf("dbg.declare did not follow the ABI home:\n%s", ir) + 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, "%loaded = load i64, ptr %replacement") { - t.Fatalf("executable alloca use did not follow the ABI 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) } }