diff --git a/cl/cltest/cltest.go b/cl/cltest/cltest.go index 51f199bf94..6a151f67ef 100644 --- a/cl/cltest/cltest.go +++ b/cl/cltest/cltest.go @@ -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) @@ -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)) @@ -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. @@ -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 { @@ -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) } } diff --git a/cl/compile.go b/cl/compile.go index 434a25d773..fe1dbfc8b1 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -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) + } var childInits []func() if len(f.AnonFuncs) > 0 { parentInits := p.inits @@ -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 { diff --git a/cl/funcinfo_metadata_test.go b/cl/funcinfo_metadata_test.go new file mode 100644 index 0000000000..5319b16751 --- /dev/null +++ b/cl/funcinfo_metadata_test.go @@ -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 +} diff --git a/internal/build/build.go b/internal/build/build.go index 149411815f..93041b7406 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -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} @@ -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 { @@ -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") } @@ -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. @@ -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" @@ -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) } diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 51401c2131..bc6f89d785 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -55,6 +55,49 @@ func TestNeedsLinuxNoPIE(t *testing.T) { } } +func TestNeedsLinuxExportDynamic(t *testing.T) { + t.Setenv(llgoFuncInfo, "") + ctx := &context{buildConf: &Config{Goos: "linux"}} + if !needsLinuxExportDynamic(ctx) { + t.Fatal("linux funcinfo executable should export dynamic symbols") + } + if got := linuxExportDynamicArgs(ctx); strings.Join(got, " ") != "-Wl,--export-dynamic-symbol=main.* -Wl,--export-dynamic-symbol=command-line-arguments.*" { + t.Fatalf("linuxExportDynamicArgs = %v", got) + } + t.Setenv(llgoFuncInfo, "0") + if needsLinuxExportDynamic(ctx) { + t.Fatal("LLGO_FUNCINFO=0 should disable dynamic symbol export") + } + if got := linuxExportDynamicArgs(ctx); got != nil { + t.Fatalf("disabled linuxExportDynamicArgs = %v, want nil", got) + } + t.Setenv(llgoFuncInfo, "1") + ctx.buildConf.Goos = "darwin" + if needsLinuxExportDynamic(ctx) { + t.Fatal("non-linux executable should not export dynamic symbols for funcinfo") + } + ctx.buildConf.Goos = "linux" + ctx.buildConf.Target = "wasi" + if needsLinuxExportDynamic(ctx) { + t.Fatal("named targets should not force host linux dynamic symbol export") + } +} + +func TestIsFuncInfoEnabled(t *testing.T) { + t.Setenv(llgoFuncInfo, "") + if !IsFuncInfoEnabled() { + t.Fatal("funcinfo should be enabled by default") + } + t.Setenv(llgoFuncInfo, "0") + if IsFuncInfoEnabled() { + t.Fatal("LLGO_FUNCINFO=0 should disable funcinfo") + } + t.Setenv(llgoFuncInfo, "1") + if !IsFuncInfoEnabled() { + t.Fatal("LLGO_FUNCINFO=1 should enable funcinfo") + } +} + func mockRun(args []string, cfg *Config) { defer mockable.DisableMock() mockable.EnableMock() diff --git a/internal/build/collect.go b/internal/build/collect.go index ab6d7076f6..dd30daf72b 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -82,6 +82,7 @@ func (c *context) collectEnvInputs(m *manifestBuilder) { envVars := []string{ llgoDebug, llgoDbgSyms, + llgoFuncInfo, llgoTrace, llgoOptimize, llgoWasmRuntime, diff --git a/internal/build/funcinfo/funcinfo.go b/internal/build/funcinfo/funcinfo.go new file mode 100644 index 0000000000..c092b606e9 --- /dev/null +++ b/internal/build/funcinfo/funcinfo.go @@ -0,0 +1,156 @@ +/* + * 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 funcinfo + +import ( + "fmt" + "math" + "sort" + "strings" +) + +type Record struct { + Symbol string + Name string + File string + Line uint32 + Column uint32 +} + +type EncodedRecord struct { + Symbol uint32 + Name uint32 + File uint32 + Line uint32 + Column uint32 +} + +type Table struct { + Records []EncodedRecord + Strings []byte + Hash []uint32 +} + +func Encode(records []Record) (Table, error) { + if len(records) == 0 { + return Table{}, nil + } + pool := stringPool{ + offsets: map[string]uint32{"": 0}, + data: []byte{0}, + text: "\x00", + } + for _, s := range collectStrings(records) { + if _, err := pool.offset(s); err != nil { + return Table{}, err + } + } + out := Table{ + Records: make([]EncodedRecord, 0, len(records)), + } + for _, rec := range records { + out.Records = append(out.Records, EncodedRecord{ + Symbol: pool.offsets[rec.Symbol], + Name: pool.offsets[rec.Name], + File: pool.offsets[rec.File], + Line: rec.Line, + Column: rec.Column, + }) + } + out.Strings = pool.data + out.Hash = buildHash(records) + return out, nil +} + +func collectStrings(records []Record) []string { + seen := make(map[string]bool) + for _, rec := range records { + seen[rec.Symbol] = true + seen[rec.Name] = true + seen[rec.File] = true + } + delete(seen, "") + out := make([]string, 0, len(seen)) + for s := range seen { + out = append(out, s) + } + sort.Slice(out, func(i, j int) bool { + if len(out[i]) != len(out[j]) { + return len(out[i]) > len(out[j]) + } + return out[i] < out[j] + }) + return out +} + +type stringPool struct { + offsets map[string]uint32 + data []byte + text string +} + +func (p *stringPool) offset(s string) (uint32, error) { + if off, ok := p.offsets[s]; ok { + return off, nil + } + if off := strings.Index(p.text, s+"\x00"); off >= 0 { + uoff := uint32(off) + p.offsets[s] = uoff + return uoff, nil + } + if len(p.data)+len(s)+1 > math.MaxUint32 { + return 0, fmt.Errorf("funcinfo string table exceeds 4 GiB") + } + off := uint32(len(p.data)) + p.data = append(p.data, s...) + p.data = append(p.data, 0) + p.text = string(p.data) + p.offsets[s] = off + return off, nil +} + +func buildHash(records []Record) []uint32 { + if len(records) == 0 { + return nil + } + buckets := 1 + for buckets*3 < len(records)*4 { + buckets <<= 1 + } + hash := make([]uint32, buckets) + for i, rec := range records { + slot := int(HashString(rec.Symbol) & uint32(buckets-1)) + for hash[slot] != 0 { + slot = (slot + 1) & (buckets - 1) + } + hash[slot] = uint32(i + 1) + } + return hash +} + +func HashString(s string) uint32 { + const ( + offset = uint32(2166136261) + prime = uint32(16777619) + ) + h := offset + for i := 0; i < len(s); i++ { + h ^= uint32(s[i]) + h *= prime + } + return h +} diff --git a/internal/build/funcinfo/funcinfo_test.go b/internal/build/funcinfo/funcinfo_test.go new file mode 100644 index 0000000000..7bc92ec8b1 --- /dev/null +++ b/internal/build/funcinfo/funcinfo_test.go @@ -0,0 +1,134 @@ +/* + * 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 funcinfo + +import "testing" + +func TestEncodePoolsStringsAndBuildsHash(t *testing.T) { + table, err := Encode([]Record{ + {Symbol: "example.com/p.a", Name: "example.com/p.A", File: "/src/p/shared.go", Line: 10, Column: 1}, + {Symbol: "example.com/p.b", Name: "example.com/p.B", File: "shared.go", Line: 20, Column: 2}, + }) + if err != nil { + t.Fatal(err) + } + if len(table.Records) != 2 { + t.Fatalf("encoded records = %d, want 2", len(table.Records)) + } + if table.Records[0].File == table.Records[1].File { + t.Fatalf("suffix sharing should not collapse distinct file strings to the same offset") + } + if got := cstring(table.Strings, table.Records[1].File); got != "shared.go" { + t.Fatalf("suffix file string = %q, want shared.go", got) + } + if len(table.Hash) == 0 || len(table.Hash)&(len(table.Hash)-1) != 0 { + t.Fatalf("hash bucket count = %d, want power-of-two non-zero", len(table.Hash)) + } + if idx, ok := lookup(table, "example.com/p.a"); !ok || idx != 0 { + t.Fatalf("lookup a = %d, %v; want 0, true", idx, ok) + } + if idx, ok := lookup(table, "example.com/p.b"); !ok || idx != 1 { + t.Fatalf("lookup b = %d, %v; want 1, true", idx, ok) + } + if _, ok := lookup(table, "missing"); ok { + t.Fatalf("lookup missing succeeded") + } +} + +func TestEncodeUsesUint32Records(t *testing.T) { + table, err := Encode([]Record{{Symbol: "s", Name: "n", File: "f", Line: 1, Column: 2}}) + if err != nil { + t.Fatal(err) + } + if got, want := len(table.Records), 1; got != want { + t.Fatalf("records = %d, want %d", got, want) + } + rec := table.Records[0] + if got, want := cstring(table.Strings, rec.Symbol), "s"; got != want { + t.Fatalf("symbol = %q, want %q", got, want) + } + if got, want := cstring(table.Strings, rec.Name), "n"; got != want { + t.Fatalf("name = %q, want %q", got, want) + } + if got, want := cstring(table.Strings, rec.File), "f"; got != want { + t.Fatalf("file = %q, want %q", got, want) + } + if rec.Line != 1 || rec.Column != 2 { + t.Fatalf("source position = %d:%d, want 1:2", rec.Line, rec.Column) + } +} + +func TestEncodeHashHandlesCollisions(t *testing.T) { + a, b := collisionPair(t) + table, err := Encode([]Record{ + {Symbol: a, Name: a, File: "a.go"}, + {Symbol: b, Name: b, File: "b.go"}, + }) + if err != nil { + t.Fatal(err) + } + if idx, ok := lookup(table, a); !ok || idx != 0 { + t.Fatalf("lookup collision a = %d, %v; want 0, true", idx, ok) + } + if idx, ok := lookup(table, b); !ok || idx != 1 { + t.Fatalf("lookup collision b = %d, %v; want 1, true", idx, ok) + } +} + +func collisionPair(t *testing.T) (string, string) { + t.Helper() + const mask = uint32(3) + seen := make(map[uint32]string) + for i := 0; i < 100; i++ { + s := string(rune('a' + i)) + slot := HashString(s) & mask + if prev, ok := seen[slot]; ok { + return prev, s + } + seen[slot] = s + } + t.Fatal("failed to find hash collision") + return "", "" +} + +func cstring(data []byte, off uint32) string { + end := int(off) + for end < len(data) && data[end] != 0 { + end++ + } + return string(data[off:end]) +} + +func lookup(table Table, symbol string) (int, bool) { + if len(table.Hash) == 0 { + return 0, false + } + mask := uint32(len(table.Hash) - 1) + slot := HashString(symbol) & mask + for probes := 0; probes < len(table.Hash); probes++ { + idx := table.Hash[slot] + if idx == 0 { + return 0, false + } + rec := table.Records[idx-1] + if cstring(table.Strings, rec.Symbol) == symbol { + return int(idx - 1), true + } + slot = (slot + 1) & mask + } + return 0, false +} diff --git a/internal/build/funcinfo_table.go b/internal/build/funcinfo_table.go new file mode 100644 index 0000000000..30c63c5fe4 --- /dev/null +++ b/internal/build/funcinfo_table.go @@ -0,0 +1,223 @@ +/* + * 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 build + +import ( + "sort" + + "github.com/xgo-dev/llvm" + + buildfuncinfo "github.com/goplus/llgo/internal/build/funcinfo" + llssa "github.com/goplus/llgo/ssa" +) + +const ( + funcInfoTableSymbol = "__llgo_funcinfo_table" + funcInfoCountSymbol = "__llgo_funcinfo_count" + funcInfoStringsSymbol = "__llgo_funcinfo_strings" + funcInfoHashSymbol = "__llgo_funcinfo_hash" + funcInfoHashMaskSymbol = "__llgo_funcinfo_hash_mask" + funcInfoDataSymbol = "__llgo_funcinfo_table$data" + funcInfoStringsDataSymbol = "__llgo_funcinfo_strings$data" + funcInfoHashDataSymbol = "__llgo_funcinfo_hash$data" +) + +type funcInfoRecord struct { + symbol string + name string + file string + line uint32 + column uint32 +} + +func collectFuncInfo(pkgs []Package) []funcInfoRecord { + seen := make(map[string]funcInfoRecord) + for _, pkg := range pkgs { + if pkg == nil || pkg.LPkg == nil { + continue + } + for _, rec := range readFuncInfo(pkg.LPkg.Module()) { + if rec.symbol == "" { + continue + } + if _, ok := seen[rec.symbol]; !ok { + seen[rec.symbol] = rec + } + } + } + if len(seen) == 0 { + return nil + } + out := make([]funcInfoRecord, 0, len(seen)) + for _, rec := range seen { + out = append(out, rec) + } + sort.Slice(out, func(i, j int) bool { + return out[i].symbol < out[j].symbol + }) + return out +} + +func prepareFuncInfoTableRecords(records []funcInfoRecord, liveSymbols map[string]none) []funcInfoRecord { + if len(records) == 0 { + return nil + } + // A nil liveSymbols means no post-DCE live symbol set is available yet. + // The current table is still DCE-compatible because it stores only strings, + // never function pointers or llvm.compiler.used references. Once the linker + // or an LTO hook exposes a live-symbol set, pass it here to drop metadata for + // functions removed by global DCE before materializing the runtime table. + if liveSymbols == nil { + return records + } + out := records[:0] + for _, rec := range records { + if _, ok := liveSymbols[rec.symbol]; ok { + out = append(out, rec) + } + } + if len(out) == 0 { + return nil + } + return out +} + +func readFuncInfo(mod llvm.Module) []funcInfoRecord { + rows := mod.NamedMetadataOperands(llssa.FuncInfoMetadataName) + if len(rows) == 0 { + return nil + } + out := make([]funcInfoRecord, 0, len(rows)) + for _, row := range rows { + fields := row.MDNodeOperands() + if len(fields) != 6 || fields[0].ZExtValue() != 1 { + continue + } + if !fields[1].IsAMDString() || !fields[2].IsAMDString() || !fields[3].IsAMDString() { + continue + } + out = append(out, funcInfoRecord{ + symbol: fields[1].MDString(), + name: fields[2].MDString(), + file: fields[3].MDString(), + line: uint32(fields[4].ZExtValue()), + column: uint32(fields[5].ZExtValue()), + }) + } + return out +} + +func emitFuncInfoTable(ctx *context, pkg llssa.Package, records []funcInfoRecord) { + mod := pkg.Module() + llvmCtx := mod.Context() + i8Type := llvmCtx.Int8Type() + i32Type := llvmCtx.Int32Type() + countType := llvmCtx.IntType(ctx.prog.PointerSize() * 8) + recordType := llvmCtx.StructType([]llvm.Type{ + i32Type, + i32Type, + i32Type, + i32Type, + i32Type, + }, false) + + tablePtr := llvm.AddGlobal(mod, llvm.PointerType(recordType, 0), funcInfoTableSymbol) + stringsPtr := llvm.AddGlobal(mod, llvm.PointerType(i8Type, 0), funcInfoStringsSymbol) + hashPtr := llvm.AddGlobal(mod, llvm.PointerType(i32Type, 0), funcInfoHashSymbol) + count := llvm.AddGlobal(mod, countType, funcInfoCountSymbol) + hashMask := llvm.AddGlobal(mod, countType, funcInfoHashMaskSymbol) + if len(records) == 0 { + tablePtr.SetInitializer(llvm.ConstPointerNull(tablePtr.GlobalValueType())) + stringsPtr.SetInitializer(llvm.ConstPointerNull(stringsPtr.GlobalValueType())) + hashPtr.SetInitializer(llvm.ConstPointerNull(hashPtr.GlobalValueType())) + count.SetInitializer(llvm.ConstInt(countType, 0, false)) + hashMask.SetInitializer(llvm.ConstInt(countType, 0, false)) + return + } + + encoded, err := buildfuncinfo.Encode(toFuncInfoRecords(records)) + if err != nil { + panic(err) + } + + values := make([]llvm.Value, 0, len(encoded.Records)) + for _, rec := range encoded.Records { + values = append(values, llvm.ConstNamedStruct(recordType, []llvm.Value{ + llvm.ConstInt(i32Type, uint64(rec.Symbol), false), + llvm.ConstInt(i32Type, uint64(rec.Name), false), + llvm.ConstInt(i32Type, uint64(rec.File), false), + llvm.ConstInt(i32Type, uint64(rec.Line), false), + llvm.ConstInt(i32Type, uint64(rec.Column), false), + })) + } + arrayType := llvm.ArrayType(recordType, len(values)) + data := llvm.AddGlobal(mod, arrayType, funcInfoDataSymbol) + data.SetInitializer(llvm.ConstArray(recordType, values)) + data.SetLinkage(llvm.PrivateLinkage) + data.SetGlobalConstant(true) + data.SetUnnamedAddr(true) + data.SetAlignment(4) + + stringArrayType := llvm.ArrayType(i8Type, len(encoded.Strings)) + stringData := llvm.AddGlobal(mod, stringArrayType, funcInfoStringsDataSymbol) + stringData.SetInitializer(llvmCtx.ConstString(string(encoded.Strings), false)) + stringData.SetLinkage(llvm.PrivateLinkage) + stringData.SetGlobalConstant(true) + stringData.SetUnnamedAddr(true) + stringData.SetAlignment(1) + + hashValues := make([]llvm.Value, 0, len(encoded.Hash)) + for _, idx := range encoded.Hash { + hashValues = append(hashValues, llvm.ConstInt(i32Type, uint64(idx), false)) + } + hashArrayType := llvm.ArrayType(i32Type, len(hashValues)) + hashData := llvm.AddGlobal(mod, hashArrayType, funcInfoHashDataSymbol) + hashData.SetInitializer(llvm.ConstArray(i32Type, hashValues)) + hashData.SetLinkage(llvm.PrivateLinkage) + hashData.SetGlobalConstant(true) + hashData.SetUnnamedAddr(true) + hashData.SetAlignment(4) + + tablePtr.SetInitializer(llvm.ConstInBoundsGEP(arrayType, data, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + stringsPtr.SetInitializer(llvm.ConstInBoundsGEP(stringArrayType, stringData, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + hashPtr.SetInitializer(llvm.ConstInBoundsGEP(hashArrayType, hashData, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + count.SetInitializer(llvm.ConstInt(countType, uint64(len(encoded.Records)), false)) + hashMask.SetInitializer(llvm.ConstInt(countType, uint64(len(encoded.Hash)-1), false)) +} + +func toFuncInfoRecords(records []funcInfoRecord) []buildfuncinfo.Record { + out := make([]buildfuncinfo.Record, len(records)) + for i, rec := range records { + out[i] = buildfuncinfo.Record{ + Symbol: rec.symbol, + Name: rec.name, + File: rec.file, + Line: rec.line, + Column: rec.column, + } + } + return out +} diff --git a/internal/build/funcinfo_table_test.go b/internal/build/funcinfo_table_test.go new file mode 100644 index 0000000000..f7367ebf56 --- /dev/null +++ b/internal/build/funcinfo_table_test.go @@ -0,0 +1,182 @@ +/* + * 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 build + +import ( + "strings" + "testing" + + "github.com/xgo-dev/llvm" + + "github.com/goplus/llgo/internal/packages" + llssa "github.com/goplus/llgo/ssa" +) + +func TestFuncInfoTableMaterializesMetadataWithoutFunctionPointers(t *testing.T) { + prog := llssa.NewProgram(nil) + src := prog.NewPackage("example.com/p", "example.com/p") + src.EmitFuncInfo("example.com/p.live", "example.com/p.Live", "live.go", 17, 3) + src.EmitFuncInfo("example.com/p.live", "example.com/p.LiveDuplicate", "dup.go", 19, 1) + + records := collectFuncInfo([]Package{{LPkg: src}}) + if len(records) != 1 { + t.Fatalf("collectFuncInfo returned %d records, want 1", len(records)) + } + if got := records[0]; got.symbol != "example.com/p.live" || got.name != "example.com/p.Live" || got.file != "live.go" || got.line != 17 || got.column != 3 { + t.Fatalf("unexpected record: %+v", got) + } + + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + entry := genMainModule(ctx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records}) + ir := entry.LPkg.String() + for _, want := range []string{ + "@__llgo_funcinfo_table = global ptr", + "@__llgo_funcinfo_strings = global ptr", + "@__llgo_funcinfo_hash = global ptr", + "@__llgo_funcinfo_count = global i64 1", + "@__llgo_funcinfo_hash_mask = global i64 1", + `@"__llgo_funcinfo_table$data" = private unnamed_addr constant [1 x { i32, i32, i32, i32, i32 }]`, + `@"__llgo_funcinfo_strings$data" = private unnamed_addr constant [47 x i8]`, + `@"__llgo_funcinfo_hash$data" = private unnamed_addr constant [2 x i32]`, + `example.com/p.live\00`, + `example.com/p.Live\00`, + `live.go\00`, + "i32 17", + "i32 3", + } { + if !strings.Contains(ir, want) { + t.Fatalf("funcinfo table IR missing %q:\n%s", want, ir) + } + } + if strings.Contains(ir, `ptr @"example.com/p.live"`) { + t.Fatalf("funcinfo table must not reference function pointers:\n%s", ir) + } +} + +func TestPrepareFuncInfoTableRecordsFiltersLiveSymbols(t *testing.T) { + records := []funcInfoRecord{ + {symbol: "dead", name: "dead"}, + {symbol: "live", name: "live"}, + } + if got := prepareFuncInfoTableRecords(nil, nil); got != nil { + t.Fatalf("empty records = %+v, want nil", got) + } + if got := prepareFuncInfoTableRecords(records, nil); len(got) != 2 { + t.Fatalf("nil live set kept %d records, want 2", len(got)) + } + got := prepareFuncInfoTableRecords(records, map[string]none{"live": {}}) + if len(got) != 1 || got[0].symbol != "live" { + t.Fatalf("filtered records = %+v, want live only", got) + } + if got := prepareFuncInfoTableRecords(records, map[string]none{}); got != nil { + t.Fatalf("empty live set = %+v, want nil", got) + } +} + +func TestFuncInfoTablePoolsRepeatedStrings(t *testing.T) { + prog := llssa.NewProgram(nil) + records := []funcInfoRecord{ + {symbol: "example.com/p.a", name: "example.com/p.A", file: "shared.go", line: 10}, + {symbol: "example.com/p.b", name: "example.com/p.B", file: "shared.go", line: 20}, + } + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + entry := genMainModule(ctx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records}) + if got := strings.Count(entry.LPkg.String(), `shared.go\00`); got != 1 { + t.Fatalf("shared file string emitted %d times, want 1", got) + } +} + +func TestFuncInfoTableEmptyDefinitions(t *testing.T) { + prog := llssa.NewProgram(nil) + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + entry := genMainModule(ctx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{}) + ir := entry.LPkg.String() + for _, want := range []string{ + "@__llgo_funcinfo_table = global ptr null", + "@__llgo_funcinfo_strings = global ptr null", + "@__llgo_funcinfo_hash = global ptr null", + "@__llgo_funcinfo_count = global i64 0", + "@__llgo_funcinfo_hash_mask = global i64 0", + } { + if !strings.Contains(ir, want) { + t.Fatalf("empty funcinfo table IR missing %q:\n%s", want, ir) + } + } +} + +func TestFuncInfoTableIgnoresInvalidMetadata(t *testing.T) { + prog := llssa.NewProgram(nil) + pkg := prog.NewPackage("example.com/p", "example.com/p") + mod := pkg.Module() + ctx := mod.Context() + i32 := ctx.Int32Type() + mdstr := func(s string) llvm.Metadata { return ctx.MDString(s) } + mdint := func(v uint64) llvm.Metadata { + return llvm.ConstInt(i32, v, false).ConstantAsMetadata() + } + add := func(fields ...llvm.Metadata) { + mod.AddNamedMetadataOperand(llssa.FuncInfoMetadataName, ctx.MDNode(fields)) + } + + add(mdstr("short")) + add(mdint(2), mdstr("bad.version"), mdstr("bad.version"), mdstr("bad.go"), mdint(1), mdint(1)) + add(mdint(1), mdint(0), mdstr("bad.symbol"), mdstr("bad.go"), mdint(1), mdint(1)) + add(mdint(1), mdstr(""), mdstr("empty.symbol"), mdstr("empty.go"), mdint(1), mdint(1)) + + if got := readFuncInfo(mod); len(got) != 1 || got[0].symbol != "" { + t.Fatalf("readFuncInfo invalid rows = %+v, want one empty-symbol row", got) + } + if got := collectFuncInfo([]Package{nil, {}, {LPkg: pkg}}); len(got) != 0 { + t.Fatalf("collectFuncInfo invalid rows = %+v, want none", got) + } + + empty := ctx.NewModule("empty") + defer empty.Dispose() + if got := readFuncInfo(empty); got != nil { + t.Fatalf("readFuncInfo(empty) = %+v, want nil", got) + } +} diff --git a/internal/build/main_module.go b/internal/build/main_module.go index d5ac73671e..9f68a976ac 100644 --- a/internal/build/main_module.go +++ b/internal/build/main_module.go @@ -43,6 +43,7 @@ type genConfig struct { methodByIndex map[int]none methodByName map[string]none abiSymbols map[string]none + funcInfo []funcInfoRecord } // genMainModule generates the main entry module for an llgo program. @@ -60,6 +61,7 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g argvValueType := prog.Pointer(prog.CStr()) argvVar := mainPkg.NewVarEx("__llgo_argv", prog.Pointer(argvValueType)) argvVar.InitNil() + emitFuncInfoTable(ctx, mainPkg, cfg.funcInfo) exportFile := pkg.ExportFile if exportFile == "" { diff --git a/runtime/internal/clite/debug/_wrap/debug.c b/runtime/internal/clite/debug/_wrap/debug.c index 32d87903bf..cf050c8848 100644 --- a/runtime/internal/clite/debug/_wrap/debug.c +++ b/runtime/internal/clite/debug/_wrap/debug.c @@ -7,6 +7,7 @@ #endif #include +#include #include void *llgo_address() { @@ -14,10 +15,14 @@ void *llgo_address() { } int llgo_addrinfo(void *addr, Dl_info *info) { - return dladdr(addr, info); + int saved_errno = errno; + int ret = dladdr(addr, info); + errno = saved_errno; + return ret; } void llgo_stacktrace(int skip, void *ctx, int (*fn)(void *ctx, void *pc, void *offset, void *sp, char *name)) { + int saved_errno = errno; unw_cursor_t cursor; unw_context_t context; unw_word_t offset, pc, sp; @@ -31,11 +36,17 @@ void llgo_stacktrace(int skip, void *ctx, int (*fn)(void *ctx, void *pc, void *o continue; } if (unw_get_reg(&cursor, UNW_REG_IP, &pc) == 0) { - unw_get_proc_name(&cursor, fname, sizeof(fname), &offset); + fname[0] = 0; + offset = 0; + if (unw_get_proc_name(&cursor, fname, sizeof(fname), &offset) == 0) { + fname[sizeof(fname) - 1] = 0; + } unw_get_reg(&cursor, UNW_REG_SP, &sp); if (fn(ctx, (void*)pc, (void*)offset, (void*)sp, fname) == 0) { + errno = saved_errno; return; } } } -} \ No newline at end of file + errno = saved_errno; +} diff --git a/runtime/internal/lib/runtime/extern.go b/runtime/internal/lib/runtime/extern.go index 1fb397dd8a..377c973876 100644 --- a/runtime/internal/lib/runtime/extern.go +++ b/runtime/internal/lib/runtime/extern.go @@ -9,16 +9,26 @@ import ( ) 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 { + if Callers(skip+2, pcs[:]) < 1 { return 0, "", 0, false } - return pcs[0], "???", 1, true + sym := frameSymbol(pcs[0]) + file, line = sym.file, sym.line + if file == "" { + file = "???" + } + if line == 0 { + line = 1 + } + return pcs[0], file, line, true } func Callers(skip int, pc []uintptr) int { + return callers(skip+1, pc) +} + +func callers(skip int, pc []uintptr) int { if len(pc) == 0 { return 0 } @@ -28,6 +38,7 @@ func Callers(skip int, pc []uintptr) int { return false } pc[n] = fr.PC + recordFrameSymbol(fr.PC, fr.Offset, fr.Name) n++ return true }) diff --git a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go index 5b8155d0e8..bd131c9a25 100644 --- a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go +++ b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go @@ -85,5 +85,17 @@ func NumGoroutine() int { func SetCPUProfileRate(hz int) {} func FuncForPC(pc uintptr) *Func { - return nil + sym := frameSymbol(pc) + if !sym.ok && sym.function == "" { + return &Func{entry: pc, name: unknownFunctionName(pc)} + } + name := sym.function + if name == "" { + name = unknownFunctionName(pc) + } + entry := sym.entry + if entry == 0 { + entry = pc + } + return &Func{entry: entry, name: name} } diff --git a/runtime/internal/lib/runtime/runtime2.go b/runtime/internal/lib/runtime/runtime2.go index 7327f9892c..8bf049e087 100644 --- a/runtime/internal/lib/runtime/runtime2.go +++ b/runtime/internal/lib/runtime/runtime2.go @@ -17,7 +17,55 @@ type _func struct { } func Stack(buf []byte, all bool) int { - return 0 + var pcs [64]uintptr + n := Callers(0, pcs[:]) + out := make([]byte, 0, 1024) + out = append(out, "goroutine 1 [running]:\n"...) + frames := CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function == "" { + frame.Function = unknownFunctionName(frame.PC) + } + out = append(out, frame.Function...) + out = append(out, "()\n\t"...) + if frame.File == "" { + out = append(out, "???"...) + } else { + out = append(out, frame.File...) + } + out = append(out, ':') + out = appendInt(out, frame.Line) + out = append(out, ' ') + out = append(out, "+0x0\n"...) + if !more { + break + } + } + if len(out) > len(buf) { + copy(buf, out[:len(buf)]) + return len(buf) + } + copy(buf, out) + return len(out) +} + +func appendInt(out []byte, v int) []byte { + if v == 0 { + return append(out, '0') + } + if v < 0 { + out = append(out, '-') + v = -v + } + var digits [20]byte + i := len(digits) + for v > 0 { + i-- + digits[i] = byte('0' + v%10) + v /= 10 + } + return append(out, digits[i:]...) } type traceError string diff --git a/runtime/internal/lib/runtime/symtab.go b/runtime/internal/lib/runtime/symtab.go index a4d18d9b30..84e04aa943 100644 --- a/runtime/internal/lib/runtime/symtab.go +++ b/runtime/internal/lib/runtime/symtab.go @@ -105,6 +105,249 @@ func unknownFunctionName(pc uintptr) string { return "pc=" + uintptrHex(pc) } +type pcSymbol struct { + pc uintptr + entry uintptr + function string + file string + line int + startLine int + ok bool +} + +type frameSymbolCacheEntry struct { + pc uintptr + offset uintptr + name string +} + +const frameSymbolCacheSize = 128 + +var frameSymbolCache [frameSymbolCacheSize]frameSymbolCacheEntry + +func recordFrameSymbol(pc, offset uintptr, name string) { + if pc == 0 || name == "" { + return + } + i := (pc >> 4) & (frameSymbolCacheSize - 1) + frameSymbolCache[i] = frameSymbolCacheEntry{pc: pc, offset: offset, name: name} +} + +type runtimeFuncInfoRecord struct { + symbol uint32 + name uint32 + file uint32 + line uint32 + column uint32 +} + +//go:linkname runtimeFuncInfoTable __llgo_funcinfo_table +var runtimeFuncInfoTable *runtimeFuncInfoRecord + +//go:linkname runtimeFuncInfoStrings __llgo_funcinfo_strings +var runtimeFuncInfoStrings *c.Char + +//go:linkname runtimeFuncInfoHash __llgo_funcinfo_hash +var runtimeFuncInfoHash *uint32 + +//go:linkname runtimeFuncInfoCount __llgo_funcinfo_count +var runtimeFuncInfoCount uintptr + +//go:linkname runtimeFuncInfoHashMask __llgo_funcinfo_hash_mask +var runtimeFuncInfoHashMask uintptr + +func hasStringPrefix(s, prefix string) bool { + if len(s) < len(prefix) { + return false + } + for i := 0; i < len(prefix); i++ { + if s[i] != prefix[i] { + return false + } + } + return true +} + +func publicFunctionName(name string) string { + const commandLineArguments = "command-line-arguments." + if hasStringPrefix(name, commandLineArguments) { + return "main." + name[len(commandLineArguments):] + } + if len(name) > 0 && name[0] == '_' { + name = name[1:] + } + return name +} + +func cStringEqual(cstr *c.Char, s string) bool { + return cStringCompare(cstr, s) == 0 +} + +func cStringCompare(cstr *c.Char, s string) int { + if cstr == nil { + if s == "" { + return 0 + } + return -1 + } + ptr := unsafe.Pointer(cstr) + for i := 0; ; i++ { + c := *(*byte)(unsafe.Add(ptr, i)) + if i == len(s) { + if c == 0 { + return 0 + } + return 1 + } + if c == 0 { + return -1 + } + if c < s[i] { + return -1 + } + if c > s[i] { + return 1 + } + } +} + +func funcInfoCString(off uint32) *c.Char { + if runtimeFuncInfoStrings == nil { + return nil + } + return (*c.Char)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoStrings), uintptr(off))) +} + +func funcInfoAt(i uintptr) *runtimeFuncInfoRecord { + size := unsafe.Sizeof(*runtimeFuncInfoTable) + return (*runtimeFuncInfoRecord)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoTable), i*size)) +} + +func funcInfoHashString(s string) uintptr { + const ( + offset = uint32(2166136261) + prime = uint32(16777619) + ) + h := offset + for i := 0; i < len(s); i++ { + h ^= uint32(s[i]) + h *= prime + } + return uintptr(h) +} + +func funcInfoForSymbol(symbol string) *runtimeFuncInfoRecord { + if symbol == "" || runtimeFuncInfoTable == nil || runtimeFuncInfoCount == 0 { + return nil + } + if runtimeFuncInfoHash != nil && runtimeFuncInfoHashMask != 0 { + slot := funcInfoHashString(symbol) & runtimeFuncInfoHashMask + for probes := uintptr(0); probes <= runtimeFuncInfoHashMask; probes++ { + idx := *(*uint32)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoHash), slot*unsafe.Sizeof(*runtimeFuncInfoHash))) + if idx == 0 { + return nil + } + if uintptr(idx) <= runtimeFuncInfoCount { + rec := funcInfoAt(uintptr(idx) - 1) + if cStringEqual(funcInfoCString(rec.symbol), symbol) { + return rec + } + } + slot = (slot + 1) & runtimeFuncInfoHashMask + } + return nil + } + for i := uintptr(0); i < runtimeFuncInfoCount; i++ { + rec := funcInfoAt(i) + if cStringEqual(funcInfoCString(rec.symbol), symbol) { + return rec + } + } + return nil +} + +func applyFuncInfo(sym *pcSymbol, rawFunction string) { + rec := funcInfoForSymbol(rawFunction) + if rec == nil { + public := publicFunctionName(rawFunction) + if public != rawFunction { + rec = funcInfoForSymbol(public) + } + } + if rec == nil { + return + } + if name := safeGoString(funcInfoCString(rec.name), ""); name != "" { + sym.function = publicFunctionName(name) + } + if file := safeGoString(funcInfoCString(rec.file), ""); file != "" { + if sym.file == "" { + sym.file = file + } + } + if rec.line != 0 { + sym.startLine = int(rec.line) + if sym.line == 0 { + sym.line = int(rec.line) + } + } + sym.ok = sym.ok || sym.function != "" || sym.file != "" +} + +func cachedFrameSymbol(pc uintptr) pcSymbol { + i := (pc >> 4) & (frameSymbolCacheSize - 1) + entry := frameSymbolCache[i] + if entry.pc != pc || entry.name == "" { + return pcSymbol{pc: pc} + } + rawFn := entry.name + fn := publicFunctionName(rawFn) + sym := pcSymbol{ + pc: pc, + entry: pc - entry.offset, + function: fn, + ok: fn != "" || entry.offset != 0, + } + applyFuncInfo(&sym, rawFn) + return sym +} + +func addrInfoSymbol(pc uintptr) pcSymbol { + var info clitedebug.Info + if clitedebug.Addrinfo(unsafe.Pointer(pc), &info) == 0 { + return cachedFrameSymbol(pc) + } + rawFn := safeGoString(info.Sname, "") + if rawFn == "" { + if sym := cachedFrameSymbol(pc); sym.ok { + return sym + } + } + fn := publicFunctionName(rawFn) + sym := pcSymbol{ + pc: pc, + entry: uintptr(info.Saddr), + function: fn, + ok: fn != "" || info.Saddr != nil, + } + applyFuncInfo(&sym, rawFn) + return sym +} + +func frameSymbol(pc uintptr) pcSymbol { + sym := addrInfoSymbol(pc) + if pc == 0 { + return sym + } + if sym.entry == 0 || pc > sym.entry { + if callSym := addrInfoSymbol(pc - 1); callSym.ok { + callSym.pc = pc + return callSym + } + } + return sym +} + func (ci *Frames) Next() (frame Frame, more bool) { for len(ci.frames) < 2 { // Find the next frame. @@ -119,8 +362,8 @@ func (ci *Frames) Next() (frame Frame, more bool) { } else { pc, ci.callers = ci.callers[0], ci.callers[1:] } - info := &clitedebug.Info{} - if clitedebug.Addrinfo(unsafe.Pointer(pc), info) == 0 { + sym := frameSymbol(pc) + if !sym.ok { ci.frames = append(ci.frames, Frame{ PC: pc, Function: unknownFunctionName(pc), @@ -131,17 +374,22 @@ func (ci *Frames) Next() (frame Frame, more bool) { }) continue } - fn := safeGoString(info.Sname, "") + fn := sym.function if fn == "" { fn = unknownFunctionName(pc) } + var f *Func + if sym.entry != 0 || fn != "" { + f = &Func{entry: sym.entry, name: fn} + } ci.frames = append(ci.frames, Frame{ PC: pc, + Func: f, Function: fn, - File: "", - Line: 0, - startLine: 0, - Entry: uintptr(info.Saddr), + File: sym.file, + Line: sym.line, + startLine: sym.startLine, + Entry: sym.entry, }) } @@ -176,19 +424,27 @@ 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 + entry uintptr + name string } func (f *Func) Name() string { - panic("todo") + if f == nil { + return "" + } + return f.name } -func (f *Func) FileLine(pc uintptr) (file string, line int) { - var info clitedebug.Info - if pc == 0 || clitedebug.Addrinfo(unsafe.Pointer(pc), &info) == 0 { - return "", 0 +func (f *Func) Entry() uintptr { + if f == nil { + return 0 } - return safeGoString(info.Fname, ""), 0 + return f.entry +} + +func (f *Func) FileLine(pc uintptr) (file string, line int) { + sym := frameSymbol(pc) + return sym.file, sym.line } // moduledata records information about the layout of the executable diff --git a/ssa/decl.go b/ssa/decl.go index 115bf28fc2..3e17b40e9f 100644 --- a/ssa/decl.go +++ b/ssa/decl.go @@ -423,4 +423,9 @@ func (p Function) Inline(inline inlineAttr) { p.impl.AddFunctionAttr(inlineAttr) } +func (p Function) DisableTailCalls() { + attr := p.Pkg.mod.Context().CreateStringAttribute("disable-tail-calls", "true") + p.impl.AddFunctionAttr(attr) +} + // ----------------------------------------------------------------------------- diff --git a/ssa/funcinfo.go b/ssa/funcinfo.go new file mode 100644 index 0000000000..734399093d --- /dev/null +++ b/ssa/funcinfo.go @@ -0,0 +1,63 @@ +/* + * 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 ssa + +import "github.com/xgo-dev/llvm" + +const ( + FuncInfoMetadataName = "llgo.funcinfo" + funcInfoVersion = 1 +) + +// EnableFuncInfoMetadata controls emission of DCE-safe function source +// metadata. The metadata intentionally stores symbol names as strings instead +// of function pointer operands, so it can be consumed before materializing a +// final runtime line/func table without keeping otherwise-dead functions alive. +func (p Program) EnableFuncInfoMetadata(enable bool) { + p.enableFuncInfoMetadata = enable +} + +func (p Program) FuncInfoMetadataEnabled() bool { + return p.enableFuncInfoMetadata +} + +// EmitFuncInfo records a function's linker symbol, Go name, and declaration +// source position as LLVM named metadata. The row layout is: +// +// !{i32 version, !"symbol", !"go.name", !"file", i32 line, i32 column} +func (p Package) EmitFuncInfo(symbol, name, file string, line, column int) { + if symbol == "" { + return + } + if line < 0 { + line = 0 + } + if column < 0 { + column = 0 + } + i32 := p.Prog.Int32().ll + p.mod.AddNamedMetadataOperand(FuncInfoMetadataName, + p.Prog.ctx.MDNode([]llvm.Metadata{ + llvm.ConstInt(i32, funcInfoVersion, false).ConstantAsMetadata(), + p.Prog.ctx.MDString(symbol), + p.Prog.ctx.MDString(name), + p.Prog.ctx.MDString(file), + llvm.ConstInt(i32, uint64(line), false).ConstantAsMetadata(), + llvm.ConstInt(i32, uint64(column), false).ConstantAsMetadata(), + }), + ) +} diff --git a/ssa/package.go b/ssa/package.go index 74256eddcc..f0f118d639 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -233,6 +233,8 @@ type aProgram struct { is32Bits bool enableGoGlobalDCE bool + + enableFuncInfoMetadata bool } type AbiSymbol struct { diff --git a/ssa/ssa_test.go b/ssa/ssa_test.go index 74d4242286..378bde70ff 100644 --- a/ssa/ssa_test.go +++ b/ssa/ssa_test.go @@ -200,6 +200,104 @@ func TestNewFuncExLLVMUsed(t *testing.T) { } } +func TestFuncInfoMetadataDoesNotPreserveFunctions(t *testing.T) { + testFuncInfoMetadataDoesNotPreserveFunctions(t) +} + +func testFuncInfoMetadataDoesNotPreserveFunctions(t *testing.T) { + t.Helper() + + prog := NewProgram(nil) + if prog.FuncInfoMetadataEnabled() { + t.Fatal("funcinfo metadata should be disabled by default") + } + prog.EnableFuncInfoMetadata(true) + if !prog.FuncInfoMetadataEnabled() { + t.Fatal("funcinfo metadata should be enabled") + } + + pkg := prog.NewPackage("main", "main") + sig := types.NewSignatureType(nil, nil, nil, nil, nil, false) + + pkg.NewFunc("main.unused", sig, InGo) + pkg.EmitFuncInfo("", "ignored", "ignored.go", -1, -1) + if ir := pkg.String(); strings.Contains(ir, FuncInfoMetadataName) { + t.Fatalf("empty symbol should not emit funcinfo metadata:\n%s", ir) + } + + pkg.EmitFuncInfo("main.unused", "main.unused", "unused.go", 7, 1) + pkg.EmitFuncInfo("main.negative", "main.negative", "negative.go", -7, -1) + ir := pkg.String() + + if !strings.Contains(ir, `!llgo.funcinfo = !{!`) { + t.Fatalf("missing %s metadata:\n%s", FuncInfoMetadataName, ir) + } + for _, want := range []string{`!"main.unused"`, `!"unused.go"`, `i32 7`, `!"main.negative"`, `!"negative.go"`, `i32 0`} { + if !strings.Contains(ir, want) { + t.Fatalf("missing funcinfo field %s:\n%s", want, ir) + } + } + if strings.Contains(ir, "llvm.compiler.used") { + t.Fatalf("funcinfo metadata must not preserve symbols with llvm.compiler.used:\n%s", ir) + } + if strings.Contains(ir, `ptr @"main.unused"`) || strings.Contains(ir, `ptr @main.unused`) { + t.Fatalf("funcinfo metadata must not contain function pointer operands:\n%s", ir) + } +} + +func TestFuncInfoMetadataDoesNotBlockGlobalDCE(t *testing.T) { + testFuncInfoMetadataDoesNotBlockGlobalDCE(t) +} + +func testFuncInfoMetadataDoesNotBlockGlobalDCE(t *testing.T) { + t.Helper() + + prog := NewProgram(nil) + pkg := prog.NewPackage("main", "main") + sig := types.NewSignatureType(nil, nil, nil, nil, nil, false) + + live := pkg.NewFunc("main.main", sig, InGo) + lb := live.MakeBody(1) + lb.Return() + lb.EndBuild() + + unused := pkg.NewFuncEx("main.unused", sig, InGo, false, true) + ub := unused.MakeBody(1) + ub.Return() + ub.EndBuild() + pkg.EmitFuncInfo(unused.Name(), unused.Name(), "unused.go", 7, 1) + + mod := pkg.Module() + if mod.NamedFunction("main.unused").IsNil() { + t.Fatal("missing main.unused before DCE") + } + mod.SetDataLayout(prog.DataLayout()) + mod.SetTarget(prog.Target().Spec().Triple) + pbo := llvm.NewPassBuilderOptions() + defer pbo.Dispose() + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify module before DCE: %v", err) + } + if err := mod.RunPasses("globaldce", prog.TargetMachine(), pbo); err != nil { + t.Fatalf("run globaldce: %v", err) + } + if !mod.NamedFunction("main.unused").IsNil() { + t.Fatalf("funcinfo metadata kept main.unused alive:\n%s", mod.String()) + } + if mod.NamedFunction("main.main").IsNil() { + t.Fatalf("globaldce removed externally visible live function:\n%s", mod.String()) + } + if ir := mod.String(); !strings.Contains(ir, `!"main.unused"`) { + t.Fatalf("funcinfo metadata should remain available for later materialization:\n%s", ir) + } +} + +func TestDevLTOGlobalDCEFuncInfoMetadata(t *testing.T) { + requireGoGlobalDCE(t) + testFuncInfoMetadataDoesNotPreserveFunctions(t) + testFuncInfoMetadataDoesNotBlockGlobalDCE(t) +} + func requireGoGlobalDCE(t *testing.T) { t.Helper() } diff --git a/test/go/runtime_lineinfo_stack_test.go b/test/go/runtime_lineinfo_stack_test.go new file mode 100644 index 0000000000..4d46601112 --- /dev/null +++ b/test/go/runtime_lineinfo_stack_test.go @@ -0,0 +1,187 @@ +/* + * 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 gotest + +import ( + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" +) + +const runtimeLineInfoProbe = `package main + +import ( + "strconv" + "runtime" + "runtime/debug" + "strings" + _ "unsafe" +) + +func main() { + checkCaller() + checkCallerSkip() + checkFrames() + checkFuncForPC() + checkFuncInfoRename() + checkRuntimeStack() + checkPanicStack() +} + +//go:noinline +func checkCaller() { + _, file, line, ok := runtime.Caller(0) // CALLER_MARK + if !ok || !strings.HasSuffix(file, "main.go") || line != CALLER_LINE { + panic("bad caller: " + file + ":" + strconv.Itoa(line)) + } +} + +//go:noinline +func checkCallerSkip() { + helperCallerSkip() +} + +//go:noinline +func helperCallerSkip() { + _, file, line, ok := runtime.Caller(1) + if !ok || !strings.HasSuffix(file, "main.go") || line != CALLER_SKIP_LINE { + panic("bad caller skip: " + file + ":" + strconv.Itoa(line)) + } +} + +//go:noinline +func checkFrames() { + var pcs [8]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function == "main.checkFrames" { + if !strings.HasSuffix(frame.File, "main.go") || frame.Line == 0 { + panic("bad frame") + } + return + } + if !more { + break + } + } + panic("missing frame") +} + +//go:noinline +func checkFuncForPC() { + pc, _, _, ok := runtime.Caller(0) // FUNC_FILELINE_MARK + if !ok { + panic("missing pc") + } + fn := runtime.FuncForPC(pc) + if fn == nil { + panic("missing func") + } + if name := fn.Name(); name != "main.checkFuncForPC" { + panic("bad func: " + name) + } + if entry := fn.Entry(); entry == 0 { + panic("missing func entry") + } + file, line := fn.FileLine(pc) + if !strings.HasSuffix(file, "main.go") || line != FUNC_FILELINE_LINE { + panic("bad func fileline: " + file + ":" + strconv.Itoa(line)) + } +} + +//go:noinline +func checkFuncInfoRename() { + pc := renamedPC() + if name := runtime.FuncForPC(pc).Name(); name != "main.renamedPC" { + panic("bad renamed func: " + name) + } +} + +//go:linkname renamedPC main.renamedPCSymbol +//go:noinline +func renamedPC() uintptr { + pc, _, _, ok := runtime.Caller(0) + if !ok { + panic("missing renamed pc") + } + return pc +} + +//go:noinline +func checkRuntimeStack() { + var buf [4096]byte + n := runtime.Stack(buf[:], false) // RUNTIME_STACK_MARK + stack := string(buf[:n]) + if !strings.Contains(stack, "main.checkRuntimeStack") || !strings.Contains(stack, "main.go:RUNTIME_STACK_LINE") { + panic("bad runtime stack: " + stack) + } +} + +//go:noinline +func checkPanicStack() { + defer func() { // DEBUG_STACK_MARK + if recover() == nil { + panic("missing panic") + } + stack := string(debug.Stack()) + if !strings.Contains(stack, "main.checkPanicStack") || !strings.Contains(stack, "main.go:DEBUG_STACK_LINE") { + panic("bad stack: " + stack) + } + }() + s := []int{1, 2, 3} + _ = s[3] +} +` + +func TestRuntimeLineInfoAndStack(t *testing.T) { + source := runtimeLineInfoProbe + source = strings.ReplaceAll(source, "CALLER_LINE", strconv.Itoa(markerLine(source, "func checkCaller()"))) + source = strings.ReplaceAll(source, "CALLER_SKIP_LINE", strconv.Itoa(markerLine(source, "func checkCallerSkip()"))) + source = strings.ReplaceAll(source, "FUNC_FILELINE_LINE", strconv.Itoa(markerLine(source, "func checkFuncForPC()"))) + source = strings.ReplaceAll(source, "RUNTIME_STACK_LINE", strconv.Itoa(markerLine(source, "func checkRuntimeStack()"))) + source = strings.ReplaceAll(source, "DEBUG_STACK_LINE", strconv.Itoa(markerLine(source, "DEBUG_STACK_MARK"))) + + dir := t.TempDir() + file := filepath.Join(dir, "main.go") + if err := os.WriteFile(file, []byte(source), 0644); err != nil { + t.Fatal(err) + } + + repoRoot := findStringConversionRepoRoot(t) + t.Setenv("LLGO_ROOT", repoRoot) + cmd := exec.Command("go", "run", "./cmd/llgo", "run", "-a", file) + cmd.Dir = repoRoot + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("llgo lineinfo probe failed: %v\n%s", err, out) + } +} + +func markerLine(source, marker string) int { + line := 1 + for _, part := range strings.SplitAfter(source, "\n") { + if strings.Contains(part, marker) { + return line + } + line++ + } + panic("missing marker " + marker) +} diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index d16ea32788..458df947fc 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2159,10 +2159,6 @@ xfails: directive: run case: fixedbugs/issue29504.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue29735.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue32477.go