diff --git a/cl/compile.go b/cl/compile.go index bc073c23de..9212fe227e 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". @@ -552,6 +611,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") } @@ -1122,6 +1184,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 @@ -1452,6 +1517,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 31ac30788e..6e3ae9953b 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -23,6 +23,7 @@ import ( "go/types" "log" "os" + "path/filepath" "regexp" "strings" @@ -791,6 +792,151 @@ 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 || !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 + } + 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(p.callerFrameFile(fn.Pos(), pos.Filename)), + p.prog.IntVal(uint64(pos.Line), p.prog.Int()), + ) +} + +func (p *context) setCallerLocation(b llssa.Builder, pos token.Pos) { + if !p.shouldTrackCallerFrames() { + return + } + position := p.fset.Position(pos) + line := position.Line + if line <= 0 { + return + } + b.Call( + p.pkg.RuntimeFunc("SetCallerLocation"), + b.Str(p.callerFrameFile(pos, position.Filename)), + 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 +} + +func (p *context) callerFrameFile(pos token.Pos, filename string) string { + if filename == "" { + return "??" + } + original := p.fset.PositionFor(pos, false).Filename + if original == "" || original == filename { + return filename + } + if rel, ok := relativeLineDirectiveFile(original, filename); ok { + return rel + } + return filename +} + +func relativeLineDirectiveFile(original, adjusted string) (string, bool) { + rel, err := filepath.Rel(filepath.Dir(original), adjusted) + if err != nil || rel == "." || rel == ".." { + return "", false + } + if strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return "", false + } + return filepath.ToSlash(rel), true +} + // ----------------------------------------------------------------------------- type explicitDeferStack struct { @@ -830,7 +976,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.setCallerLocation(b, pos) if ds != nil { b.DeferTo(ds.owner, ds.stack, fn, buildCall, args...) return llssa.Nil @@ -848,7 +995,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) @@ -871,7 +1018,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 @@ -880,13 +1027,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...) @@ -960,33 +1107,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 { @@ -996,7 +1143,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..90bedec697 100644 --- a/runtime/internal/lib/runtime/symtab.go +++ b/runtime/internal/lib/runtime/symtab.go @@ -9,6 +9,7 @@ import ( 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 +120,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 +149,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 +192,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 len(name) >= len(commandLineArguments) && name[:len(commandLineArguments)] == 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..65b00a0f3a --- /dev/null +++ b/runtime/internal/runtime/caller.go @@ -0,0 +1,147 @@ +/* + * 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 SetCallerLocation(file string, line int) { + if line <= 0 || len(callerFrames) == 0 { + return + } + frame := &callerFrames[len(callerFrames)-1] + frame.File = file + frame.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..a025ac502c --- /dev/null +++ b/test/go/runtime_caller_test.go @@ -0,0 +1,173 @@ +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) + } + } +} + +func TestRuntimeCallerLineDirectiveCallSites(t *testing.T) { + got := runtimeCallerLineDirectiveChecks() + want := []struct { + file string + line int + }{ + {"/foo/bar.go", 123}, + {"c:/foo/bar.go", 987}, + {"??", 1}, + {"foo.go", 1}, + {"bar.go", 10}, + {"bar.go", 11}, + } + if len(got) != len(want) { + t.Fatalf("got %d caller snapshots, want %d", len(got), len(want)) + } + for i, tt := range want { + if !got[i].ok { + t.Fatalf("snapshot %d runtime.Caller failed", i) + } + if got[i].file != tt.file || got[i].line != tt.line { + t.Fatalf("snapshot %d = %s:%d, want %s:%d", i, got[i].file, got[i].line, tt.file, tt.line) + } + fn := runtime.FuncForPC(got[i].pc) + if fn == nil { + t.Fatalf("snapshot %d FuncForPC = nil", i) + } + file, line := fn.FileLine(got[i].pc) + if file != tt.file || line != tt.line { + t.Fatalf("snapshot %d FuncForPC.FileLine = %s:%d, want %s:%d", i, file, line, tt.file, tt.line) + } + } +} + +func runtimeCallerLineDirectiveChecks() []runtimeCallerSnapshot { + var out []runtimeCallerSnapshot +//line /foo/bar.go:123 + out = append(out, runtimeCallerAtCallSite()) +//line c:/foo/bar.go:987 + out = append(out, runtimeCallerAtCallSite()) +//line :1 + out = append(out, runtimeCallerAtCallSite()) +//line foo.go:1 + out = append(out, runtimeCallerAtCallSite()) +//line bar.go:10:20 + out = append(out, runtimeCallerAtCallSite()) +//line :11:22 + out = append(out, runtimeCallerAtCallSite()) + return out +} + +func runtimeCallerAtCallSite() runtimeCallerSnapshot { + pc, file, line, ok := runtime.Caller(1) + return runtimeCallerSnapshot{pc: pc, file: file, line: line, ok: ok} +} + +//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..fb4246aeb9 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2419,10 +2419,6 @@ xfails: directive: run case: fixedbugs/issue17381.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue18149.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue21879.go @@ -2431,10 +2427,6 @@ xfails: directive: run case: fixedbugs/issue22083.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue22662.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue23837.go @@ -3172,16 +3164,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