Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/cmd/compile/internal/ssa/llvmdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
265 changes: 265 additions & 0 deletions src/cmd/compile/internal/ssa/llvmdebug.go
Original file line number Diff line number Diff line change
@@ -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))
}
13 changes: 13 additions & 0 deletions src/cmd/compile/internal/ssa/ssa2llvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -3552,5 +3564,6 @@ func Output(fileName string) error {
emitGoObjCgoModuleAsm()
goObjImportsWritten = true
}
finalizeLLVMDebugInfo()
return llvm.LLVMPrintModuleToFile(CurrentModule, fileName)
}
7 changes: 7 additions & 0 deletions src/cmd/compile/internal/ssa/ssa2llvm_nilcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
Expand All @@ -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)
}
Expand Down
11 changes: 11 additions & 0 deletions src/cmd/llvmplugin/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 "$<TARGET_FILE:GoALLCStatepoints>"
--input "${CMAKE_CURRENT_SOURCE_DIR}/testdata/debug-inline.ll"
)

add_test(
NAME GoALLCStatepoints.ZeroLengthPointerArray
COMMAND
Expand Down
Loading
Loading