Skip to content
Closed
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
47 changes: 43 additions & 4 deletions cl/cltest/cltest.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,10 @@ func testFrom(t *testing.T, pkgDir, sel string) {
if spec.Mode == littest.ModeSkip {
return
}
v := llgen.GenFrom(pkgDir)
var v string
withFuncInfoDisabled(func() {
v = llgen.GenFrom(pkgDir)
})
if spec.Mode == littest.ModeFileCheck {
if err := littest.Check(spec, v); err != nil {
_ = os.WriteFile(pkgDir+"/result.txt", []byte(v), 0644)
Expand Down Expand Up @@ -294,7 +297,14 @@ func testRunAndTestFrom(t *testing.T, pkgDir, relPkg, sel string, opts runOption
}
}

output, err := runWithConf(relPkg, pkgDir, conf)
var output []byte
if checkIR {
withFuncInfoDisabled(func() {
output, err = runWithConf(relPkg, pkgDir, conf)
})
} else {
output, err = runWithConf(relPkg, pkgDir, conf)
}
if err != nil {
t.Logf("raw output:\n%s", string(output))
t.Fatalf("run failed: %v\noutput: %s", err, string(output))
Expand Down Expand Up @@ -509,6 +519,20 @@ func readIRSpec(pkgDir string) (littest.Spec, bool, error) {
return spec, true, nil
}

func withFuncInfoDisabled(fn func()) {
const key = "LLGO_FUNCINFO"
old, ok := os.LookupEnv(key)
_ = os.Setenv(key, "0")
defer func() {
if ok {
_ = os.Setenv(key, old)
} else {
_ = os.Unsetenv(key)
}
}()
fn()
}

func filterRunOutput(in []byte) []byte {
// Tests compare output with expect.txt. Some toolchain/environment warnings are
// inherently machine-specific and should not be part of the golden output.
Expand Down Expand Up @@ -540,8 +564,15 @@ func filterRunOutput(in []byte) []byte {
return out.Bytes()
}

func TestCompileEx(t *testing.T, src any, fname, expected string, dbg bool) {
func CompileIREx(t *testing.T, src any, fname string, dbg bool, configure func(llssa.Program)) string {
t.Helper()
// Build.Do configures cl debug globals for full-package builds. Keep the
// single-file compiler assertions independent from any prior build test.
cl.EnableDebug(dbg)
cl.EnableDbgSyms(dbg)
defer cl.EnableDebug(false)
defer cl.EnableDbgSyms(false)

fset := token.NewFileSet()
f, err := parser.ParseFile(fset, fname, src, parser.ParseComments)
if err != nil {
Expand All @@ -563,13 +594,21 @@ func TestCompileEx(t *testing.T, src any, fname, expected string, dbg bool) {
foo.WriteTo(os.Stderr)
prog := ssatest.NewProgramEx(t, nil, imp)
prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH))
if configure != nil {
configure(prog)
}

ret, err := cl.NewPackage(prog, foo, files)
if err != nil {
t.Fatal("cl.NewPackage failed:", err)
}
return ret.String()
}

if v := ret.String(); llssa.StripModuleTarget(v) != expected && expected != ";" { // expected == ";" means skipping out.ll
func TestCompileEx(t *testing.T, src any, fname, expected string, dbg bool) {
t.Helper()
v := CompileIREx(t, src, fname, dbg, nil)
if llssa.StripModuleTarget(v) != expected && expected != ";" { // expected == ";" means skipping out.ll
t.Fatalf("\n==> got:\n%s\n==> expected:\n%s\n", v, expected)
}
}
48 changes: 45 additions & 3 deletions cl/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,13 +469,26 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
}
if fn == nil {
fn = pkg.NewFuncEx(name, sig, llssa.Background(ftype), hasCtx, p.needsLinkOnce(f))
if disableInline {
fn.Inline(llssa.NoInline)
}
}
noInlineDirective := hasNoInlineDirective(f)
runtimeStackNoInline := needsRuntimeStackNoInline(pkgTypes, f)
if disableInline || noInlineDirective || runtimeStackNoInline {
fn.Inline(llssa.NoInline)
}
if noInlineDirective || runtimeStackNoInline {
fn.DisableTailCalls()
}
p.funcs[f] = fn
isCgo := isCgoExternSymbol(f)
if nblk := len(f.Blocks); nblk > 0 {
if p.prog.FuncInfoMetadataEnabled() {
goName := fn.Name()
if pkgTypes != nil {
goName = funcName(pkgTypes, f, false)
}
pos := p.goProg.Fset.Position(f.Pos())
pkg.EmitFuncInfo(fn.Name(), goName, pos.Filename, pos.Line, pos.Column)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里记录的原始信息是函数定义的位置的文件名 行号等信息 这样的话 运行时恢复的堆栈就也是定义的位置,而不是调用的位置,这个是预期的吗?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

之前是自定义定义位置加上 llvm-symbolizer,感觉太重了,这个 PR 先移掉。#2002 尝试把调用行也加进来,对比一下数据和运行时开销,先 draft

}
var childInits []func()
if len(f.AnonFuncs) > 0 {
parentInits := p.inits
Expand Down Expand Up @@ -560,6 +573,35 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
return fn, nil, goFunc
}

func hasNoInlineDirective(f *ssa.Function) bool {
decl, _ := f.Syntax().(*ast.FuncDecl)
if decl == nil || decl.Doc == nil {
return false
}
for _, c := range decl.Doc.List {
if c.Text == "//go:noinline" {
return true
}
}
return false
}

func needsRuntimeStackNoInline(pkg *types.Package, f *ssa.Function) bool {
if pkg == nil || f == nil || f.Signature.Recv() != nil {
return false
}
switch pkg.Path() {
case "runtime", "github.com/goplus/llgo/runtime/internal/lib/runtime":
switch f.Name() {
case "Caller", "Callers", "callers":
return true
}
case "github.com/goplus/llgo/runtime/internal/clite/debug":
return f.Name() == "StackTrace"
}
return false
}

func (p *context) getFuncBodyPos(f *ssa.Function) token.Position {
if f.Object() != nil {
if fn, ok := f.Object().(*types.Func); ok && fn.Scope() != nil {
Expand Down
159 changes: 159 additions & 0 deletions cl/funcinfo_metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//go:build !llgo
// +build !llgo

/*
* Copyright (c) 2024 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 cl_test

import (
"regexp"
"strconv"
"strings"
"testing"

"github.com/goplus/llgo/cl/cltest"
llssa "github.com/goplus/llgo/ssa"
)

type funcInfoRecord struct {
symbol string
name string
file string
line int
column int
}

func TestFuncInfoMetadataEmission(t *testing.T) {
const src = `package foo

type T struct{}

func top() {
_ = func() int { return leaf() }()
}

func leaf() int { return 1 }

func (T) method() {}
`
ir := cltest.CompileIREx(t, src, "foo.go", false, func(prog llssa.Program) {
prog.EnableFuncInfoMetadata(true)
})

for _, want := range []string{
`!llgo.funcinfo = !{!`,
`!"foo.top"`,
`!"foo.top$1"`,
`!"foo.T.method"`,
`!"foo.go"`,
} {
if !strings.Contains(ir, want) {
t.Fatalf("missing funcinfo metadata %s:\n%s", want, ir)
}
}
if strings.Contains(ir, "llvm.compiler.used") {
t.Fatalf("funcinfo metadata should not add llvm.compiler.used:\n%s", ir)
}
if strings.Contains(ir, `ptr @"foo.top"`) || strings.Contains(ir, `ptr @foo.top`) {
t.Fatalf("funcinfo metadata should use symbol strings, not function pointers:\n%s", ir)
}

records := parseFuncInfoRecords(t, ir)
stackSymbols := []string{"foo.leaf", "foo.top$1", "foo.top"}
for _, symbol := range stackSymbols {
record, ok := records[symbol]
if !ok {
t.Fatalf("stack symbol %q not found in funcinfo metadata: %#v", symbol, records)
}
if record.name == "" || record.file != "foo.go" || record.line <= 0 || record.column <= 0 {
t.Fatalf("bad funcinfo for stack symbol %q: %#v", symbol, record)
}
}
if got := records["foo.leaf"].name; got != "foo.leaf" {
t.Fatalf("leaf stack frame name = %q, want foo.leaf", got)
}
if got := records["foo.top$1"].name; got != "foo.top$1" {
t.Fatalf("closure stack frame name = %q, want foo.top$1", got)
}
if got := records["foo.top"].name; got != "foo.top" {
t.Fatalf("caller stack frame name = %q, want foo.top", got)
}
}

func TestNoInlineDirectiveDisablesTailCalls(t *testing.T) {
const src = `package foo

func caller() { callee() }

//go:noinline
func callee() {}
`
ir := cltest.CompileIREx(t, src, "foo.go", false, nil)
if !strings.Contains(ir, `define void @foo.callee()`) {
t.Fatalf("missing callee function:\n%s", ir)
}
if !strings.Contains(ir, `noinline`) || !strings.Contains(ir, `"disable-tail-calls"="true"`) {
t.Fatalf("callee should disable inlining and tail calls:\n%s", ir)
}
}

func parseFuncInfoRecords(t *testing.T, ir string) map[string]funcInfoRecord {
t.Helper()

listRE := regexp.MustCompile(`!llgo\.funcinfo = !\{([^}]*)\}`)
listMatch := listRE.FindStringSubmatch(ir)
if listMatch == nil {
t.Fatalf("missing funcinfo metadata list:\n%s", ir)
}
refRE := regexp.MustCompile(`!(\d+)`)
refs := refRE.FindAllStringSubmatch(listMatch[1], -1)
if len(refs) == 0 {
t.Fatalf("empty funcinfo metadata list:\n%s", ir)
}
wantRefs := make(map[string]bool, len(refs))
for _, ref := range refs {
wantRefs[ref[1]] = true
}

rowRE := regexp.MustCompile(`^!(\d+) = !\{i32 1, !"([^"]+)", !"([^"]+)", !"([^"]*)", i32 ([0-9]+), i32 ([0-9]+)\}$`)
records := make(map[string]funcInfoRecord)
for _, line := range strings.Split(ir, "\n") {
row := rowRE.FindStringSubmatch(line)
if row == nil || !wantRefs[row[1]] {
continue
}
lineNo, err := strconv.Atoi(row[5])
if err != nil {
t.Fatalf("bad funcinfo line in %q: %v", line, err)
}
column, err := strconv.Atoi(row[6])
if err != nil {
t.Fatalf("bad funcinfo column in %q: %v", line, err)
}
records[row[2]] = funcInfoRecord{
symbol: row[2],
name: row[3],
file: row[4],
line: lineNo,
column: column,
}
}
if len(records) != len(wantRefs) {
t.Fatalf("parsed %d funcinfo records, want %d:\n%s", len(records), len(wantRefs), ir)
}
return records
}
23 changes: 22 additions & 1 deletion internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ func Do(args []string, conf *Config) ([]Package, error) {

prog := llssa.NewProgram(target)
prog.EnableGoGlobalDCE(conf.goGlobalDCEEnabled())
prog.EnableFuncInfoMetadata(conf.Mode != ModeGen && IsFuncInfoEnabled())
sizes := func(sizes types.Sizes, compiler, arch string) types.Sizes {
if arch == "wasm" {
sizes = &types.StdSizes{WordSize: 4, MaxAlign: 4}
Expand Down Expand Up @@ -1050,6 +1051,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa
methodByIndex: methodByIndex,
methodByName: methodByName,
abiSymbols: linkedModuleGlobals(linkedOrder),
funcInfo: prepareFuncInfoTableRecords(collectFuncInfo(linkedOrder), nil),
})
entryObjFile, err := exportObject(ctx, "entry_main", entryPkg.ExportFile, entryPkg.LPkg)
if err != nil {
Expand Down Expand Up @@ -1130,9 +1132,9 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose
if needsLinuxNoPIE(ctx, linkArgs) {
buildArgs = append(buildArgs, "-no-pie")
}
buildArgs = append(buildArgs, linuxExportDynamicArgs(ctx)...)
}

// Add common linker arguments based on target OS and architecture
if IsDbgSymsEnabled() {
buildArgs = append(buildArgs, "-gdwarf-4")
}
Expand Down Expand Up @@ -1178,6 +1180,20 @@ func needsLinuxNoPIE(ctx *context, linkArgs []string) bool {
return true
}

func needsLinuxExportDynamic(ctx *context) bool {
return ctx.buildConf.Target == "" && ctx.buildConf.Goos == "linux" && IsFuncInfoEnabled()
}

func linuxExportDynamicArgs(ctx *context) []string {
if !needsLinuxExportDynamic(ctx) {
return nil
}
return []string{
"-Wl,--export-dynamic-symbol=main.*",
"-Wl,--export-dynamic-symbol=command-line-arguments.*",
}
}

// archiver returns the archiving tool to use for the current context.
// For wasm targets and LTO builds, it prefers llvm-ar because linkers need
// LLVM-aware archive indexes for wasm objects and bitcode members.
Expand Down Expand Up @@ -1796,6 +1812,7 @@ var (

const llgoDebug = "LLGO_DEBUG"
const llgoDbgSyms = "LLGO_DEBUG_SYMBOLS"
const llgoFuncInfo = "LLGO_FUNCINFO"
const llgoTrace = "LLGO_TRACE"
const llgoOptimize = "LLGO_OPTIMIZE"
const llgoWasmRuntime = "LLGO_WASM_RUNTIME"
Expand Down Expand Up @@ -1843,6 +1860,10 @@ func IsDbgEnabled() bool {
return isEnvOn(llgoDebug, false) || isEnvOn(llgoDbgSyms, false)
}

func IsFuncInfoEnabled() bool {
return isEnvOn(llgoFuncInfo, true)
}

func IsDbgSymsEnabled() bool {
return isEnvOn(llgoDbgSyms, false)
}
Expand Down
Loading
Loading