From 819d4513e845e6284d19a325b057b06aa8e93c82 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 22 May 2026 11:05:57 +0800 Subject: [PATCH 1/5] runtime: track caller frames for LLGo metadata --- cl/compile.go | 6 + cl/instr.go | 75 ++++++++-- cl/rewrite_internal_test.go | 6 +- runtime/internal/lib/runtime/extern.go | 12 +- .../lib/runtime/pprof_runtime_stub_llgo.go | 2 +- runtime/internal/lib/runtime/symtab.go | 79 +++++++++- runtime/internal/runtime/caller.go | 138 ++++++++++++++++++ ssa/package.go | 4 + test/go/runtime_caller_test.go | 117 +++++++++++++++ test/goroot/xfail.yaml | 10 -- 10 files changed, 415 insertions(+), 34 deletions(-) create mode 100644 runtime/internal/runtime/caller.go create mode 100644 test/go/runtime_caller_test.go diff --git a/cl/compile.go b/cl/compile.go index 5b6d7c9fbe..41750fa7af 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -559,6 +559,9 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do var instrs = block.Instrs[n:] var ret = fn.Block(block.Index) b.SetBlock(ret) + if block.Index == 0 && p.shouldTrackCallerFrames() { + p.pushCallerFrame(b, block.Parent()) + } if block.Index == 0 && enableCallTracing && !strings.HasPrefix(fn.Name(), "github.com/goplus/llgo/runtime/internal/runtime.Print") { b.Printf("call " + fn.Name() + "\n\x00") } @@ -1141,6 +1144,9 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { if p.returnNeedsImplicitRunDefers(v) { b.RunDefers() } + if p.shouldTrackCallerFrames() { + p.popCallerFrame(b) + } b.Return(results...) case *ssa.If: fn := p.fn diff --git a/cl/instr.go b/cl/instr.go index 0184f1c7fd..932a883bdd 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -799,6 +799,54 @@ func (p *context) sourceLine(filename string, line int) (string, bool) { return lines[line-1], true } +func (p *context) shouldTrackCallerFrames() bool { + if p == nil || p.pkg == nil || p.fn == nil { + return false + } + pkgPath := p.pkg.Path() + return pkgPath != llssa.PkgRuntime && + pkgPath != "runtime" && + !strings.HasPrefix(pkgPath, "github.com/goplus/llgo/runtime/internal/") +} + +func (p *context) pushCallerFrame(b llssa.Builder, fn *ssa.Function) { + if fn == nil { + return + } + pos := p.fset.Position(fn.Pos()) + entry := b.Convert(p.prog.Uintptr(), p.fn.Expr) + b.Call( + p.pkg.RuntimeFunc("PushCallerFrame"), + entry, + b.Str(runtimeFrameName(p.fn.Name())), + b.Str(pos.Filename), + p.prog.IntVal(uint64(pos.Line), p.prog.Int()), + ) +} + +func (p *context) setCallerLine(b llssa.Builder, pos token.Pos) { + if !p.shouldTrackCallerFrames() { + return + } + line := p.fset.Position(pos).Line + if line <= 0 { + return + } + b.Call(p.pkg.RuntimeFunc("SetCallerLine"), p.prog.IntVal(uint64(line), p.prog.Int())) +} + +func (p *context) popCallerFrame(b llssa.Builder) { + b.Call(p.pkg.RuntimeFunc("PopCallerFrame")) +} + +func runtimeFrameName(name string) string { + const commandLineArguments = "command-line-arguments." + if strings.HasPrefix(name, commandLineArguments) { + return "main." + name[len(commandLineArguments):] + } + return name +} + // ----------------------------------------------------------------------------- type explicitDeferStack struct { @@ -838,7 +886,8 @@ func (p *context) deferStackOwner(fn *ssa.Function) llssa.Function { return owner } -func (p *context) emitDo(b llssa.Builder, act llssa.DoAction, ds *explicitDeferStack, fn llssa.Expr, buildCall func(llssa.Builder, llssa.Expr, ...llssa.Expr) llssa.Expr, args ...llssa.Expr) llssa.Expr { +func (p *context) emitDo(b llssa.Builder, act llssa.DoAction, pos token.Pos, ds *explicitDeferStack, fn llssa.Expr, buildCall func(llssa.Builder, llssa.Expr, ...llssa.Expr) llssa.Expr, args ...llssa.Expr) llssa.Expr { + p.setCallerLine(b, pos) if ds != nil { b.DeferTo(ds.owner, ds.stack, fn, buildCall, args...) return llssa.Nil @@ -856,7 +905,7 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm hasVArg = fnHasVArg } args := p.compileValues(b, call.Args, hasVArg) - ret = p.emitDo(b, act, ds, fn, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, call.Pos(), ds, fn, llssa.Builder.Call, args...) return } kind := p.funcKind(cv) @@ -879,7 +928,7 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm } } args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Builtin(fn), llssa.Builder.Call, args...) + ret = p.emitDo(b, act, call.Pos(), ds, llssa.Builtin(fn), llssa.Builder.Call, args...) case *ssa.Function: aFn, pyFn, ftype := p.compileFunction(cv) // TODO(xsw): check ca != llssa.Call @@ -888,13 +937,13 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm p.inCFunc = true args := p.compileValues(b, args, kind) p.inCFunc = false - ret = p.emitDo(b, act, ds, aFn.Expr, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, call.Pos(), ds, aFn.Expr, llssa.Builder.Call, args...) case goFunc: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, aFn.Expr, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, call.Pos(), ds, aFn.Expr, llssa.Builder.Call, args...) case pyFunc: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, pyFn.Expr, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, call.Pos(), ds, pyFn.Expr, llssa.Builder.Call, args...) case llgoPyList: args := p.compileValues(b, args, fnHasVArg) ret = b.PyList(args...) @@ -968,33 +1017,33 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm b.Unreachable() case llgoAtomicLoad: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, call.Pos(), ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomicLoad(b, args) }, args...) case llgoAtomicStore: args := p.compileValues(b, args, kind) - p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + p.emitDo(b, act, call.Pos(), ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomicStore(b, args) }, args...) case llgoAtomicCmpXchg: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, call.Pos(), ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomicCmpXchg(b, args) }, args...) case llgoAtomicCmpXchgOK: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, call.Pos(), ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomicCmpXchgOK(b, args) }, args...) case llgoAtomicAddReturnNew: args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, call.Pos(), ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return b.BinOp(token.ADD, p.atomic(b, llssa.OpAdd, args), args[1]) }, args...) default: if ftype >= llgoAtomicOpBase && ftype <= llgoAtomicOpLast { args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { + ret = p.emitDo(b, act, call.Pos(), ds, llssa.Nil, func(b llssa.Builder, _ llssa.Expr, args ...llssa.Expr) llssa.Expr { return p.atomic(b, llssa.AtomicOp(ftype-llgoAtomicOpBase), args) }, args...) } else { @@ -1004,7 +1053,7 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm default: fn := p.compileValue(b, cv) args := p.compileValues(b, args, kind) - ret = p.emitDo(b, act, ds, fn, llssa.Builder.Call, args...) + ret = p.emitDo(b, act, call.Pos(), ds, fn, llssa.Builder.Call, args...) } return } diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index 35112239f6..9d25dcedff 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -328,8 +328,8 @@ func TestEmitDoWithExplicitDeferStack(t *testing.T) { b.SetBlockEx(owner.Block(0), llssa.BeforeLast, true) ctx := &context{} - ctx.emitDo(b, llssa.DeferInLoop, &explicitDeferStack{stack: stack, owner: owner}, callee.Expr, llssa.Builder.Call) - ctx.emitDo(b, llssa.DeferAlways, nil, callee.Expr, llssa.Builder.Call) + ctx.emitDo(b, llssa.DeferInLoop, token.NoPos, &explicitDeferStack{stack: stack, owner: owner}, callee.Expr, llssa.Builder.Call) + ctx.emitDo(b, llssa.DeferAlways, token.NoPos, nil, callee.Expr, llssa.Builder.Call) b.DeferStackDrain() b.RunDefers() b.Return() @@ -465,7 +465,7 @@ func TestEmitDoWithoutExplicitDeferStack(t *testing.T) { b := fn.MakeBody(1) ctx := &context{} - got := ctx.emitDo(b, llssa.Call, nil, callee.Expr, llssa.Builder.Call) + got := ctx.emitDo(b, llssa.Call, token.NoPos, nil, callee.Expr, llssa.Builder.Call) if got.IsNil() { t.Fatal("emitDo without explicit defer stack should return direct call result") } diff --git a/runtime/internal/lib/runtime/extern.go b/runtime/internal/lib/runtime/extern.go index 1fb397dd8a..51863461a7 100644 --- a/runtime/internal/lib/runtime/extern.go +++ b/runtime/internal/lib/runtime/extern.go @@ -6,22 +6,24 @@ package runtime import ( clitedebug "github.com/goplus/llgo/runtime/internal/clite/debug" + rtdebug "github.com/goplus/llgo/runtime/internal/runtime" ) func Caller(skip int) (pc uintptr, file string, line int, ok bool) { - // llgo currently doesn't have reliable source file/line mapping from PC. - // Return a stable placeholder location so stdlib log/testing can proceed. - var pcs [1]uintptr - if Callers(skip+1, pcs[:]) < 1 { + frame, ok := rtdebug.Caller(skip) + if !ok { return 0, "", 0, false } - return pcs[0], "???", 1, true + return frame.PC, frame.File, frame.Line, true } func Callers(skip int, pc []uintptr) int { if len(pc) == 0 { return 0 } + if n := rtdebug.Callers(skip, pc); n > 0 { + return n + } n := 0 clitedebug.StackTrace(skip, func(fr *clitedebug.Frame) bool { if n >= len(pc) { diff --git a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go index 8f7045430b..49c0374c22 100644 --- a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go +++ b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go @@ -54,5 +54,5 @@ func NumGoroutine() int { func SetCPUProfileRate(hz int) {} func FuncForPC(pc uintptr) *Func { - return nil + return funcForPC(pc) } diff --git a/runtime/internal/lib/runtime/symtab.go b/runtime/internal/lib/runtime/symtab.go index 57ac543cbf..ae39f32fb6 100644 --- a/runtime/internal/lib/runtime/symtab.go +++ b/runtime/internal/lib/runtime/symtab.go @@ -5,10 +5,12 @@ package runtime import ( + "strings" "unsafe" c "github.com/goplus/llgo/runtime/internal/clite" clitedebug "github.com/goplus/llgo/runtime/internal/clite/debug" + rtdebug "github.com/goplus/llgo/runtime/internal/runtime" ) // Frames may be used to get function/file/line information for a @@ -119,6 +121,19 @@ func (ci *Frames) Next() (frame Frame, more bool) { } else { pc, ci.callers = ci.callers[0], ci.callers[1:] } + if known, ok := rtdebug.FrameForPC(pc); ok { + fn := newFunc(known.Function, known.Entry, known.File, known.StartLine) + ci.frames = append(ci.frames, Frame{ + PC: pc, + Func: fn, + Function: known.Function, + File: known.File, + Line: known.Line, + startLine: known.StartLine, + Entry: known.Entry, + }) + continue + } info := &clitedebug.Info{} if clitedebug.Addrinfo(unsafe.Pointer(pc), info) == 0 { ci.frames = append(ci.frames, Frame{ @@ -135,8 +150,10 @@ func (ci *Frames) Next() (frame Frame, more bool) { if fn == "" { fn = unknownFunctionName(pc) } + fn = normalizeLLGoSymbolName(fn) ci.frames = append(ci.frames, Frame{ PC: pc, + Func: newFunc(fn, uintptr(info.Saddr), "", 0), Function: fn, File: "", Line: 0, @@ -176,11 +193,69 @@ func CallersFrames(callers []uintptr) *Frames { // A Func represents a Go function in the running binary. type Func struct { - opaque struct{} // unexported field to disallow conversions + name string + entry uintptr + file string + startLine int } func (f *Func) Name() string { - panic("todo") + if f == nil { + return "" + } + return f.name +} + +func (f *Func) Entry() uintptr { + if f == nil { + return 0 + } + return f.entry +} + +func (f *Func) FileLine(pc uintptr) (file string, line int) { + if f == nil { + return "", 0 + } + if frame, ok := rtdebug.FrameForPC(pc); ok { + return frame.File, frame.Line + } + return f.file, f.startLine +} + +func newFunc(name string, entry uintptr, file string, startLine int) *Func { + return &Func{name: name, entry: entry, file: file, startLine: startLine} +} + +func funcForPC(pc uintptr) *Func { + if frame, ok := rtdebug.FrameForPC(pc); ok { + return newFunc(frame.Function, frame.Entry, frame.File, frame.StartLine) + } + if pc > 0 { + if frame, ok := rtdebug.FrameForPC(pc - 1); ok { + return newFunc(frame.Function, frame.Entry, frame.File, frame.StartLine) + } + } + if frame, ok := rtdebug.FrameForPC(pc + 1); ok { + return newFunc(frame.Function, frame.Entry, frame.File, frame.StartLine) + } + info := &clitedebug.Info{} + if clitedebug.Addrinfo(unsafe.Pointer(pc), info) == 0 { + return nil + } + fn := safeGoString(info.Sname, "") + if fn == "" { + return nil + } + return newFunc(normalizeLLGoSymbolName(fn), uintptr(info.Saddr), "", 0) +} + +func normalizeLLGoSymbolName(name string) string { + const commandLineArguments = "command-line-arguments." + if strings.HasPrefix(name, commandLineArguments) { + return "main." + name[len(commandLineArguments):] + } + return name } // moduledata records information about the layout of the executable diff --git a/runtime/internal/runtime/caller.go b/runtime/internal/runtime/caller.go new file mode 100644 index 0000000000..097a98db04 --- /dev/null +++ b/runtime/internal/runtime/caller.go @@ -0,0 +1,138 @@ +/* + * 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 runtime + +import "unsafe" + +type CallerFrame struct { + PC uintptr + Entry uintptr + Function string + File string + Line int + StartLine int +} + +var ( + callerFrames []CallerFrame + pcFrames []*CallerFrame +) + +var ( + runtimeCallersFrame = CallerFrame{Function: "runtime.Callers"} + runtimeMainFrame = CallerFrame{Function: "runtime.main"} + runtimeGoexitFrame = CallerFrame{Function: "runtime.goexit"} +) + +func PushCallerFrame(entry uintptr, name, file string, startLine int) { + callerFrames = append(callerFrames, CallerFrame{ + PC: entry, + Entry: entry, + Function: name, + File: file, + Line: startLine, + StartLine: startLine, + }) +} + +func SetCallerLine(line int) { + if line <= 0 || len(callerFrames) == 0 { + return + } + callerFrames[len(callerFrames)-1].Line = line +} + +func PopCallerFrame() { + if len(callerFrames) == 0 { + return + } + callerFrames = callerFrames[:len(callerFrames)-1] +} + +func Caller(skip int) (CallerFrame, bool) { + if skip < 0 { + return CallerFrame{}, false + } + if skip < len(callerFrames) { + return captureFrame(callerFrames[len(callerFrames)-1-skip], false), true + } + switch skip - len(callerFrames) { + case 0: + return captureFrame(runtimeMainFrame, false), true + case 1: + return captureFrame(runtimeGoexitFrame, false), true + default: + return CallerFrame{}, false + } +} + +func Callers(skip int, pcs []uintptr) int { + if skip < 0 { + skip = 0 + } + n := 0 + add := func(frame CallerFrame) bool { + if skip > 0 { + skip-- + return true + } + if n >= len(pcs) { + return false + } + pcs[n] = captureFrame(frame, true).PC + n++ + return true + } + if !add(runtimeCallersFrame) { + return n + } + for i := len(callerFrames) - 1; i >= 0; i-- { + if !add(callerFrames[i]) { + return n + } + } + _ = add(runtimeMainFrame) + _ = add(runtimeGoexitFrame) + return n +} + +func FrameForPC(pc uintptr) (CallerFrame, bool) { + if pc == 0 { + return CallerFrame{}, false + } + for _, frame := range pcFrames { + if uintptr(unsafe.Pointer(frame)) == pc || uintptr(unsafe.Pointer(frame))+1 == pc { + return *frame, true + } + } + return CallerFrame{}, false +} + +func captureFrame(frame CallerFrame, callersPC bool) CallerFrame { + rec := new(CallerFrame) + *rec = frame + pc := uintptr(unsafe.Pointer(rec)) + if callersPC { + pc++ + } + rec.PC = pc + if rec.Entry == 0 { + rec.Entry = pc + } + pcFrames = append(pcFrames, rec) + return *rec +} diff --git a/ssa/package.go b/ssa/package.go index 0db719c157..e3a1f6cc94 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -788,6 +788,10 @@ func (p Package) rtFunc(fnName string) Expr { return p.NewFunc(name, sig, InGo).Expr } +func (p Package) RuntimeFunc(fnName string) Expr { + return p.rtFunc(fnName) +} + func (p Package) cFunc(fullName string, sig *types.Signature) Expr { return p.NewFunc(fullName, sig, InC).Expr } diff --git a/test/go/runtime_caller_test.go b/test/go/runtime_caller_test.go new file mode 100644 index 0000000000..0c244a6b38 --- /dev/null +++ b/test/go/runtime_caller_test.go @@ -0,0 +1,117 @@ +package gotest + +import ( + "runtime" + "strings" + "testing" +) + +type runtimeCallerSnapshot struct { + pc uintptr + file string + line int + ok bool +} + +func TestRuntimeCallerMetadata(t *testing.T) { + tests := []struct { + skip int + nameSuffix string + line int + }{ + {0, ".runtimeCallerLeaf", 101}, + {1, ".runtimeCallerMid", 111}, + {2, ".runtimeCallerTop", 121}, + } + for _, tt := range tests { + got := runtimeCallerTop(tt.skip) + if !got.ok { + t.Fatalf("runtime.Caller(%d) failed", tt.skip) + } + if !strings.HasSuffix(got.file, "runtime_caller_metadata.go") { + t.Fatalf("runtime.Caller(%d) file = %q", tt.skip, got.file) + } + if got.line != tt.line { + t.Fatalf("runtime.Caller(%d) line = %d, want %d", tt.skip, got.line, tt.line) + } + fn := runtime.FuncForPC(got.pc) + if fn == nil { + t.Fatalf("FuncForPC(runtime.Caller(%d) pc) = nil", tt.skip) + } + if name := fn.Name(); !strings.HasSuffix(name, tt.nameSuffix) { + t.Fatalf("FuncForPC(runtime.Caller(%d) pc).Name = %q, want suffix %q", tt.skip, name, tt.nameSuffix) + } + } +} + +func TestRuntimeCallersFramesMetadata(t *testing.T) { + frames := runtimeCallersTop(0) + want := []struct { + nameSuffix string + line int + }{ + {"runtime.Callers", 0}, + {".runtimeCallersLeaf", 202}, + {".runtimeCallersMid", 211}, + {".runtimeCallersTop", 221}, + } + if len(frames) < len(want) { + t.Fatalf("runtime.CallersFrames returned %d frames, want at least %d: %#v", len(frames), len(want), frames) + } + for i, tt := range want { + if name := frames[i].Function; !strings.HasSuffix(name, tt.nameSuffix) { + t.Fatalf("frame %d function = %q, want suffix %q", i, name, tt.nameSuffix) + } + if tt.line != 0 && frames[i].Line != tt.line { + t.Fatalf("frame %d line = %d, want %d", i, frames[i].Line, tt.line) + } + if frames[i].Func == nil { + continue + } + if name := frames[i].Func.Name(); !strings.HasSuffix(name, tt.nameSuffix) { + t.Fatalf("frame %d Func.Name = %q, want suffix %q", i, name, tt.nameSuffix) + } + } +} + +//line runtime_caller_metadata.go:100 +func runtimeCallerLeaf(skip int) runtimeCallerSnapshot { + pc, file, line, ok := runtime.Caller(skip) + return runtimeCallerSnapshot{pc: pc, file: file, line: line, ok: ok} +} + +//line runtime_caller_metadata.go:110 +func runtimeCallerMid(skip int) runtimeCallerSnapshot { + return runtimeCallerLeaf(skip) +} + +//line runtime_caller_metadata.go:120 +func runtimeCallerTop(skip int) runtimeCallerSnapshot { + return runtimeCallerMid(skip) +} + +//line runtime_callers_metadata.go:200 +func runtimeCallersLeaf(skip int) []runtime.Frame { + var pcs [16]uintptr + n := runtime.Callers(skip, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + var out []runtime.Frame + for { + frame, more := frames.Next() + out = append(out, frame) + if !more { + break + } + } + return out +} + +//line runtime_callers_metadata.go:210 +func runtimeCallersMid(skip int) []runtime.Frame { + return runtimeCallersLeaf(skip) +} + +//line runtime_callers_metadata.go:220 +func runtimeCallersTop(skip int) []runtime.Frame { + return runtimeCallersMid(skip) +} diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index a5a66d10b4..80bd817d5f 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -3172,16 +3172,6 @@ xfails: directive: run case: fixedbugs/issue8606b.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: darwin/arm64 - directive: run - case: inline_caller.go - reason: go1.24 goroot ci-mode run failure on darwin/arm64 - - version: go1.24 - platform: darwin/arm64 - directive: run - case: inline_callers.go - reason: go1.24 goroot ci-mode run failure on darwin/arm64 - version: go1.24 platform: darwin/arm64 directive: run From ae868e0979da35f543cfac0ead78fb32e394a3ce Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 22 May 2026 17:14:51 +0800 Subject: [PATCH 2/5] runtime: limit caller frame instrumentation --- cl/compile.go | 61 +++++++++++++++++++++++ cl/instr.go | 69 +++++++++++++++++++++++++- runtime/internal/lib/runtime/symtab.go | 3 +- 3 files changed, 130 insertions(+), 3 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index 41750fa7af..53f413e1a1 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -191,6 +191,8 @@ type context struct { rewrites map[string]string embedMap goembed.VarMap embedInits []embedInit + + trackCallerFrames bool } func (p *context) rewriteValue(name string) (string, bool) { @@ -206,6 +208,63 @@ func (p *context) rewriteValue(name string) (string, bool) { return val, ok } +func filesUseRuntimeCaller(files []*ast.File) bool { + for _, file := range files { + runtimeNames := make(map[string]struct{}) + var dotRuntime bool + for _, imp := range file.Imports { + path, err := strconv.Unquote(imp.Path.Value) + if err != nil || path != "runtime" { + continue + } + name := "runtime" + if imp.Name != nil { + switch imp.Name.Name { + case ".": + dotRuntime = true + continue + case "_": + continue + default: + name = imp.Name.Name + } + } + runtimeNames[name] = struct{}{} + } + if len(runtimeNames) == 0 && !dotRuntime { + continue + } + found := false + ast.Inspect(file, func(n ast.Node) bool { + if found { + return false + } + switch n := n.(type) { + case *ast.SelectorExpr: + if !isRuntimeCallerName(n.Sel.Name) { + return true + } + if ident, ok := n.X.(*ast.Ident); ok { + if _, ok := runtimeNames[ident.Name]; ok { + found = true + return false + } + } + case *ast.Ident: + if dotRuntime && isRuntimeCallerName(n.Name) { + found = true + return false + } + } + return true + }) + if found { + return true + } + } + return false +} + // isStringPtrType checks if typ is a pointer to the basic string type (*string). // This is used to validate that -ldflags -X can only rewrite variables of type *string, // not derived string types like "type T string". @@ -1477,6 +1536,8 @@ func newPackageEx(prog llssa.Program, patches Patches, rewrites map[string]strin }, cgoSymbols: make([]string, 0, 128), rewrites: rewrites, + + trackCallerFrames: filesUseRuntimeCaller(files) || packageUsesRuntimeCaller(pkg), } if embedMap != nil { ctx.embedMap = *embedMap diff --git a/cl/instr.go b/cl/instr.go index 932a883bdd..d63ba0ac89 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -800,15 +800,82 @@ func (p *context) sourceLine(filename string, line int) (string, bool) { } func (p *context) shouldTrackCallerFrames() bool { - if p == nil || p.pkg == nil || p.fn == nil { + if p == nil || p.pkg == nil || p.fn == nil || !p.trackCallerFrames { + return false + } + if target := p.prog.Target(); target != nil && (target.Target != "" || target.GOARCH == "wasm") { return false } pkgPath := p.pkg.Path() + return canTrackCallerFramesForPackage(pkgPath) +} + +func canTrackCallerFramesForPackage(pkgPath string) bool { return pkgPath != llssa.PkgRuntime && pkgPath != "runtime" && + !isStandardLibraryPackage(pkgPath) && !strings.HasPrefix(pkgPath, "github.com/goplus/llgo/runtime/internal/") } +func isStandardLibraryPackage(pkgPath string) bool { + return pkgPath != "command-line-arguments" && !strings.Contains(pkgPath, ".") +} + +func packageUsesRuntimeCaller(pkg *ssa.Package) bool { + if pkg == nil { + return false + } + for _, member := range pkg.Members { + fn, ok := member.(*ssa.Function) + if ok && fnUsesRuntimeCaller(fn) { + return true + } + } + return false +} + +func fnUsesRuntimeCaller(fn *ssa.Function) bool { + if fn == nil { + return false + } + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + call, ok := instr.(ssa.CallInstruction) + if !ok { + continue + } + if isRuntimeCallerFunc(call.Common().StaticCallee()) { + return true + } + } + } + for _, anon := range fn.AnonFuncs { + if fnUsesRuntimeCaller(anon) { + return true + } + } + return false +} + +func isRuntimeCallerFunc(fn *ssa.Function) bool { + if fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + return false + } + if fn.Pkg.Pkg.Path() != "runtime" { + return false + } + return isRuntimeCallerName(fn.Name()) +} + +func isRuntimeCallerName(name string) bool { + switch name { + case "Caller", "Callers", "CallersFrames", "FuncForPC": + return true + default: + return false + } +} + func (p *context) pushCallerFrame(b llssa.Builder, fn *ssa.Function) { if fn == nil { return diff --git a/runtime/internal/lib/runtime/symtab.go b/runtime/internal/lib/runtime/symtab.go index ae39f32fb6..90bedec697 100644 --- a/runtime/internal/lib/runtime/symtab.go +++ b/runtime/internal/lib/runtime/symtab.go @@ -5,7 +5,6 @@ package runtime import ( - "strings" "unsafe" c "github.com/goplus/llgo/runtime/internal/clite" @@ -252,7 +251,7 @@ func funcForPC(pc uintptr) *Func { func normalizeLLGoSymbolName(name string) string { const commandLineArguments = "command-line-arguments." - if strings.HasPrefix(name, commandLineArguments) { + if len(name) >= len(commandLineArguments) && name[:len(commandLineArguments)] == commandLineArguments { return "main." + name[len(commandLineArguments):] } return name From 329e2dae9d6deee897c79e6dc1d01b0b413decd6 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 24 May 2026 12:15:07 +0800 Subject: [PATCH 3/5] runtime: preserve caller frames across recovered panics --- cl/compile.go | 6 +- cl/instr.go | 25 +++++++-- runtime/internal/runtime/caller.go | 10 ++-- test/go/runtime_caller_test.go | 88 ++++++++++++++++++++++++++++++ test/goroot/xfail.yaml | 10 ---- 5 files changed, 119 insertions(+), 20 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index 53f413e1a1..4036241016 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -193,6 +193,7 @@ type context struct { embedInits []embedInit trackCallerFrames bool + callerFrameMark llssa.Expr } func (p *context) rewriteValue(name string) (string, bool) { @@ -492,12 +493,13 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun dbgEnabled := enableDbg && (f == nil || f.Origin() == nil) dbgSymsEnabled := enableDbgSyms && (f == nil || f.Origin() == nil) p.inits = append(p.inits, func() { - oldFn, oldGoFn := p.fn, p.goFn + oldFn, oldGoFn, oldCallerFrameMark := p.fn, p.goFn, p.callerFrameMark p.fn = fn p.goFn = f + p.callerFrameMark = llssa.Nil p.state = state // restore pkgState when compiling funcBody defer func() { - p.fn, p.goFn = oldFn, oldGoFn + p.fn, p.goFn, p.callerFrameMark = oldFn, oldGoFn, oldCallerFrameMark }() p.phis = nil if dbgSymsEnabled { diff --git a/cl/instr.go b/cl/instr.go index d63ba0ac89..cebb1991d2 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -882,7 +882,7 @@ func (p *context) pushCallerFrame(b llssa.Builder, fn *ssa.Function) { } pos := p.fset.Position(fn.Pos()) entry := b.Convert(p.prog.Uintptr(), p.fn.Expr) - b.Call( + p.callerFrameMark = b.Call( p.pkg.RuntimeFunc("PushCallerFrame"), entry, b.Str(runtimeFrameName(p.fn.Name())), @@ -903,15 +903,31 @@ func (p *context) setCallerLine(b llssa.Builder, pos token.Pos) { } func (p *context) popCallerFrame(b llssa.Builder) { - b.Call(p.pkg.RuntimeFunc("PopCallerFrame")) + if p.callerFrameMark.IsNil() { + return + } + b.Call(p.pkg.RuntimeFunc("PopCallerFrame"), p.callerFrameMark) } func runtimeFrameName(name string) string { const commandLineArguments = "command-line-arguments." if strings.HasPrefix(name, commandLineArguments) { - return "main." + name[len(commandLineArguments):] + name = "main." + name[len(commandLineArguments):] } - return name + return normalizeRuntimeAnonFuncName(name) +} + +func normalizeRuntimeAnonFuncName(name string) string { + dollar := strings.LastIndexByte(name, '$') + if dollar < 0 || dollar == len(name)-1 { + return name + } + for i := dollar + 1; i < len(name); i++ { + if name[i] < '0' || name[i] > '9' { + return name + } + } + return name[:dollar] + ".func" + name[dollar+1:] } // ----------------------------------------------------------------------------- @@ -963,6 +979,7 @@ func (p *context) emitDo(b llssa.Builder, act llssa.DoAction, pos token.Pos, ds } func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallCommon, ds *explicitDeferStack) (ret llssa.Expr) { + p.setCallerLine(b, call.Pos()) cv := call.Value if mthd := call.Method; mthd != nil { o := p.compileValue(b, cv) diff --git a/runtime/internal/runtime/caller.go b/runtime/internal/runtime/caller.go index 097a98db04..19a826ab78 100644 --- a/runtime/internal/runtime/caller.go +++ b/runtime/internal/runtime/caller.go @@ -38,7 +38,8 @@ var ( runtimeGoexitFrame = CallerFrame{Function: "runtime.goexit"} ) -func PushCallerFrame(entry uintptr, name, file string, startLine int) { +func PushCallerFrame(entry uintptr, name, file string, startLine int) int { + mark := len(callerFrames) callerFrames = append(callerFrames, CallerFrame{ PC: entry, Entry: entry, @@ -47,6 +48,7 @@ func PushCallerFrame(entry uintptr, name, file string, startLine int) { Line: startLine, StartLine: startLine, }) + return mark } func SetCallerLine(line int) { @@ -56,11 +58,11 @@ func SetCallerLine(line int) { callerFrames[len(callerFrames)-1].Line = line } -func PopCallerFrame() { - if len(callerFrames) == 0 { +func PopCallerFrame(mark int) { + if mark < 0 || mark > len(callerFrames) { return } - callerFrames = callerFrames[:len(callerFrames)-1] + callerFrames = callerFrames[:mark] } func Caller(skip int) (CallerFrame, bool) { diff --git a/test/go/runtime_caller_test.go b/test/go/runtime_caller_test.go index 0c244a6b38..0f465db711 100644 --- a/test/go/runtime_caller_test.go +++ b/test/go/runtime_caller_test.go @@ -74,6 +74,94 @@ func TestRuntimeCallersFramesMetadata(t *testing.T) { } } +type runtimeCallerNilIface interface{ runtimeCallerNilMethod() } + +type runtimeCallerNilImpl struct{} + +func (*runtimeCallerNilImpl) runtimeCallerNilMethod() {} + +func TestRuntimeCallersFramesRecoveredNilPanicMetadata(t *testing.T) { + tests := []struct { + nameSuffix string + line int + run func() (runtime.Frame, bool) + }{ + {".runtimeRecoveredNilPanicClosureFrame.func1", 303, runtimeRecoveredNilPanicClosureFrame}, + {".runtimeRecoveredNilPanicSplitClosureFrame.func1", 314, runtimeRecoveredNilPanicSplitClosureFrame}, + } + for _, tt := range tests { + frame, ok := tt.run() + if !ok { + t.Fatalf("CallersFrames did not include %s", tt.nameSuffix) + } + if name := frame.Function; !strings.HasSuffix(name, tt.nameSuffix) { + t.Fatalf("recovered panic frame function = %q, want suffix %q", name, tt.nameSuffix) + } + if frame.Func == nil { + t.Fatalf("recovered panic frame Func = nil for %s", tt.nameSuffix) + } + if name := frame.Func.Name(); !strings.HasSuffix(name, tt.nameSuffix) { + t.Fatalf("recovered panic frame Func.Name = %q, want suffix %q", name, tt.nameSuffix) + } + if frame.Line != tt.line { + t.Fatalf("recovered panic frame line = %d, want %d", frame.Line, tt.line) + } + } + + pc, _, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller(0) failed after recovered panic") + } + fn := runtime.FuncForPC(pc) + if fn == nil { + t.Fatal("FuncForPC(runtime.Caller(0) pc) = nil after recovered panic") + } + if name := fn.Name(); !strings.HasSuffix(name, ".TestRuntimeCallersFramesRecoveredNilPanicMetadata") { + t.Fatalf("runtime.Caller(0) after recovered panic = %q, want current test frame", name) + } +} + +func runtimeRecoveredNilPanicFrame(wantSuffix string, f func()) (frame runtime.Frame, ok bool) { + defer func() { + if recover() == nil { + return + } + var pcs [32]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + for { + next, more := frames.Next() + if strings.HasSuffix(next.Function, wantSuffix) { + frame = next + ok = true + return + } + if !more { + return + } + } + }() + f() + return +} + +//line runtime_callers_recovered_nil_metadata.go:300 +func runtimeRecoveredNilPanicClosureFrame() (runtime.Frame, bool) { + return runtimeRecoveredNilPanicFrame(".runtimeRecoveredNilPanicClosureFrame.func1", func() { + var v runtimeCallerNilIface + v.runtimeCallerNilMethod() + }) +} + +//line runtime_callers_recovered_nil_metadata.go:310 +func runtimeRecoveredNilPanicSplitClosureFrame() (runtime.Frame, bool) { + return runtimeRecoveredNilPanicFrame(".runtimeRecoveredNilPanicSplitClosureFrame.func1", func() { + var v runtimeCallerNilIface + v. // method name is on the following line + runtimeCallerNilMethod() + }) +} + //line runtime_caller_metadata.go:100 func runtimeCallerLeaf(skip int) runtimeCallerSnapshot { pc, file, line, ok := runtime.Caller(skip) diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index 80bd817d5f..f90e509ad3 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -3207,11 +3207,6 @@ xfails: directive: run case: maymorestack.go reason: go1.26 goroot ci-mode run failure on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: devirtualization_nil_panics.go - reason: go1.26 goroot run failure on darwin/arm64 - version: go1.26 platform: linux/amd64 directive: run @@ -3417,11 +3412,6 @@ xfails: directive: run case: fixedbugs/issue8606b.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: devirtualization_nil_panics.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run From 5c7577d7ec84d6b236c99c09991a3e7781624271 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 24 May 2026 12:43:06 +0800 Subject: [PATCH 4/5] test: cover inline runtime caller metadata --- test/go/runtime_caller_test.go | 192 +++++++++++++++++++++++++++++++++ test/goroot/xfail.yaml | 50 --------- 2 files changed, 192 insertions(+), 50 deletions(-) diff --git a/test/go/runtime_caller_test.go b/test/go/runtime_caller_test.go index 0f465db711..32976d1aab 100644 --- a/test/go/runtime_caller_test.go +++ b/test/go/runtime_caller_test.go @@ -121,6 +121,121 @@ func TestRuntimeCallersFramesRecoveredNilPanicMetadata(t *testing.T) { } } +var ( + runtimeInlineCallerSkip int + runtimeInlineCallerFrame runtimeCallerSnapshot +) + +func TestRuntimeCallerInlineMetadata(t *testing.T) { + tests := []struct { + skip int + nameSuffix string + line int + }{ + {0, ".runtimeInlineCallerH", 422}, + {1, ".runtimeInlineCallerG", 411}, + {2, ".runtimeInlineCallerF", 401}, + {3, ".runtimeInlineCallerProbe", 432}, + } + for _, tt := range tests { + got := runtimeInlineCallerProbe(tt.skip) + if !got.ok { + t.Fatalf("runtime.Caller(%d) failed", tt.skip) + } + if !strings.HasSuffix(got.file, "runtime_inline_caller_metadata.go") { + t.Fatalf("runtime.Caller(%d) file = %q", tt.skip, got.file) + } + if got.line != tt.line { + t.Fatalf("runtime.Caller(%d) line = %d, want %d", tt.skip, got.line, tt.line) + } + fn := runtime.FuncForPC(got.pc) + if fn == nil { + t.Fatalf("FuncForPC(runtime.Caller(%d) pc) = nil", tt.skip) + } + if name := fn.Name(); !strings.HasSuffix(name, tt.nameSuffix) { + t.Fatalf("FuncForPC(runtime.Caller(%d) pc).Name = %q, want suffix %q", tt.skip, name, tt.nameSuffix) + } + file, line := fn.FileLine(got.pc) + if !strings.HasSuffix(file, "runtime_inline_caller_metadata.go") { + t.Fatalf("FuncForPC(runtime.Caller(%d) pc).FileLine file = %q", tt.skip, file) + } + if line != tt.line { + t.Fatalf("FuncForPC(runtime.Caller(%d) pc).FileLine line = %d, want %d", tt.skip, line, tt.line) + } + } +} + +var ( + runtimeInlineCallersSkip int + runtimeInlineCallersN int + runtimeInlineCallersPCs [32]uintptr +) + +func TestRuntimeCallersInlineMetadata(t *testing.T) { + funcForPCWant := [][]string{ + 0: {"runtime.Callers", ".runtimeInlineCallersH", ".runtimeInlineCallersG", ".runtimeInlineCallersF", ".runtimeInlineCallersFuncForPC"}, + 1: {".runtimeInlineCallersH", ".runtimeInlineCallersG", ".runtimeInlineCallersF", ".runtimeInlineCallersFuncForPC"}, + 2: {".runtimeInlineCallersG", ".runtimeInlineCallersF", ".runtimeInlineCallersFuncForPC"}, + 3: {".runtimeInlineCallersF", ".runtimeInlineCallersFuncForPC"}, + 4: {".runtimeInlineCallersFuncForPC"}, + } + for skip, want := range funcForPCWant { + got := runtimeInlineCallersFuncForPC(skip) + if !runtimeFrameNameSuffixes(got, want) { + t.Fatalf("runtime.Callers FuncForPC(%d) = %#v, want suffixes %#v", skip, got, want) + } + } + + framesWant := []struct { + nameSuffix string + line int + }{ + {"runtime.Callers", 0}, + {".runtimeInlineCallersH", 521}, + {".runtimeInlineCallersG", 511}, + {".runtimeInlineCallersF", 501}, + {".runtimeInlineCallersFrames", 562}, + } + for skip := range framesWant { + frames := runtimeInlineCallersFrames(skip) + want := framesWant[skip:] + if len(frames) < len(want) { + t.Fatalf("runtime.CallersFrames(%d) returned %d frames, want at least %d: %#v", skip, len(frames), len(want), frames) + } + for i, tt := range want { + frame := frames[i] + if !strings.HasSuffix(frame.Function, tt.nameSuffix) { + t.Fatalf("runtime.CallersFrames(%d) frame %d function = %q, want suffix %q", skip, i, frame.Function, tt.nameSuffix) + } + if tt.line != 0 { + if !strings.HasSuffix(frame.File, "runtime_inline_callers_metadata.go") { + t.Fatalf("runtime.CallersFrames(%d) frame %d file = %q", skip, i, frame.File) + } + if frame.Line != tt.line { + t.Fatalf("runtime.CallersFrames(%d) frame %d line = %d, want %d", skip, i, frame.Line, tt.line) + } + } + if frame.Func != nil { + if name := frame.Func.Name(); !strings.HasSuffix(name, tt.nameSuffix) { + t.Fatalf("runtime.CallersFrames(%d) frame %d Func.Name = %q, want suffix %q", skip, i, name, tt.nameSuffix) + } + } + } + } +} + +func runtimeFrameNameSuffixes(got, want []string) bool { + if len(got) != len(want) { + return false + } + for i := range got { + if !strings.HasSuffix(got[i], want[i]) { + return false + } + } + return true +} + func runtimeRecoveredNilPanicFrame(wantSuffix string, f func()) (frame runtime.Frame, ok bool) { defer func() { if recover() == nil { @@ -162,6 +277,83 @@ func runtimeRecoveredNilPanicSplitClosureFrame() (runtime.Frame, bool) { }) } +//line runtime_inline_caller_metadata.go:400 +func runtimeInlineCallerF() { + runtimeInlineCallerG() +} + +//line runtime_inline_caller_metadata.go:410 +func runtimeInlineCallerG() { + runtimeInlineCallerH() +} + +//line runtime_inline_caller_metadata.go:420 +func runtimeInlineCallerH() { + x := &runtimeInlineCallerFrame + x.pc, x.file, x.line, x.ok = runtime.Caller(runtimeInlineCallerSkip) +} + +//line runtime_inline_caller_metadata.go:430 +func runtimeInlineCallerProbe(skip int) runtimeCallerSnapshot { + runtimeInlineCallerSkip = skip + runtimeInlineCallerF() + frame := runtimeInlineCallerFrame + runtimeInlineCallerFrame = runtimeCallerSnapshot{} + if !frame.ok { + return runtimeCallerSnapshot{} + } + return frame +} + +//line runtime_inline_callers_metadata.go:500 +func runtimeInlineCallersF() { + runtimeInlineCallersG() +} + +//line runtime_inline_callers_metadata.go:510 +func runtimeInlineCallersG() { + runtimeInlineCallersH() +} + +//line runtime_inline_callers_metadata.go:520 +func runtimeInlineCallersH() { + runtimeInlineCallersN = runtime.Callers(runtimeInlineCallersSkip, runtimeInlineCallersPCs[:]) +} + +//line runtime_inline_callers_metadata.go:530 +func runtimeInlineCallersFuncForPC(skip int) (frames []string) { + runtimeInlineCallersSkip = skip + runtimeInlineCallersF() + for i := 0; i < runtimeInlineCallersN; i++ { + fn := runtime.FuncForPC(runtimeInlineCallersPCs[i] - 1) + if fn == nil { + frames = append(frames, "") + continue + } + frames = append(frames, fn.Name()) + if strings.HasSuffix(fn.Name(), ".runtimeInlineCallersFuncForPC") { + break + } + } + return frames +} + +//line runtime_inline_callers_metadata.go:560 +func runtimeInlineCallersFrames(skip int) (frames []runtime.Frame) { + runtimeInlineCallersSkip = skip + runtimeInlineCallersF() + callers := runtimeInlineCallersPCs[:runtimeInlineCallersN] + iter := runtime.CallersFrames(callers) + for { + frame, more := iter.Next() + frames = append(frames, frame) + if !more || strings.HasSuffix(frame.Function, ".runtimeInlineCallersFrames") { + break + } + } + return frames +} + //line runtime_caller_metadata.go:100 func runtimeCallerLeaf(skip int) runtimeCallerSnapshot { pc, file, line, ok := runtime.Caller(skip) diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index f90e509ad3..9a341c09c3 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2252,46 +2252,16 @@ xfails: directive: run case: typeparam/nested.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: inline_caller.go - reason: go1.24 goroot ci-mode run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: inline_callers.go - reason: go1.24 goroot ci-mode run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run case: maymorestack.go reason: go1.24 goroot ci-mode run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: inline_caller.go - reason: go1.25 goroot ci-mode run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: inline_callers.go - reason: go1.25 goroot ci-mode run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run case: maymorestack.go reason: go1.25 goroot ci-mode run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: inline_caller.go - reason: go1.26 goroot ci-mode run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: inline_callers.go - reason: go1.26 goroot ci-mode run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -3177,31 +3147,11 @@ xfails: directive: run case: maymorestack.go reason: go1.24 goroot ci-mode run failure on darwin/arm64 - - version: go1.25 - platform: darwin/arm64 - directive: run - case: inline_caller.go - reason: go1.25 goroot ci-mode run failure on darwin/arm64 - - version: go1.25 - platform: darwin/arm64 - directive: run - case: inline_callers.go - reason: go1.25 goroot ci-mode run failure on darwin/arm64 - version: go1.25 platform: darwin/arm64 directive: run case: maymorestack.go reason: go1.25 goroot ci-mode run failure on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: inline_caller.go - reason: go1.26 goroot ci-mode run failure on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: inline_callers.go - reason: go1.26 goroot ci-mode run failure on darwin/arm64 - version: go1.26 platform: darwin/arm64 directive: run From 607cc427effb3afc52ad02c5ff5888941983d9ce Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 24 May 2026 22:36:50 +0800 Subject: [PATCH 5/5] runtime: attribute panic caller lines --- cl/compile.go | 20 ++++ cl/instr.go | 5 +- runtime/internal/runtime/caller.go | 16 +++ runtime/internal/runtime/z_rt.go | 1 + test/go/runtime_panic_caller_line_test.go | 140 ++++++++++++++++++++++ test/goroot/xfail.yaml | 24 ---- 6 files changed, 181 insertions(+), 25 deletions(-) create mode 100644 test/go/runtime_panic_caller_line_test.go diff --git a/cl/compile.go b/cl/compile.go index 4036241016..eaa131da77 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -562,6 +562,18 @@ func (p *context) getFuncBodyPos(f *ssa.Function) token.Position { return p.goProg.Fset.Position(f.Pos()) } +func (p *context) getFuncEndPos(f *ssa.Function) token.Position { + if syntax := f.Syntax(); syntax != nil && syntax.End().IsValid() { + return p.goProg.Fset.Position(syntax.End()) + } + if f.Object() != nil { + if fn, ok := f.Object().(*types.Func); ok && fn.Scope() != nil && fn.Scope().End().IsValid() { + return p.goProg.Fset.Position(fn.Scope().End()) + } + } + return p.getFuncBodyPos(f) +} + func isGlobal(v *types.Var) bool { // TODO(lijie): better implementation return strings.HasPrefix(v.Parent().String(), "package ") @@ -970,6 +982,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue if v.Op == token.ARROW { ret = b.Recv(x, v.CommaOk) } else { + p.setCallerLine(b, v.Pos()) ret = b.UnOp(v.Op, x) } case *ssa.ChangeType: @@ -982,6 +995,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue ret = b.Convert(p.type_(t, llssa.InGo), x) case *ssa.FieldAddr: x := p.compileValue(b, v.X) + p.setCallerLine(b, v.Pos()) ret = b.FieldAddr(x, v.Field) case *ssa.Alloc: t := v.Type().(*types.Pointer) @@ -997,10 +1011,12 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue } x := p.compileValue(b, vx) idx := p.compileValue(b, v.Index) + p.setCallerLine(b, v.Pos()) ret = b.IndexAddr(x, idx) case *ssa.Index: x := p.compileValue(b, v.X) idx := p.compileValue(b, v.Index) + p.setCallerLine(b, v.Pos()) ret = b.Index(x, idx, func() (addr llssa.Expr, zero bool) { switch n := v.X.(type) { case *ssa.Const: @@ -1030,6 +1046,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue if v.Max != nil { max = p.compileValue(b, v.Max) } + p.setCallerLine(b, v.Pos()) ret = b.Slice(x, low, high, max) ret.Type = p.type_(v.Type(), llssa.InGo) case *ssa.MakeInterface: @@ -1203,6 +1220,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { } } if p.returnNeedsImplicitRunDefers(v) { + p.setCallerLineNumber(b, p.getFuncEndPos(v.Parent()).Line) b.RunDefers() } if p.shouldTrackCallerFrames() { @@ -1230,9 +1248,11 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { case *ssa.Go: p.call(b, llssa.Go, &v.Call) case *ssa.RunDefers: + p.setCallerLineNumber(b, p.getFuncEndPos(v.Parent()).Line) b.RunDefers() case *ssa.Panic: arg := p.compileValue(b, v.X) + p.setCallerLine(b, v.Pos()) b.Panic(arg) case *ssa.Send: ch := p.compileValue(b, v.Chan) diff --git a/cl/instr.go b/cl/instr.go index cebb1991d2..0d499adcd4 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -892,10 +892,13 @@ func (p *context) pushCallerFrame(b llssa.Builder, fn *ssa.Function) { } func (p *context) setCallerLine(b llssa.Builder, pos token.Pos) { + p.setCallerLineNumber(b, p.fset.Position(pos).Line) +} + +func (p *context) setCallerLineNumber(b llssa.Builder, line int) { if !p.shouldTrackCallerFrames() { return } - line := p.fset.Position(pos).Line if line <= 0 { return } diff --git a/runtime/internal/runtime/caller.go b/runtime/internal/runtime/caller.go index 19a826ab78..7e13e4fa3d 100644 --- a/runtime/internal/runtime/caller.go +++ b/runtime/internal/runtime/caller.go @@ -30,6 +30,8 @@ type CallerFrame struct { var ( callerFrames []CallerFrame pcFrames []*CallerFrame + + panicCallerFrames []CallerFrame ) var ( @@ -59,16 +61,26 @@ func SetCallerLine(line int) { } func PopCallerFrame(mark int) { + oldLen := len(callerFrames) if mark < 0 || mark > len(callerFrames) { return } callerFrames = callerFrames[:mark] + if len(panicCallerFrames) > 0 && oldLen > len(panicCallerFrames) && len(callerFrames) <= len(panicCallerFrames) { + panicCallerFrames = nil + } } func Caller(skip int) (CallerFrame, bool) { if skip < 0 { return CallerFrame{}, false } + if skip >= 2 && len(panicCallerFrames) > 0 { + idx := len(panicCallerFrames) - 1 - (skip - 2) + if idx >= 0 { + return captureFrame(panicCallerFrames[idx], false), true + } + } if skip < len(callerFrames) { return captureFrame(callerFrames[len(callerFrames)-1-skip], false), true } @@ -112,6 +124,10 @@ func Callers(skip int, pcs []uintptr) int { return n } +func SavePanicCallerFrames() { + panicCallerFrames = append(panicCallerFrames[:0], callerFrames...) +} + func FrameForPC(pc uintptr) (CallerFrame, bool) { if pc == 0 { return CallerFrame{}, false diff --git a/runtime/internal/runtime/z_rt.go b/runtime/internal/runtime/z_rt.go index 3b17c951e1..4cd79f22ac 100644 --- a/runtime/internal/runtime/z_rt.go +++ b/runtime/internal/runtime/z_rt.go @@ -49,6 +49,7 @@ func Recover() (ret any) { // Panic panics with a value. func Panic(v any) { + SavePanicCallerFrames() ptr := c.Malloc(unsafe.Sizeof(v)) *(*any)(ptr) = v excepKey.Set(ptr) diff --git a/test/go/runtime_panic_caller_line_test.go b/test/go/runtime_panic_caller_line_test.go new file mode 100644 index 0000000000..e24918210f --- /dev/null +++ b/test/go/runtime_panic_caller_line_test.go @@ -0,0 +1,140 @@ +package gotest + +import ( + "os" + "path/filepath" + "testing" +) + +const runtimePanicCallerLineProbe = `package main + +import ( + "fmt" + "os" + "runtime" + "strings" +) + +var idx = 9 +var nilStruct *struct { + c chan int + val int +} +var sink int + +func fail(format string, args ...any) { + fmt.Fprintf(os.Stderr, format+"\n", args...) + os.Exit(1) +} + +func fileOK(file string) bool { + return strings.HasSuffix(file, "main.go") || strings.HasSuffix(file, "panic_lines.go") +} + +func main() { + explicitPanicCaller() + nilFieldCallerLine() + boundsFramesLine() + deferReturnCallerLine() +} + +func explicitPanicCaller() { + const wantLine = 110 + defer func() { + if recover() == nil { + fail("explicit panic did not panic") + } + _, file, line, ok := runtime.Caller(2) + if !ok || !fileOK(file) || line != wantLine { + fail("runtime.Caller(2) = %s:%d ok=%v, want line %d", file, line, ok, wantLine) + } + }() +//line panic_lines.go:110 + panic("boom") +} + +func nilFieldCallerLine() { + const wantLine = 311 + defer func() { + if recover() == nil { + fail("nil field did not panic") + } + for i := 0; ; i++ { + pc, file, line, ok := runtime.Caller(i) + if !ok { + fail("runtime.Caller could not find nilFieldCallerLine") + } + fn := runtime.FuncForPC(pc) + name := "" + if fn != nil { + name = fn.Name() + } + if !strings.HasSuffix(name, ".nilFieldCallerLine") { + continue + } + if !fileOK(file) || line != wantLine { + fail("nil field frame = %s:%d %s, want line %d", file, line, name, wantLine) + } + return + } + }() +//line panic_lines.go:310 + select { + case <-nilStruct.c: + default: + } +} + +func boundsFramesLine() { + const wantLine = 999999 + defer func() { + if recover() == nil { + fail("bounds check did not panic") + } + var pcs [16]uintptr + n := runtime.Callers(1, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if strings.HasSuffix(frame.Function, ".boundsFramesLine") { + if !fileOK(frame.File) || frame.Line != wantLine { + fail("bounds frame = %s:%d %s, want line %d", frame.File, frame.Line, frame.Function, wantLine) + } + return + } + if !more { + break + } + } + fail("CallersFrames could not find boundsFramesLine") + }() + var a [1]int +//line panic_lines.go:999999 + sink = a[idx] +} + +//line main.go:500 +func deferReturnCallerLine() { + const wantLine = 507 + got := 0 + func() { + defer func() { + _, _, got, _ = runtime.Caller(1) + }() + }() + if got != wantLine { + fail("defer runtime.Caller(1) line = %d, want %d", got, wantLine) + } +} +` + +func TestRuntimePanicCallerLineAttribution(t *testing.T) { + dir := t.TempDir() + file := filepath.Join(dir, "main.go") + if err := os.WriteFile(file, []byte(runtimePanicCallerLineProbe), 0644); err != nil { + t.Fatal(err) + } + root := findLLGoRoot(t) + t.Setenv("LLGO_ROOT", root) + runGoCmd(t, root, "run", "./cmd/llgo", "run", file) +} diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index 9a341c09c3..75d598552f 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2365,22 +2365,10 @@ xfails: directive: run case: tinyfin.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/bug347.go - reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/bug348.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue10332.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue14646.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue15281.go @@ -2429,10 +2417,6 @@ xfails: directive: run case: fixedbugs/issue28748.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue29504.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue29735.go @@ -2465,10 +2449,6 @@ xfails: directive: run case: fixedbugs/issue45045.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue4562.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue46725.go @@ -2505,10 +2485,6 @@ xfails: directive: run case: fixedbugs/issue58300b.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue5856.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue5963.go