diff --git a/src/cmd/compile/internal/ssa/llvmdata.go b/src/cmd/compile/internal/ssa/llvmdata.go index 196f4bf62faa23..855b41803d0f03 100644 --- a/src/cmd/compile/internal/ssa/llvmdata.go +++ b/src/cmd/compile/internal/ssa/llvmdata.go @@ -61,6 +61,13 @@ func attachGoObjSymbolRef(value llvm.Value, s *obj.LSym) { return } } + // Linknamed symbols live in GoObj's non-package namespace even when the + // compiler learned about them through an imported package. Their export + // symbol index addresses that package's ordinary symbol block and must not + // be attached to the LLVM declaration as an imported reference. + if s.PkgIdx == goobj.PkgIdxNone || s.IsLinkname() { + return + } localPkg := objabi.PathToPrefix(base.Ctxt.Pkgpath) if s.Pkg == "" || s.Pkg == `""` || s.Pkg == "_" || s.Pkg == localPkg || !s.Indexed() { return diff --git a/src/cmd/compile/internal/ssa/llvmdebug.go b/src/cmd/compile/internal/ssa/llvmdebug.go new file mode 100644 index 00000000000000..d6f27cd396dadf --- /dev/null +++ b/src/cmd/compile/internal/ssa/llvmdebug.go @@ -0,0 +1,265 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !compiler_bootstrap + +package ssa + +import ( + "cmd/compile/internal/base" + "cmd/compile/internal/types" + "cmd/internal/obj" + "cmd/internal/src" + "internal/buildcfg" + "path/filepath" + "sort" + + "github.com/goallc/go-llvm" +) + +// This deliberately contains only the source locations LLVM needs to build +// Go pcfile, pcline, and pcinline after final machine layout. Source types, +// variables, and their locations belong to the separate target-DWARF path. +var ( + llvmDIBuilder *llvm.DIBuilder + llvmDICompileUnit llvm.Metadata + llvmDIFiles map[string]llvm.Metadata + llvmDISubprograms map[*obj.LSym]llvm.Metadata + llvmDISubprogramVals map[*obj.LSym]llvm.Value + llvmDIDebugFinalized bool +) + +func initLLVMDebugInfo(pkg *types.Pkg) { + llvmDIBuilder = llvm.NewDIBuilder(CurrentModule) + llvmDIFiles = make(map[string]llvm.Metadata) + llvmDISubprograms = make(map[*obj.LSym]llvm.Metadata) + llvmDISubprogramVals = make(map[*obj.LSym]llvm.Value) + llvmDIDebugFinalized = false + + name := pkg.Path + if name == "" { + name = "go-package" + } + llvmDICompileUnit = llvmDIBuilder.CreateCompileUnit(llvm.DICompileUnit{ + Language: llvm.DW_LANG_Go, + File: name, + Producer: "Go compiler " + buildcfg.Version, + Optimized: base.Flag.N == 0, + EmissionKind: llvm.DwarfEmissionLineTablesOnly, + }) + CurrentModule.AddNamedMetadataOperand("goobj.debug.config", + GlobalCtxt.MDNode([]llvm.Metadata{GlobalCtxt.MDString("pcln-v1")})) + + flag := func(name string, value uint64) { + CurrentModule.AddNamedMetadataOperand("llvm.module.flags", GlobalCtxt.MDNode([]llvm.Metadata{ + llvm.ConstInt(GlobalCtxt.Int32Type(), 2, false).ConstantAsMetadata(), + GlobalCtxt.MDString(name), + llvm.ConstInt(GlobalCtxt.Int32Type(), value, false).ConstantAsMetadata(), + })) + } + flag("Dwarf Version", 4) + flag("Debug Info Version", 3) +} + +func finalizeLLVMDebugInfo() { + if llvmDIBuilder == nil || llvmDIDebugFinalized { + return + } + syms := make([]*obj.LSym, 0, len(llvmDISubprograms)) + for sym := range llvmDISubprograms { + syms = append(syms, sym) + } + sort.Slice(syms, func(i, j int) bool { + if syms[i].Name != syms[j].Name { + return syms[i].Name < syms[j].Name + } + return syms[i].ABI() < syms[j].ABI() + }) + abstractValues := make(map[string]llvm.Value) + for _, sym := range syms { + value, ok := llvmDISubprogramVals[sym] + if !ok { + value = llvmDebugSubprogramValue(sym, llvmDISubprograms[sym], abstractValues) + } + preserveGoObjMetadataValues(value) + CurrentModule.AddNamedMetadataOperand("goobj.debug.funcs", GlobalCtxt.MDNode([]llvm.Metadata{ + llvmDISubprograms[sym], + value.ConstantAsMetadata(), + })) + } + emitGoObjCompilerUsed() + llvmDIBuilder.Finalize() + llvmDIBuilder.Destroy() + llvmDIBuilder = nil + llvmDIDebugFinalized = true +} + +func llvmDebugSubprogramValue(sym *obj.LSym, sp llvm.Metadata, abstractValues map[string]llvm.Value) llvm.Value { + storageName := llvmFunctionStorageName(sym.Name, llvmCallConv(sym.ABI())) + value := CurrentModule.NamedFunction(storageName) + if !value.IsNil() && !value.IsDeclaration() { + return value + } + + if !sym.ContentAddressable() { + return llvmGoDataRef(sym) + } + + // Imported inline closures can retain the native compiler's call-stack + // hash even when their body was emitted under the canonical, unhashed name + // in this object. Reuse that equivalent definition when it exists. + canonicalName := obj.TrimInlineHash(sym.Name) + abstractKey := llvmFunctionStorageName(canonicalName, llvmCallConv(sym.ABI())) + if abstract, ok := abstractValues[abstractKey]; ok { + return abstract + } + if canonicalName != sym.Name { + canonical := CurrentModule.NamedFunction( + llvmFunctionStorageName(canonicalName, llvmCallConv(sym.ABI()))) + if !canonical.IsNil() && !canonical.IsDeclaration() { + return canonical + } + } + + // Native GoObj emits a zero-sized STEXT symbol with FuncInfo when a + // content-addressable closure was completely inlined and has no remaining + // body. LLVM needs an emitted function symbol for the same linker contract. + // The unreachable body is never called, but keeps the logical callee + // and its FuncInfo available to the final pcinline tree. + if !value.IsNil() { + if !value.FirstUse().IsNil() { + return llvmGoDataRef(sym) + } + value.EraseFromParentAsFunction() + } + value = llvm.AddFunction(CurrentModule, storageName, + llvm.FunctionType(GlobalCtxt.VoidType(), nil, false)) + value.SetFunctionCallConv(llvmCallConv(sym.ABI())) + value.SetLinkage(llvm.WeakAnyLinkage) + value.SetGlobalMetadata(GlobalCtxt.MDKindID(goObjSymbolNameMD), GlobalCtxt.MDNode([]llvm.Metadata{ + GlobalCtxt.MDString(canonicalName), + })) + value.SetSubprogram(sp) + b := GlobalCtxt.NewBuilder() + b.SetInsertPointAtEnd(GlobalCtxt.AddBasicBlock(value, "goobj.abstract")) + b.CreateUnreachable() + b.Dispose() + abstractValues[abstractKey] = value + return value +} + +func llvmSourcePos(xpos src.XPos) src.Pos { + if xpos == src.NoXPos { + return src.NoPos + } + pos := base.Ctxt.InnermostPos(xpos) + if !pos.IsKnown() { + return src.NoPos + } + return pos +} + +func llvmSourcePath(pos src.Pos) string { + if !pos.IsKnown() { + return "llvm-ir" + } + if name := pos.AbsFilename(); name != "" { + return name + } + if name := pos.RelFilename(); name != "" { + return name + } + return "llvm-ir" +} + +func llvmDIFile(pos src.Pos) llvm.Metadata { + path := llvmSourcePath(pos) + if file, ok := llvmDIFiles[path]; ok { + return file + } + dir, name := filepath.Split(path) + if dir != "" { + dir = filepath.Clean(dir) + } + file := llvmDIBuilder.CreateFile(name, dir) + llvmDIFiles[path] = file + return file +} + +func llvmDIScopeForPos(scope llvm.Metadata, pos src.Pos, discriminator int) llvm.Metadata { + return llvmDIBuilder.CreateLexicalBlockFile(scope, llvmDIFile(pos), discriminator) +} + +func llvmDebugSubprogram(sym *obj.LSym, pos src.Pos, _ *Func) llvm.Metadata { + if sym == nil { + base.Fatalf("invalid LLVM debug subprogram") + } + if sp, ok := llvmDISubprograms[sym]; ok { + return sp + } + file := llvmDIFile(pos) + line := 0 + if pos.IsKnown() { + line = int(pos.RelLine()) + } + if info := sym.Func(); info != nil && info.StartLine > 0 { + line = int(info.StartLine) + } + if line == 0 { + line = 1 + } + sp := llvmDIBuilder.CreateFunction(llvmDICompileUnit, llvm.DIFunction{ + Name: sym.Name, + LinkageName: llvmFunctionStorageName(sym.Name, llvmCallConv(sym.ABI())), + File: file, + Line: line, + Type: llvmDIBuilder.CreateSubroutineType(llvm.DISubroutineType{File: file}), + IsDefinition: true, + ScopeLine: line, + Optimized: base.Flag.N == 0, + }) + llvmDISubprograms[sym] = sp + return sp +} + +func (lfc *LLVMFuncContext) setDebugLocation(xpos src.XPos) { + pos := llvmSourcePos(xpos) + if !pos.IsKnown() || pos.RelLine() == 0 { + lfc.b.ClearCurrentDebugLocation() + return + } + + scope := lfc.DISubprogram + var inlinedAt llvm.Metadata + inlIndex := pos.Base().InliningIndex() + var chain []int + for inlIndex >= 0 { + chain = append(chain, inlIndex) + inlIndex = base.Ctxt.InlTree.Parent(inlIndex) + } + for left, right := 0, len(chain)-1; left < right; left, right = left+1, right-1 { + chain[left], chain[right] = chain[right], chain[left] + } + + for i, index := range chain { + callPos := llvmSourcePos(base.Ctxt.InlTree.CallPos(index)) + if !callPos.IsKnown() || callPos.RelLine() == 0 { + base.Fatalf("inline callsite %d has no LLVM debug position", index) + } + callScope := llvmDIScopeForPos(scope, callPos, index+1) + inlinedAt = GlobalCtxt.CreateDebugLocation( + callPos.RelLine(), callPos.RelCol(), callScope, inlinedAt) + + calleePos := pos + if i+1 < len(chain) { + calleePos = llvmSourcePos(base.Ctxt.InlTree.CallPos(chain[i+1])) + } + callee := base.Ctxt.InlTree.InlinedFunction(index) + scope = llvmDebugSubprogram(callee, calleePos, nil) + } + + locationScope := llvmDIScopeForPos(scope, pos, 0) + lfc.b.SetCurrentDebugLocationMetadata(GlobalCtxt.CreateDebugLocation( + pos.RelLine(), pos.RelCol(), locationScope, inlinedAt)) +} diff --git a/src/cmd/compile/internal/ssa/ssa2llvm.go b/src/cmd/compile/internal/ssa/ssa2llvm.go index a0cbb18a1007c0..435e3cf0fbee86 100644 --- a/src/cmd/compile/internal/ssa/ssa2llvm.go +++ b/src/cmd/compile/internal/ssa/ssa2llvm.go @@ -29,6 +29,7 @@ type LLVMFuncContext struct { OpenDeferSlots map[llvmLocalKey]int F *Func LF llvm.Value + DISubprogram llvm.Metadata Prologue llvm.BasicBlock OpenDeferRecovery llvm.BasicBlock ClosureContext llvm.Value @@ -2054,10 +2055,13 @@ func (lfc *LLVMFuncContext) GenLV(v *Value) llvm.Value { return lv } savedBlock := lfc.b.GetInsertBlock() + savedLocation := lfc.b.CurrentDebugLocationMetadata() if v.Block != nil { lfc.b.SetInsertPointAtEnd(lfc.BBs[v.Block.ID]) } + lfc.setDebugLocation(v.Pos) defer func() { + lfc.b.SetCurrentDebugLocationMetadata(savedLocation) if !savedBlock.IsNil() { lfc.b.SetInsertPointAtEnd(savedBlock) } @@ -2731,9 +2735,12 @@ func (lfc *LLVMFuncContext) GenLV(v *Value) llvm.Value { func (lfc *LLVMFuncContext) CompileBlock(BB *Block, values []*Value) { lfc.b.SetInsertPointAtEnd(lfc.BBs[BB.ID]) + lfc.b.ClearCurrentDebugLocation() for _, v := range values { lfc.GenLV(v) } + lfc.setDebugLocation(BB.Pos) + defer lfc.b.ClearCurrentDebugLocation() switch BB.Kind { case BlockRet: if lfc.ResultCount == 0 { @@ -2938,6 +2945,10 @@ func LLVMCompile(f *Func) { if FCtxt.LF.BasicBlocksCount() != 0 { f.fe.Fatalf(f.Entry.Pos, "duplicate LLVM definition for %s", f.OwnAux.Fn.Name) } + FCtxt.DISubprogram = llvmDebugSubprogram( + f.OwnAux.Fn, llvmSourcePos(f.Entry.Pos), f) + llvmDISubprogramVals[f.OwnAux.Fn] = FCtxt.LF + FCtxt.LF.SetSubprogram(FCtxt.DISubprogram) FCtxt.LF.SetGC(goGCStrategy) // Go has already made its source-level inlining decision before LLVM // lowering. Preserve both explicit //go:noinline boundaries and the @@ -3489,6 +3500,7 @@ func InitModule(pkg *types.Pkg) { currentLLVMDataLowerer = newLLVMDataLowerer(make(map[*obj.LSym]bool)) goObjCompilerUsed = nil goObjCompilerUsedNames = make(map[string]bool) + initLLVMDebugInfo(pkg) } // goObjTargetTriple identifies the GoObj target that llc should use when it @@ -3552,5 +3564,6 @@ func Output(fileName string) error { emitGoObjCgoModuleAsm() goObjImportsWritten = true } + finalizeLLVMDebugInfo() return llvm.LLVMPrintModuleToFile(CurrentModule, fileName) } diff --git a/src/cmd/compile/internal/ssa/ssa2llvm_nilcheck.go b/src/cmd/compile/internal/ssa/ssa2llvm_nilcheck.go index a214d750813b1e..cf3a11debc0d67 100644 --- a/src/cmd/compile/internal/ssa/ssa2llvm_nilcheck.go +++ b/src/cmd/compile/internal/ssa/ssa2llvm_nilcheck.go @@ -111,6 +111,7 @@ func (lfc *LLVMFuncContext) expandNilCheckIntrinsics() { if before.IsNil() || llvm.NextInstruction(call).IsNil() { lfc.F.fe.Fatalf(lfc.F.Entry.Pos, "LLVM nil-check intrinsic is not followed by a block terminator") } + debugLoc := call.InstructionDebugLoc() panicBlock := GlobalCtxt.AddBasicBlock(lfc.LF, "nilcheck.nil") continueBlock := GlobalCtxt.AddBasicBlock(lfc.LF, "nilcheck.notnil") @@ -124,6 +125,11 @@ func (lfc *LLVMFuncContext) expandNilCheckIntrinsics() { checked := call.Operand(0) call.EraseFromParentAsInstruction() + // The marker was emitted at the Go OpNilCheck position. Preserve its + // complete DILocation, including any inlined-at chain, on the explicit + // control flow and panic call that replace it. Otherwise a recovered + // panic inherits the preceding source line in Go's pcline table. + b.SetCurrentDebugLocationMetadata(debugLoc) b.SetInsertPointAtEnd(before) isNil := b.CreateICmp(llvm.IntEQ, checked, llvm.ConstNull(checked.Type()), "nilcheck.isnil") b.CreateCondBr(isNil, panicBlock, continueBlock) @@ -140,6 +146,7 @@ func (lfc *LLVMFuncContext) expandNilCheckIntrinsics() { // the conservative continuation edge until a future noreturn form can // also guarantee a valid call return PC and PCSP range. b.CreateBr(continueBlock) + b.ClearCurrentDebugLocation() lfc.replacePhiPredecessor(before, continueBlock) } diff --git a/src/cmd/llvmplugin/CMakeLists.txt b/src/cmd/llvmplugin/CMakeLists.txt index c30a1d37f95f6e..837e164bc47f00 100644 --- a/src/cmd/llvmplugin/CMakeLists.txt +++ b/src/cmd/llvmplugin/CMakeLists.txt @@ -21,6 +21,7 @@ add_definitions(${LLVM_DEFINITIONS}) include_directories(SYSTEM ${LLVM_INCLUDE_DIRS}) add_llvm_pass_plugin(GoALLCStatepoints + GoALLCInlineAnchors.cpp GoALLCPreCodeGen.cpp GoALLCStackMapPrinter.cpp GoALLCStatepoints.cpp @@ -183,6 +184,16 @@ if(BUILD_TESTING) "go-stack-growth-statepoint" ) + add_test( + NAME GoALLCStatepoints.DebugInlineGoObj + COMMAND + "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/testdata/check-debug-inline.py" + --llc "${GOALLC_LLC_EXECUTABLE}" + --plugin "$" + --input "${CMAKE_CURRENT_SOURCE_DIR}/testdata/debug-inline.ll" + ) + add_test( NAME GoALLCStatepoints.ZeroLengthPointerArray COMMAND diff --git a/src/cmd/llvmplugin/GoALLCInlineAnchors.cpp b/src/cmd/llvmplugin/GoALLCInlineAnchors.cpp new file mode 100644 index 00000000000000..8f854545f28ede --- /dev/null +++ b/src/cmd/llvmplugin/GoALLCInlineAnchors.cpp @@ -0,0 +1,167 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/CodeGen/MachineBasicBlock.h" +#include "llvm/CodeGen/MachineFunction.h" +#include "llvm/CodeGen/MachineFunctionPass.h" +#include "llvm/CodeGen/MachineInstr.h" +#include "llvm/CodeGen/TargetInstrInfo.h" +#include "llvm/IR/DebugInfoMetadata.h" +#include "llvm/IR/DebugLoc.h" +#include "llvm/MC/MCContext.h" +#include "llvm/MC/MCSymbol.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Target/TargetMachine.h" + +using namespace llvm; + +namespace { + +struct CanonicalInlineSite { + const DILocation *Original = nullptr; + const DISubprogram *Callee = nullptr; + const DILocation *Parent = nullptr; + DILocation *Canonical = nullptr; +}; + +/// Materialize one real instruction for every source inline edge that remains +/// after all generic machine layout passes. Go's inline unwinder needs a PC in +/// the parent frame; a zero-width label alone cannot create that PC range. +class GoALLCInlineAnchorPass final : public MachineFunctionPass { + SmallVector CanonicalSites; + + DILocation *canonicalizeInlineSite(const DILocation *CallSite, + const DISubprogram *Callee) { + if (!CallSite || !Callee) + report_fatal_error("GoALLC inline site has no callsite or callee"); + + DILocation *Parent = nullptr; + if (const DILocation *Outer = CallSite->getInlinedAt()) + Parent = + canonicalizeInlineSite(Outer, CallSite->getScope()->getSubprogram()); + + for (const CanonicalInlineSite &Site : CanonicalSites) + if (Site.Original == CallSite && Site.Callee == Callee && + Site.Parent == Parent) + return Site.Canonical; + + // GoObj identifies a surviving inline edge by its DILocation pointer. + // LLVM may otherwise share one callsite node between different inlinees, + // so give every (callsite, callee, parent) edge a stable distinct node. + DILocation *Canonical = DILocation::getDistinct( + CallSite->getContext(), CallSite->getLine(), CallSite->getColumn(), + CallSite->getScope(), Parent, CallSite->isImplicitCode(), + CallSite->getAtomGroup(), CallSite->getAtomRank()); + CanonicalSites.push_back({CallSite, Callee, Parent, Canonical}); + return Canonical; + } + + DILocation *canonicalizeLocation(const DILocation *Loc) { + const DILocation *CallSite = Loc->getInlinedAt(); + if (!CallSite) + return const_cast(Loc); + const DISubprogram *Callee = Loc->getScope()->getSubprogram(); + DILocation *CanonicalCallSite = canonicalizeInlineSite(CallSite, Callee); + return DILocation::get(Loc->getContext(), Loc->getLine(), Loc->getColumn(), + Loc->getScope(), CanonicalCallSite, + Loc->isImplicitCode(), Loc->getAtomGroup(), + Loc->getAtomRank()); + } + +public: + static char ID; + + GoALLCInlineAnchorPass() : MachineFunctionPass(ID) {} + + StringRef getPassName() const override { + return "GoALLC final inline unwind anchors"; + } + + bool runOnMachineFunction(MachineFunction &MF) override { + if (!MF.getTarget().getTargetTriple().isOSBinFormatGoObj() || + !MF.getFunction().getSubprogram()) + return false; + + const TargetInstrInfo &TII = *MF.getSubtarget().getInstrInfo(); + CanonicalSites.clear(); + + // Normalize complete inline chains before inspecting them. This keeps the + // anchor pass and the later GoObj debug handler on the same edge identity. + for (MachineBasicBlock &MBB : MF) + for (MachineInstr &MI : MBB) + if (!MI.isMetaInstruction() && MI.getDebugLoc()) + MI.setDebugLoc( + DebugLoc(canonicalizeLocation(MI.getDebugLoc().get()))); + + DenseSet AnchoredCallsites; + bool Changed = false; + + for (MachineBasicBlock &MBB : MF) { + for (auto It = MBB.begin(), End = MBB.end(); It != End; ++It) { + MachineInstr &MI = *It; + if (MI.isMetaInstruction() || !MI.getDebugLoc()) + continue; + + SmallVector CallSites; + for (const DILocation *Loc = MI.getDebugLoc().get(); + Loc && Loc->getInlinedAt(); Loc = Loc->getInlinedAt()) + CallSites.push_back(Loc->getInlinedAt()); + std::reverse(CallSites.begin(), CallSites.end()); + + // Insert outer-to-inner before the first surviving instruction in the + // child. Repeated insertion at It consequently leaves the anchors in + // outer-to-inner byte order immediately before that instruction. + for (const DILocation *CallSite : CallSites) { + if (!AnchoredCallsites.insert(CallSite).second) + continue; + + TII.insertNoop(MBB, It); + MachineInstr &Anchor = *std::prev(It); + + // Compiler-generated wrappers can carry a real inline edge whose + // callsite line is zero. GoObj still needs a ParentPC for that edge, + // while the line-table collector deliberately ignores line zero. + // Give only the artificial anchor the nearest existing caller line; + // the inline-tree node retains the frontend's original line zero. + unsigned AnchorLine = CallSite->getLine(); + for (const DILocation *Caller = CallSite->getInlinedAt(); + AnchorLine == 0 && Caller; Caller = Caller->getInlinedAt()) + AnchorLine = Caller->getLine(); + if (AnchorLine == 0) + if (const DISubprogram *SP = CallSite->getScope()->getSubprogram()) + AnchorLine = SP->getLine(); + if (AnchorLine == 0) + report_fatal_error( + "GoALLC inline anchor has no usable caller source line"); + + auto *Artificial = DILocation::get( + MF.getFunction().getContext(), AnchorLine, CallSite->getColumn(), + CallSite->getScope(), CallSite->getInlinedAt(), + /*ImplicitCode=*/true, CallSite->getAtomGroup(), + CallSite->getAtomRank()); + Anchor.setDebugLoc(DebugLoc(Artificial)); + + MCSymbol *Label = MF.getContext().createTempSymbol(); + Anchor.setPreInstrSymbol(MF, Label); + MF.getContext().markGoObjInlineAnchor(Label); + Changed = true; + } + } + } + return Changed; + } + + void getAnalysisUsage(AnalysisUsage &AU) const override { + AU.setPreservesCFG(); + MachineFunctionPass::getAnalysisUsage(AU); + } +}; + +char GoALLCInlineAnchorPass::ID = 0; + +} // namespace + +Pass *createGoALLCInlineAnchorPass() { return new GoALLCInlineAnchorPass(); } diff --git a/src/cmd/llvmplugin/GoALLCStatepointPlugin.cpp b/src/cmd/llvmplugin/GoALLCStatepointPlugin.cpp index 0776bc749b1eb0..a936d9bccd6e73 100644 --- a/src/cmd/llvmplugin/GoALLCStatepointPlugin.cpp +++ b/src/cmd/llvmplugin/GoALLCStatepointPlugin.cpp @@ -3,15 +3,20 @@ // license that can be found in the LICENSE file. #include "GoALLCPreCodeGen.h" +#include "llvm/CodeGen/TargetPassConfig.h" #include "llvm/Config/llvm-config.h" #include "llvm/IR/LLVMContext.h" #include "llvm/IR/Module.h" #include "llvm/Plugins/PassPlugin.h" #include "llvm/Support/CommandLine.h" #include "llvm/Support/raw_ostream.h" +#include "llvm/Target/RegisterTargetPassConfigCallback.h" +#include "llvm/Target/TargetMachine.h" using namespace llvm; +Pass *createGoALLCInlineAnchorPass(); + namespace { cl::opt ReportInvocation( @@ -41,6 +46,13 @@ bool runPreCodeGenCallback(Module &M, TargetMachine &TM, CodeGenFileType, return false; } +RegisterTargetPassConfigCallback RegisterGoALLCInlineAnchors( + [](TargetMachine &TM, PassManagerBase &, TargetPassConfig *TPC) { + if (TPC && TM.getTargetTriple().isOSBinFormatGoObj()) + TPC->addPreBranchRelaxationPass( + []() { return createGoALLCInlineAnchorPass(); }); + }); + } // namespace extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo diff --git a/src/cmd/llvmplugin/testdata/check-debug-inline.py b/src/cmd/llvmplugin/testdata/check-debug-inline.py new file mode 100644 index 00000000000000..e0512de85e0941 --- /dev/null +++ b/src/cmd/llvmplugin/testdata/check-debug-inline.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python3 + +import argparse +import pathlib +import struct +import subprocess +import tempfile + + +MAGIC = bytes([0]) + b"go120ld" +PKGIDX_NONE = (1 << 31) - 1 +PKGIDX_HASHED64 = PKGIDX_NONE - 1 +PKGIDX_HASHED = PKGIDX_NONE - 2 +PKGIDX_SELF = PKGIDX_NONE - 4 +SYMBOL_SIZE = 21 +AUX_SIZE = 9 + + +def fail(message): + raise SystemExit(message) + + +def run(command): + result = subprocess.run(command, text=True, capture_output=True) + if result.returncode: + fail( + f"command failed ({result.returncode}): {' '.join(command)}\n" + f"{result.stdout}{result.stderr}" + ) + + +def string_at(data, offset): + size, string_offset = struct.unpack_from("> 1 + if value & 1: + signed = ~signed + return signed, offset + + +def decode_pctab(payload): + ranges = [] + offset = 0 + pc = 0 + value = -1 + first = True + while offset < len(payload): + delta, offset = read_varint(payload, offset) + if delta == 0 and not first: + break + value += delta + pc_delta, offset = read_uvarint(payload, offset) + next_pc = pc + pc_delta + ranges.append((pc, next_pc, value)) + pc = next_pc + first = False + return ranges + + +def value_at(ranges, pc): + for start, end, value in ranges: + if start <= pc < end: + return value + fail(f"no PC table range covers PC {pc}: {ranges}") + + +class GoObj: + def __init__(self, path): + raw = pathlib.Path(path).read_bytes() + base = raw.index(MAGIC) + self.data = raw[base:] + self.offsets = struct.unpack_from("<19I", self.data, 20) + self.symdef = read_symbols( + self.data, self.offsets[3], self.offsets[4] + ) + self.hashed64 = read_symbols( + self.data, self.offsets[4], self.offsets[5] + ) + self.hashed = read_symbols( + self.data, self.offsets[5], self.offsets[6] + ) + self.nonpkgdef = read_symbols( + self.data, self.offsets[6], self.offsets[7] + ) + self.nonpkgref = read_symbols( + self.data, self.offsets[7], self.offsets[8] + ) + self.defined = ( + self.symdef + self.hashed64 + self.hashed + self.nonpkgdef + ) + self.aux_indexes = [ + struct.unpack_from("", + }, " ") + executable := filepath.Join(t.TempDir(), "pclninline") + buildFixture := testenv.Command(t, goTool, "build", + "-toolexec="+toolexec, + "-gcflags="+packagePath+"=-enablellvm -llvmironly", + "-ldflags=-w", + "-o", executable, + "./src/"+packagePath, + ) + buildFixture.Dir = root + buildFixture.Env = append(os.Environ(), "GOCACHE="+t.TempDir()) + if out, err := buildFixture.CombinedOutput(); err != nil { + t.Fatalf("building PCLN inline fixture: %v\n%s", err, out) + } + + output, runErr := testenv.Command(t, executable).CombinedOutput() + if runErr == nil { + t.Fatalf("PCLN inline fixture did not panic:\n%s", output) + } + trace := string(output) + if !strings.Contains(trace, "panic: pcln-inline") { + t.Fatalf("unexpected PCLN inline fixture failure: %v\n%s", runErr, output) + } + patterns := []string{ + `(?m)^main\.capture\(\)\n\t.*pclninline/main\.go:23 \+0x[0-9a-f]+$`, + `(?m)^main\.inner\(\.\.\.\)\n\t.*pclninline/main\.go:8$`, + `(?m)^main\.middle\(\.\.\.\)\n\t.*pclninline/main\.go:12$`, + `(?m)^main\.outer\.func1(?:#[^\n]+#)?\(\.\.\.\)\n\t.*pclninline/main\.go:17$`, + `(?m)^main\.outer\(\.\.\.\)\n\t.*pclninline/main\.go:18$`, + `(?m)^main\.main\(\)\n\t.*pclninline/main\.go:27 \+0x[0-9a-f]+$`, + } + last := -1 + for _, pattern := range patterns { + location := regexp.MustCompile(pattern).FindStringIndex(trace) + if location == nil { + t.Fatalf("traceback does not match %q:\n%s", pattern, output) + } + if location[0] <= last { + t.Fatalf("inline frames are out of order:\n%s", output) + } + last = location[0] + } +} + func TestLLVMInitTaskOrder(t *testing.T) { llc := os.Getenv("GOALLC_LLC") plugin := os.Getenv("GOALLC_PASS_PLUGIN") @@ -806,6 +916,18 @@ func TestLLVMExplicitNilCheckGoObj(t *testing.T) { if out, err := compileFixture.CombinedOutput(); err != nil { t.Fatalf("compiling LLVM nil-check GoObj fixture: %v\n%s", err, out) } + ir, err := os.ReadFile(archive + ".ll") + if err != nil { + t.Fatalf("reading LLVM nil-check IR: %v", err) + } + panicLoc := regexp.MustCompile(`(?m)^[[:space:]]*(?:tail )?call goabiinternal void @runtime\.panicmem\(\), !dbg !([0-9]+)$`).FindSubmatch(ir) + if panicLoc == nil { + t.Fatalf("panicmem call is missing its nil-check debug location:\n%s", ir) + } + wantLoc := []byte("!" + string(panicLoc[1]) + " = !DILocation(line: 8,") + if !bytes.Contains(ir, wantLoc) { + t.Fatalf("panicmem debug location is not the source dereference line; want %q:\n%s", wantLoc, ir) + } object := filepath.Join(t.TempDir(), "nilcheckobj.o") runLLC := testenv.Command( diff --git a/src/cmd/llvmtoolexec/testdata/importrefs/main.go b/src/cmd/llvmtoolexec/testdata/importrefs/main.go index d2bc60c293fb56..29867b08988652 100644 --- a/src/cmd/llvmtoolexec/testdata/importrefs/main.go +++ b/src/cmd/llvmtoolexec/testdata/importrefs/main.go @@ -21,6 +21,9 @@ func main() { if got := reflect.TypeOf(boxed).Name(); got != "int" { panic(got) } + if got := reflect.TypeOf((*int)(nil)).Elem().Kind(); got != reflect.Int { + panic(got) + } got := fmt.Sprintf("%s/%v", value, boxed) if got != "dep/7" { panic(got) diff --git a/src/cmd/llvmtoolexec/testdata/pclninline/main.go b/src/cmd/llvmtoolexec/testdata/pclninline/main.go new file mode 100644 index 00000000000000..b845e7ccba47b4 --- /dev/null +++ b/src/cmd/llvmtoolexec/testdata/pclninline/main.go @@ -0,0 +1,28 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +func inner() { + capture() +} + +func middle() { + inner() +} + +func outer() { + func() { + middle() + }() +} + +//go:noinline +func capture() { + panic("pcln-inline") +} + +func main() { + outer() +} diff --git a/src/cmd/vendor/github.com/goallc/go-llvm/dibuilder.go b/src/cmd/vendor/github.com/goallc/go-llvm/dibuilder.go index e5128cfec7ff04..368f1f9e8426ba 100644 --- a/src/cmd/vendor/github.com/goallc/go-llvm/dibuilder.go +++ b/src/cmd/vendor/github.com/goallc/go-llvm/dibuilder.go @@ -60,8 +60,17 @@ const ( type DwarfLang uint32 const ( - // http://dwarfstd.org/ShowIssue.php?issue=101014.1&type=open - DW_LANG_Go DwarfLang = 0x0016 + // LLVMDWARFSourceLanguage is a dense C API enum, not the raw DW_LANG + // encoding from the DWARF specification. + DW_LANG_Go DwarfLang = C.LLVMDWARFSourceLanguageGo +) + +type DwarfEmissionKind uint8 + +const ( + DwarfEmissionFull DwarfEmissionKind = iota + DwarfEmissionLineTablesOnly + DwarfEmissionNone ) type DwarfTypeEncoding uint32 @@ -120,6 +129,7 @@ type DICompileUnit struct { RuntimeVersion int SysRoot string SDK string + EmissionKind DwarfEmissionKind } // CreateCompileUnit creates compile unit debug metadata. @@ -136,6 +146,16 @@ func (d *DIBuilder) CreateCompileUnit(cu DICompileUnit) Metadata { defer C.free(unsafe.Pointer(sysroot)) sdk := C.CString(cu.SDK) defer C.free(unsafe.Pointer(sdk)) + emissionKind := C.LLVMDWARFEmissionKind(C.LLVMDWARFEmissionFull) + switch cu.EmissionKind { + case DwarfEmissionFull: + case DwarfEmissionLineTablesOnly: + emissionKind = C.LLVMDWARFEmissionLineTablesOnly + case DwarfEmissionNone: + emissionKind = C.LLVMDWARFEmissionNone + default: + panic("invalid DWARF emission kind") + } result := C.LLVMDIBuilderCreateCompileUnit( d.ref, C.LLVMDWARFSourceLanguage(cu.Language), @@ -145,7 +165,7 @@ func (d *DIBuilder) CreateCompileUnit(cu DICompileUnit) Metadata { flags, C.size_t(len(cu.Flags)), C.unsigned(cu.RuntimeVersion), /*SplitName=*/ nil, 0, - C.LLVMDWARFEmissionFull, + emissionKind, /*DWOId=*/ 0, /*SplitDebugInlining*/ C.LLVMBool(boolToCInt(true)), /*DebugInfoForProfiling*/ C.LLVMBool(boolToCInt(false)), @@ -155,6 +175,13 @@ func (d *DIBuilder) CreateCompileUnit(cu DICompileUnit) Metadata { return Metadata{C: result} } +// CreateDebugLocation creates an exact DILocation, including an optional +// inlinedAt chain. +func (c Context) CreateDebugLocation(line, col uint, scope, inlinedAt Metadata) Metadata { + return Metadata{C: C.LLVMDIBuilderCreateDebugLocation( + c.C, C.uint(line), C.uint(col), scope.C, inlinedAt.C)} +} + // CreateFile creates file debug metadata. func (d *DIBuilder) CreateFile(filename, dir string) Metadata { cfilename := C.CString(filename) diff --git a/src/cmd/vendor/github.com/goallc/go-llvm/ir.go b/src/cmd/vendor/github.com/goallc/go-llvm/ir.go index 9d471a44fab3a9..c24fe620fb7b89 100644 --- a/src/cmd/vendor/github.com/goallc/go-llvm/ir.go +++ b/src/cmd/vendor/github.com/goallc/go-llvm/ir.go @@ -1390,6 +1390,20 @@ func (b Builder) SetCurrentDebugLocation(line, col uint, scope, inlinedAt Metada C.LLVMGoSetCurrentDebugLocation(b.C, C.unsigned(line), C.unsigned(col), scope.C, inlinedAt.C) } +// SetCurrentDebugLocationMetadata sets the exact DILocation inherited by +// subsequently created instructions. A nil value clears it. +func (b Builder) SetCurrentDebugLocationMetadata(loc Metadata) { + C.LLVMSetCurrentDebugLocation2(b.C, loc.C) +} + +func (b Builder) CurrentDebugLocationMetadata() Metadata { + return Metadata{C: C.LLVMGetCurrentDebugLocation2(b.C)} +} + +func (b Builder) ClearCurrentDebugLocation() { + C.LLVMSetCurrentDebugLocation2(b.C, nil) +} + // Get current debug location. Please do not call this function until setting debug location with SetCurrentDebugLocation() func (b Builder) GetCurrentDebugLocation() (loc DebugLoc) { md := C.LLVMGoGetCurrentDebugLocation(b.C) diff --git a/test/codegen/_cgo_llvm_unsafe_args.go b/test/codegen/_cgo_llvm_unsafe_args.go index 59766a8ed5415e..902ed5053f777e 100644 --- a/test/codegen/_cgo_llvm_unsafe_args.go +++ b/test/codegen/_cgo_llvm_unsafe_args.go @@ -21,7 +21,7 @@ func llvmCgoUnsafeSink(*uintptr) // LLVM: {{%.*}} = load i64, ptr [[RESULT]] // LLVM: attributes #[[NOINLINE]] = { {{.*}}noinline // LLVM-OPT-LABEL: define goabi0 i64 @codegen.llvmCgoUnsafeFrame.goallc.abi0( -// LLVM-OPT-SAME: i64 %p) {{.*}} #[[OPT_NOINLINE:[0-9]+]] gc "goallc" +// LLVM-OPT-SAME: i64 %p) {{.*}}#[[OPT_NOINLINE:[0-9]+]] gc "goallc" // LLVM-OPT-NOT: alloca // LLVM-OPT: [[OPT_FRAME:%.*]] = {{.*}}call ptr @llvm.go.abi0.frame() // LLVM-OPT-NOT: llvm.addressofreturnaddress diff --git a/test/codegen/llvm_nilcheck.go b/test/codegen/llvm_nilcheck.go index 90e6a15b4d706d..d48ec94ae50e27 100644 --- a/test/codegen/llvm_nilcheck.go +++ b/test/codegen/llvm_nilcheck.go @@ -48,7 +48,7 @@ package codegen // LLVM-OPT-LABEL: define goabiinternal i64 @codegen.llvmExplicitNilcheck( // LLVM-OPT-NOT: llvm.goallc.nilcheck // LLVM-OPT: icmp eq ptr %p, null -// LLVM-OPT: br i1 {{%.*}}, label %[[OPTNIL:.*]], label %[[OPTCONT:.*]] +// LLVM-OPT: br i1 {{%.*}}, label %[[OPTNIL:[^,]+]], label %[[OPTCONT:[^,]+]], !dbg // LLVM-OPT: [[OPTNIL]]: // LLVM-OPT-NEXT: call goabiinternal void @runtime.panicmem() // LLVM-OPT-NEXT: br label %[[OPTCONT]] diff --git a/test/llvm_tests.json b/test/llvm_tests.json index 7328bc95644242..1ada77c4d836c9 100644 --- a/test/llvm_tests.json +++ b/test/llvm_tests.json @@ -1165,20 +1165,20 @@ "unsafebuiltins.go": "defer/recover execution repeatedly qualified through LLVM O2 compile, link, and execution", "abi/open_defer_1.go": "open-coded defer registration and return repeatedly pass LLVM O2 compile, GoObj link, and execution on both CI architectures", "chan/doubleselect.go": "double select execution repeatedly passes LLVM O2 compile, GoObj link, and execution on both CI architectures", + "devirtualization_nil_panics.go": "expanded LLVM nil checks preserve the source call DILocation through LLVM O2, GoObj pcline emission, recover, and execution", "fixedbugs/issue19658.go": "closure and interface regression repeatedly passes LLVM O2 compile, GoObj link, and execution on both CI architectures", "fixedbugs/issue26411.go": "compiler-generated stack address observations across regexp and bytes calls repeatedly pass LLVM O2 compile, GoObj link, and execution on both CI architectures", "fixedbugs/issue28688.go": "soft-float copy regression repeatedly passes LLVM O2 compile, GoObj link, and execution on both CI architectures", + "fixedbugs/issue73916.go": "frontend inline metadata preserves the deferred function as a logical frame through LLVM O2, GoObj PCLN emission, recover, and execution", + "fixedbugs/issue73916b.go": "frontend inline metadata preserves nested logical frames through LLVM O2, GoObj PCLN emission, recover, and execution", + "fixedbugs/issue73917.go": "frontend inline metadata preserves an inlined value-method wrapper through LLVM O2, GoObj PCLN emission, recover, and execution", + "fixedbugs/issue73920.go": "frontend inline metadata preserves an inlined pointer-method wrapper through LLVM O2, GoObj PCLN emission, recover, and execution", "fixedbugs/issue78081.go": "concurrent GC and stack-shrink regression repeatedly passes LLVM O2 compile, GoObj link, and execution on both CI architectures", + "inline_caller.go": "LLVM GoObj pcfile and pcline metadata preserves runtime.Caller source locations through LLVM O2, linking, and execution", + "inline_callers.go": "LLVM GoObj pcinline metadata preserves logical inline frames for runtime.Callers through LLVM O2, linking, and execution", "zerodivide.go": "defer/recover execution repeatedly qualified through LLVM O2 compile, link, and execution" }, "graylist": { - "devirtualization_nil_panics.go": "panic line checks require LLVM pcfile and pcline emission from the pending Go #117 and LLVM #62 changes", - "fixedbugs/issue73916.go": "recover needs frontend pcinline metadata to count the inlined deferred function as a logical frame", - "fixedbugs/issue73916b.go": "recover needs frontend pcinline metadata to count nested inlined logical frames", - "fixedbugs/issue73917.go": "recover needs frontend pcinline metadata through an inlined value-method wrapper", - "fixedbugs/issue73920.go": "recover needs frontend pcinline metadata through an inlined pointer-method wrapper", - "inline_caller.go": "LLVM GoObj does not yet emit source line metadata for runtime.Caller", - "inline_callers.go": "LLVM GoObj does not yet emit logical inline frames for runtime.Callers", "maymorestack.go": "LLVM-generated stack checks do not yet call the configured mayMoreStack hook", "*": "LLVM lowering or runtime ABI support has not reached this test yet" },