diff --git a/benchmark/runtime_funcinfo/.gitignore b/benchmark/runtime_funcinfo/.gitignore new file mode 100644 index 0000000000..d4632d51c8 --- /dev/null +++ b/benchmark/runtime_funcinfo/.gitignore @@ -0,0 +1,2 @@ +out/ +out-* diff --git a/benchmark/runtime_funcinfo/README.md b/benchmark/runtime_funcinfo/README.md new file mode 100644 index 0000000000..b473f94602 --- /dev/null +++ b/benchmark/runtime_funcinfo/README.md @@ -0,0 +1,62 @@ +# Runtime Funcinfo Benchmark + +This benchmark keeps runtime funcinfo measurements comparable across branches by +generating the same probe programs and rebuilding them with each compiler/root +pair in one run. + +It covers: + +- hot runtime metadata calls: `Caller`, `Callers`, `CallersFrames`, + `FuncForPC`, and `Func.FileLine`. +- deep stacks through direct calls, interface calls, and closures. +- many packages and methods, generated from configurable package/method counts. +- cold first-use runtime metadata paths, including lazy table initialization. +- a stdlib-heavy program with `encoding/json`, `text/template`, `regexp`, + `go/parser`, `go/token`, and `net/netip` imports. +- ordinary code (`plain`): pure-compute probes (recursive `fib`, JSON + round-trip, `sort.Ints`, map churn) with no runtime introspection at all, + measuring what the funcinfo machinery costs code that never asks for it. + +Generated modules use `example.com/llgo-bench/...` import paths. This is +intentional: LLGo does not enable caller-frame tracking for stdlib-shaped paths +without a dot, and that would benchmark the fallback path instead of normal +third-party package behavior. + +Example: + +```sh +go run ./benchmark/runtime_funcinfo \ + -runs=11 \ + -llgo-opt=2 \ + -variant go=go \ + -variant main=llgo,/path/to/llgo-main,/path/to/llgo-main-root \ + -variant 2002=llgo,/path/to/llgo-2002,/path/to/llgo-2002-root \ + -variant 2009=llgo,/path/to/llgo-2009,/path/to/llgo-2009-root \ + -variant 2010=llgo,/path/to/llgo-2010,/path/to/llgo-2010-root +``` + +Add `-include-lto` to build an additional `+lto` variant for every LLGo +compiler. LLGo builds use `-O2` by default; pass `-llgo-opt=` to omit the +optimization flag. Add `-scales=6x6,12x12,24x24` to generate separate +`multipkg_*` and `cold_*` scenarios for several package/function counts in one +run. Output is written to `benchmark/runtime_funcinfo/out` by default: + +- `summary.md`: markdown performance and size tables. +- `results.json`: raw build and run data. +- `work/`: generated probe modules. +- `bin/`: generated executables. + +Performance cells are `best/trimmed avg` from process-level runs. The trimmed +average drops one minimum and one maximum when at least three runs are present. +`-iters` is a base iteration count: `hot` uses the full count, `deep` uses a +quarter, `multipkg`/`stdlib` use one twentieth, and `plain` uses 1/2000 +because each operation does substantially more work. + +`multipkg.FuncForPCMany` and `multipkg.FileLineMany` are batch metrics over all +generated target functions (`-packages * -methods`, 144 targets with the default +settings), not single-lookup timings. + +`cold.First*` metrics are single measurements from a fresh process and include +lazy runtime initialization that has not already happened in that process. +`cold.WarmFuncForPCMany` and `cold.WarmFileLineMany` use the same batch target +count as `multipkg`. diff --git a/benchmark/runtime_funcinfo/main.go b/benchmark/runtime_funcinfo/main.go new file mode 100644 index 0000000000..618b78499a --- /dev/null +++ b/benchmark/runtime_funcinfo/main.go @@ -0,0 +1,1596 @@ +package main + +import ( + "encoding/json" + "errors" + "flag" + "fmt" + "math" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + "time" +) + +type variantFlag []string + +func (v *variantFlag) String() string { + return strings.Join(*v, ";") +} + +func (v *variantFlag) Set(s string) error { + *v = append(*v, s) + return nil +} + +type variant struct { + Name string `json:"name"` + Kind string `json:"kind"` + Tool string `json:"tool"` + Root string `json:"root,omitempty"` + LTO bool `json:"lto,omitempty"` +} + +type scenario struct { + Name string `json:"name"` + Kind string `json:"kind"` + Dir string `json:"-"` + PackageCount int `json:"package_count,omitempty"` + MethodCount int `json:"method_count,omitempty"` + TargetCount int `json:"target_count,omitempty"` + Scale scenarioSize `json:"scale,omitempty"` +} + +type scenarioSize struct { + Packages int `json:"packages"` + Methods int `json:"methods"` +} + +type buildResult struct { + Variant string `json:"variant"` + Scenario string `json:"scenario"` + Binary string `json:"binary"` + Size int64 `json:"size_bytes"` + BuildMS int64 `json:"build_ms"` + Error string `json:"error,omitempty"` +} + +type runResult struct { + Variant string `json:"variant"` + Scenario string `json:"scenario"` + Metrics map[string][]int64 `json:"metrics_ns"` + Error string `json:"error,omitempty"` + Output string `json:"output,omitempty"` + Env map[string]string `json:"env,omitempty"` +} + +type resultFile struct { + GeneratedAt time.Time `json:"generated_at"` + PackageCount int `json:"package_count"` + MethodCount int `json:"method_count"` + Variants []variant `json:"variants"` + Scenarios []string `json:"scenarios"` + ScenarioMeta []scenario `json:"scenario_meta,omitempty"` + Builds []buildResult `json:"builds"` + Runs []runResult `json:"runs"` +} + +func main() { + var variants variantFlag + outDir := flag.String("out", filepath.Join("benchmark", "runtime_funcinfo", "out"), "output directory") + runs := flag.Int("runs", 11, "process runs per executable") + iters := flag.Int("iters", 200000, "inner benchmark iterations") + llgoOpt := flag.String("llgo-opt", "2", "LLGo optimization level passed as -O; empty disables the flag") + scenarioList := flag.String("scenarios", "hot,deep,multipkg,cold,stdlib,plain", "comma-separated scenarios") + includeLTO := flag.Bool("include-lto", false, "also build full-LTO variants for LLGo compilers") + pkgCount := flag.Int("packages", 12, "generated package count for multipkg") + methodCount := flag.Int("methods", 12, "generated functions and methods per generated package") + scaleList := flag.String("scales", "", "optional comma-separated package x method scales for multipkg/cold, for example 6x6,12x12,24x24") + depthList := flag.String("depths", "", "optional comma-separated call depths for the deep scenario, for example 32,128,512") + bigList := flag.String("bigsizes", "", "optional comma-separated funcs x statements sizes for the bigfunc scenario, for example 32x200,16x2000") + flag.Var(&variants, "variant", "variant definition: name=go or name=llgo,/path/to/llgo,/path/to/root") + flag.Parse() + + if len(variants) == 0 { + variants = append(variants, "go=go") + } + parsed, err := parseVariants(variants, *includeLTO) + if err != nil { + fatal(err) + } + if *runs <= 0 { + fatal(errors.New("-runs must be positive")) + } + if *iters <= 0 { + fatal(errors.New("-iters must be positive")) + } + if *pkgCount <= 0 || *methodCount <= 0 { + fatal(errors.New("-packages and -methods must be positive")) + } + scales, err := parseScales(*scaleList) + if err != nil { + fatal(err) + } + depths, err := parseInts(*depthList) + if err != nil { + fatal(err) + } + bigSizes, err := parseScalePairs(*bigList) + if err != nil { + fatal(err) + } + + absOut, err := filepath.Abs(*outDir) + if err != nil { + fatal(err) + } + if err := os.RemoveAll(absOut); err != nil { + fatal(err) + } + for _, dir := range []string{"work", "bin"} { + if err := os.MkdirAll(filepath.Join(absOut, dir), 0755); err != nil { + fatal(err) + } + } + + scenarios, err := generateScenarios(filepath.Join(absOut, "work"), splitList(*scenarioList), *pkgCount, *methodCount, scales, depths, bigSizes) + if err != nil { + fatal(err) + } + + var builds []buildResult + var runsOut []runResult + for _, sc := range scenarios { + for _, v := range parsed { + br := buildScenario(absOut, sc, v, *llgoOpt) + builds = append(builds, br) + if br.Error != "" { + fmt.Fprintf(os.Stderr, "build failed: %s/%s: %s\n", v.Name, sc.Name, br.Error) + continue + } + rr := runScenario(sc, v, br.Binary, *runs, *iters) + runsOut = append(runsOut, rr) + if rr.Error != "" { + fmt.Fprintf(os.Stderr, "run failed: %s/%s: %s\n", v.Name, sc.Name, rr.Error) + } + } + } + + result := resultFile{ + GeneratedAt: time.Now(), + PackageCount: *pkgCount, + MethodCount: *methodCount, + Variants: parsed, + Scenarios: scenarioNames(scenarios), + ScenarioMeta: scenarios, + Builds: builds, + Runs: runsOut, + } + if err := writeJSON(filepath.Join(absOut, "results.json"), result); err != nil { + fatal(err) + } + summary := renderSummary(result) + if err := os.WriteFile(filepath.Join(absOut, "summary.md"), []byte(summary), 0644); err != nil { + fatal(err) + } + fmt.Print(summary) +} + +func parseVariants(values []string, includeLTO bool) ([]variant, error) { + var out []variant + seen := map[string]bool{} + for _, raw := range values { + name, spec, ok := strings.Cut(raw, "=") + if !ok || name == "" || spec == "" { + return nil, fmt.Errorf("bad -variant %q", raw) + } + if seen[name] { + return nil, fmt.Errorf("duplicate variant %q", name) + } + seen[name] = true + var v variant + v.Name = name + switch { + case spec == "go": + v.Kind = "go" + v.Tool = "go" + case strings.HasPrefix(spec, "go,"): + parts := strings.Split(spec, ",") + if len(parts) != 2 || parts[1] == "" { + return nil, fmt.Errorf("bad go variant %q", raw) + } + v.Kind = "go" + v.Tool = parts[1] + case strings.HasPrefix(spec, "llgo,"): + parts := strings.Split(spec, ",") + if len(parts) != 3 || parts[1] == "" || parts[2] == "" { + return nil, fmt.Errorf("bad llgo variant %q", raw) + } + v.Kind = "llgo" + v.Tool = parts[1] + v.Root = parts[2] + default: + return nil, fmt.Errorf("unknown variant kind in %q", raw) + } + out = append(out, v) + if includeLTO && v.Kind == "llgo" { + lto := v + lto.Name = v.Name + "+lto" + lto.LTO = true + out = append(out, lto) + } + } + return out, nil +} + +func generateScenarios(workDir string, names []string, pkgCount, methodCount int, scales []scenarioSize, depths []int, bigSizes []scenarioSize) ([]scenario, error) { + var out []scenario + for _, name := range names { + sizes := []scenarioSize{{Packages: pkgCount, Methods: methodCount}} + if len(scales) != 0 && (name == "multipkg" || name == "cold") { + sizes = scales + } + if name == "deep" { + sizes = []scenarioSize{{Packages: 32}} + if len(depths) != 0 { + sizes = sizes[:0] + for _, d := range depths { + sizes = append(sizes, scenarioSize{Packages: d}) + } + } + } + if name == "bigfunc" { + sizes = []scenarioSize{{Packages: 32, Methods: 200}} + if len(bigSizes) != 0 { + sizes = bigSizes + } + } + for _, size := range sizes { + scenarioName := name + if len(sizes) > 1 { + scenarioName = fmt.Sprintf("%s_%dx%d", name, size.Packages, size.Methods) + if name == "deep" { + scenarioName = fmt.Sprintf("%s_%d", name, size.Packages) + } + } + dir := filepath.Join(workDir, scenarioName) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + var err error + switch name { + case "hot": + err = generateHot(dir) + case "deep": + err = generateDeep(dir, size.Packages) + case "multipkg": + err = generateMultipkg(dir, size.Packages, size.Methods) + case "cold": + err = generateCold(dir, size.Packages, size.Methods) + case "stdlib": + err = generateStdlib(dir) + case "plain": + err = generatePlain(dir) + case "bigfunc": + err = generateBigfunc(dir, size.Packages, size.Methods) + default: + return nil, fmt.Errorf("unknown scenario %q", name) + } + if err != nil { + return nil, err + } + sc := scenario{Name: scenarioName, Kind: name, Dir: dir} + if name == "multipkg" || name == "cold" || name == "bigfunc" { + sc.PackageCount = size.Packages + sc.MethodCount = size.Methods + sc.TargetCount = size.Packages * size.Methods + sc.Scale = size + } + out = append(out, sc) + } + } + return out, nil +} + +func parseInts(list string) ([]int, error) { + var out []int + for _, tok := range splitList(list) { + n, err := strconv.Atoi(tok) + if err != nil || n <= 0 { + return nil, fmt.Errorf("bad int %q", tok) + } + out = append(out, n) + } + return out, nil +} + +// parseScalePairs parses "AxB,CxD" lists that are not tied to the +// multipkg/cold flag defaults. +func parseScalePairs(list string) ([]scenarioSize, error) { + var out []scenarioSize + for _, tok := range splitList(list) { + a, b, ok := strings.Cut(tok, "x") + if !ok { + return nil, fmt.Errorf("bad size %q", tok) + } + f, err1 := strconv.Atoi(a) + st, err2 := strconv.Atoi(b) + if err1 != nil || err2 != nil || f <= 0 || st <= 0 { + return nil, fmt.Errorf("bad size %q", tok) + } + out = append(out, scenarioSize{Packages: f, Methods: st}) + } + return out, nil +} + +func writeModule(dir, module string) error { + return os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module "+module+"\n\ngo 1.24\n"), 0644) +} + +func generateHot(dir string) error { + if err := writeModule(dir, "example.com/llgo-bench/hot"); err != nil { + return err + } + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(hotSource), 0644) +} + +func generateDeep(dir string, depth int) error { + if err := writeModule(dir, "example.com/llgo-bench/deep"); err != nil { + return err + } + var b strings.Builder + b.WriteString(deepPrefix) + for i := 0; i < depth; i++ { + fmt.Fprintf(&b, "//go:noinline\nfunc frame%d() { frame%d() }\n\n", i, i+1) + } + fmt.Fprintf(&b, `//go:noinline +func frame%d() { + pc, file, line, ok := runtime.Caller(%d) + if !ok || pc == 0 || file == "" || line == 0 { + panic("bad deep caller") + } + sinkPC = pc + sinkString = file + sinkInt += line +} + +`, depth, depth/2) + suffix := strings.ReplaceAll(deepSuffix, "[64]uintptr", fmt.Sprintf("[%d]uintptr", depth+64)) + for _, m := range []string{"Direct", "Interface", "Closure"} { + suffix = strings.ReplaceAll(suffix, "deep."+m+"32", fmt.Sprintf("deep.%s%d", m, depth)) + } + b.WriteString(suffix) + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(b.String()), 0644) +} + +func generateMultipkg(dir string, pkgCount, methodCount int) error { + if err := writeModule(dir, "example.com/llgo-bench/multipkg"); err != nil { + return err + } + for i := 0; i < pkgCount; i++ { + pkgName := fmt.Sprintf("p%02d", i) + pkgDir := filepath.Join(dir, pkgName) + if err := os.MkdirAll(pkgDir, 0755); err != nil { + return err + } + var b strings.Builder + fmt.Fprintf(&b, "package %s\n\n", pkgName) + b.WriteString("import (\n\t\"reflect\"\n\t\"runtime\"\n") + if i+1 < pkgCount { + fmt.Fprintf(&b, "\tnext \"example.com/llgo-bench/multipkg/p%02d\"\n", i+1) + } + b.WriteString(")\n\n") + fmt.Fprintf(&b, "type T%02d struct { V int }\n", i) + b.WriteString("type Worker interface { M00(int) int }\n\n") + for j := 0; j < methodCount; j++ { + fmt.Fprintf(&b, "//go:noinline\nfunc F%02d_%02d(x int) int { return x + %d }\n\n", i, j, i*100+j) + fmt.Fprintf(&b, "//go:noinline\nfunc (t T%02d) M%02d(x int) int { return t.V + x + %d }\n\n", i, j, j) + } + b.WriteString("//go:noinline\nfunc Targets() []uintptr {\n\treturn []uintptr{\n") + for j := 0; j < methodCount; j++ { + fmt.Fprintf(&b, "\t\treflect.ValueOf(F%02d_%02d).Pointer(),\n", i, j) + } + b.WriteString("\t}\n}\n\n") + b.WriteString("//go:noinline\nfunc Run(x int) int {\n") + b.WriteString("\tpc, _, line, ok := runtime.Caller(0)\n\tif !ok || pc == 0 || line == 0 { panic(\"bad caller\") }\n") + fmt.Fprintf(&b, "\tvar w Worker = T%02d{V: x}\n", i) + b.WriteString("\ttotal := w.M00(x)\n") + for j := 0; j < methodCount; j++ { + fmt.Fprintf(&b, "\ttotal += F%02d_%02d(x)\n", i, j) + fmt.Fprintf(&b, "\ttotal += (T%02d{V: total}).M%02d(x)\n", i, j) + } + if i+1 < pkgCount { + b.WriteString("\ttotal += next.Run(x+1)\n") + } + b.WriteString("\treturn total + line\n}\n") + if err := os.WriteFile(filepath.Join(pkgDir, pkgName+".go"), []byte(b.String()), 0644); err != nil { + return err + } + } + + var main strings.Builder + main.WriteString("package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n") + for i := 0; i < pkgCount; i++ { + fmt.Fprintf(&main, "\tp%02d \"example.com/llgo-bench/multipkg/p%02d\"\n", i, i) + } + main.WriteString(")\n\nvar sinkInt int\nvar sinkString string\n\n") + main.WriteString(commonBenchHelpers) + main.WriteString("func main() {\n\titers := benchIters(10000)\n\tvar targets []uintptr\n") + for i := 0; i < pkgCount; i++ { + fmt.Fprintf(&main, "\ttargets = append(targets, p%02d.Targets()...)\n", i) + } + main.WriteString(` + if funcInfoReady(targets) { + measure("multipkg.FuncForPCMany", iters, func() { + total := 0 + for _, pc := range targets { + fn := runtime.FuncForPC(pc) + if fn == nil { + panic("missing func") + } + total += len(fn.Name()) + } + sinkInt += total + }) + measure("multipkg.FileLineMany", iters, func() { + total := 0 + for _, pc := range targets { + fn := runtime.FuncForPC(pc) + if fn == nil { + panic("missing func") + } + file, line := fn.FileLine(pc) + if file == "" || line == 0 { + panic("missing fileline") + } + total += line + len(file) + } + sinkInt += total + }) + } + measure("multipkg.DeepRun", iters, func() { + sinkInt += p00.Run(1) + }) + fmt.Println("sink=", sinkInt, sinkString) +} + +func funcInfoReady(targets []uintptr) bool { + for _, pc := range targets { + fn := runtime.FuncForPC(pc) + if fn == nil { + return false + } + if file, line := fn.FileLine(pc); file == "" || line == 0 { + return false + } + } + return len(targets) != 0 +} +`) + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(main.String()), 0644) +} + +func generateCold(dir string, pkgCount, methodCount int) error { + if err := writeModule(dir, "example.com/llgo-bench/cold"); err != nil { + return err + } + for i := 0; i < pkgCount; i++ { + pkgName := fmt.Sprintf("p%02d", i) + pkgDir := filepath.Join(dir, pkgName) + if err := os.MkdirAll(pkgDir, 0755); err != nil { + return err + } + var b strings.Builder + fmt.Fprintf(&b, "package %s\n\n", pkgName) + b.WriteString("import \"reflect\"\n\n") + for j := 0; j < methodCount; j++ { + fmt.Fprintf(&b, "//go:noinline\nfunc F%02d_%02d(x int) int { return x + %d }\n\n", i, j, i*100+j) + } + b.WriteString("//go:noinline\nfunc Targets() []uintptr {\n\treturn []uintptr{\n") + for j := 0; j < methodCount; j++ { + fmt.Fprintf(&b, "\t\treflect.ValueOf(F%02d_%02d).Pointer(),\n", i, j) + } + b.WriteString("\t}\n}\n") + if err := os.WriteFile(filepath.Join(pkgDir, pkgName+".go"), []byte(b.String()), 0644); err != nil { + return err + } + } + + var main strings.Builder + main.WriteString("package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n") + for i := 0; i < pkgCount; i++ { + fmt.Fprintf(&main, "\tp%02d \"example.com/llgo-bench/cold/p%02d\"\n", i, i) + } + main.WriteString(")\n\nvar sinkInt int\nvar sinkString string\n\n") + main.WriteString(commonBenchHelpers) + main.WriteString("func main() {\n\titers := benchIters(10000)\n\tvar targets []uintptr\n") + for i := 0; i < pkgCount; i++ { + fmt.Fprintf(&main, "\ttargets = append(targets, p%02d.Targets()...)\n", i) + } + main.WriteString(` + if len(targets) == 0 { + panic("missing targets") + } + first := targets[len(targets)/2] + start := time.Now() + fn := runtime.FuncForPC(first) + if fn == nil || fn.Name() == "" { + panic("missing first func") + } + fmt.Printf("cold.FirstFuncForPC=%d\n", time.Since(start).Nanoseconds()) + sinkString = fn.Name() + + start = time.Now() + file, line := fn.FileLine(first) + if file == "" || line == 0 { + panic("missing first fileline") + } + fmt.Printf("cold.FirstFileLine=%d\n", time.Since(start).Nanoseconds()) + sinkString = file + sinkInt += line + + start = time.Now() + pc, file, line, ok := runtime.Caller(0) + if !ok || pc == 0 || file == "" || line == 0 { + panic("bad first caller") + } + fmt.Printf("cold.FirstCaller0=%d\n", time.Since(start).Nanoseconds()) + sinkString = file + sinkInt += line + + start = time.Now() + var pcs [16]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + // Walk to the first fully symbolized frame: synthetic runtime frames + // (e.g. LLGo's runtime.Callers placeholder) carry no file/line. + for { + frame, more := frames.Next() + if frame.Function != "" && frame.File != "" && frame.Line != 0 { + fmt.Printf("cold.FirstCallersFrames=%d\n", time.Since(start).Nanoseconds()) + sinkString = frame.Function + sinkInt += frame.Line + break + } + if !more { + break + } + } + + measure("cold.WarmFuncForPCMany", iters, func() { + total := 0 + for _, pc := range targets { + fn := runtime.FuncForPC(pc) + if fn == nil { + panic("missing func") + } + total += len(fn.Name()) + } + sinkInt += total + }) + measure("cold.WarmFileLineMany", iters, func() { + total := 0 + for _, pc := range targets { + fn := runtime.FuncForPC(pc) + if fn == nil { + panic("missing func") + } + file, line := fn.FileLine(pc) + if file == "" || line == 0 { + panic("missing fileline") + } + total += len(file) + line + } + sinkInt += total + }) + fmt.Println("sink=", sinkInt, sinkString) +} +`) + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(main.String()), 0644) +} + +func generateStdlib(dir string) error { + if err := writeModule(dir, "example.com/llgo-bench/stdlib"); err != nil { + return err + } + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(stdlibSource), 0644) +} + +// generateBigfunc emits `funcs` functions of `stmts` call-site statements +// each: large bodies stress statement-level pcline density (many sites per +// findfunctab bucket), mid-function pc symbolization, first-use pcline table +// construction at scale, and ordinary-code performance of big method bodies. +func generateBigfunc(dir string, funcs, stmts int) error { + if err := writeModule(dir, "example.com/llgo-bench/bigfunc"); err != nil { + return err + } + var b strings.Builder + b.WriteString(bigfuncPrefix) + for i := 0; i < funcs; i++ { + fmt.Fprintf(&b, "//go:noinline\nfunc big%03d(x int) int {\n", i) + for j := 0; j < stmts; j++ { + b.WriteString("\tx = leaf(x)\n") + } + b.WriteString("\tif captureBig {\n\t\tpc, _, line, ok := runtime.Caller(0)\n\t\tif !ok || line == 0 {\n\t\t\tpanic(\"bad big caller\")\n\t\t}\n\t\tbigPCs = append(bigPCs, pc)\n\t}\n") + b.WriteString("\treturn x\n}\n\n") + } + b.WriteString("//go:noinline\nfunc runAll(x int) int {\n") + for i := 0; i < funcs; i++ { + fmt.Fprintf(&b, "\tx = big%03d(x)\n", i) + } + b.WriteString("\treturn x\n}\n\n") + b.WriteString(bigfuncMain) + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(b.String()), 0644) +} + +func generatePlain(dir string) error { + if err := writeModule(dir, "example.com/llgo-bench/plain"); err != nil { + return err + } + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(plainSource), 0644) +} + +func buildScenario(outDir string, sc scenario, v variant, llgoOpt string) buildResult { + bin := filepath.Join(outDir, "bin", safeName(v.Name)+"_"+sc.Name) + if v.LTO { + bin += "_lto" + } + if exeSuffix := executableSuffix(); exeSuffix != "" { + bin += exeSuffix + } + start := time.Now() + var cmd *exec.Cmd + switch v.Kind { + case "go": + cmd = exec.Command(v.Tool, "build", "-trimpath", "-o", bin, ".") + case "llgo": + args := []string{"build", "-trimpath", "-a", "-o", bin} + if llgoOpt != "" { + args = append(args, "-O"+llgoOpt) + } + if v.LTO { + args = append(args, "-lto=full") + } + args = append(args, ".") + cmd = exec.Command(v.Tool, args...) + default: + return buildResult{Variant: v.Name, Scenario: sc.Name, Binary: bin, Error: "unknown variant kind"} + } + cmd.Dir = sc.Dir + cmd.Env = os.Environ() + if v.Kind == "llgo" { + cmd.Env = append(cmd.Env, "LLGO_ROOT="+v.Root, "LLGO_FUNCINFO=1") + } + out, err := cmd.CombinedOutput() + br := buildResult{Variant: v.Name, Scenario: sc.Name, Binary: bin, BuildMS: time.Since(start).Milliseconds()} + if err != nil { + br.Error = strings.TrimSpace(string(out)) + if br.Error == "" { + br.Error = err.Error() + } + return br + } + info, err := os.Stat(bin) + if err != nil { + br.Error = err.Error() + return br + } + br.Size = info.Size() + return br +} + +func runScenario(sc scenario, v variant, bin string, runs, iters int) runResult { + scenarioIters := iterationsForScenario(sc.Kind, iters) + rr := runResult{ + Variant: v.Name, + Scenario: sc.Name, + Metrics: map[string][]int64{}, + Env: map[string]string{ + "BENCH_ITERS": strconv.Itoa(scenarioIters), + }, + } + for i := 0; i < runs; i++ { + cmd := exec.Command(bin) + cmd.Dir = sc.Dir + cmd.Env = append(os.Environ(), "BENCH_ITERS="+strconv.Itoa(scenarioIters)) + out, err := cmd.CombinedOutput() + if err != nil { + rr.Error = err.Error() + rr.Output = string(out) + return rr + } + metrics, err := parseMetrics(out) + if err != nil { + rr.Error = err.Error() + rr.Output = string(out) + return rr + } + for k, v := range metrics { + rr.Metrics[k] = append(rr.Metrics[k], v) + } + } + return rr +} + +func iterationsForScenario(name string, base int) int { + div := 1 + switch name { + case "deep": + div = 4 + case "multipkg", "cold", "stdlib": + div = 20 + case "plain": + div = 2000 + case "bigfunc": + div = 100 + } + n := base / div + if n < 1 { + return 1 + } + return n +} + +func parseMetrics(out []byte) (map[string]int64, error) { + metrics := map[string]int64{} + for _, raw := range strings.Split(string(out), "\n") { + line := strings.TrimSpace(raw) + if line == "" || !strings.Contains(line, "=") { + continue + } + name, value, _ := strings.Cut(line, "=") + if !strings.Contains(name, ".") { + continue + } + n, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64) + if err != nil { + return nil, fmt.Errorf("parse metric %q: %w", line, err) + } + metrics[strings.TrimSpace(name)] = n + } + return metrics, nil +} + +func renderSummary(result resultFile) string { + var b strings.Builder + fmt.Fprintf(&b, "# Runtime Funcinfo Benchmark\n\nGenerated: `%s`\n\n", result.GeneratedAt.Format(time.RFC3339)) + b.WriteString("Cells are `best/trimmed avg`. Runtime metrics use `ns/op`; sizes use MiB.\n\n") + for _, sc := range result.ScenarioMeta { + if sc.TargetCount == 0 { + continue + } + switch sc.Kind { + case "multipkg": + fmt.Fprintf(&b, "`%s` uses `multipkg.FuncForPCMany` and `multipkg.FileLineMany` batch metrics over %d target functions (%d packages x %d functions).\n\n", + sc.Name, sc.TargetCount, sc.PackageCount, sc.MethodCount) + case "cold": + fmt.Fprintf(&b, "`%s` uses `cold.WarmFuncForPCMany` and `cold.WarmFileLineMany` batch metrics over %d target functions (%d packages x %d functions). `cold.First*` metrics are one per process and include lazy runtime initialization that has not already happened in that process.\n\n", + sc.Name, sc.TargetCount, sc.PackageCount, sc.MethodCount) + } + } + for _, sc := range result.Scenarios { + metrics := metricsForScenario(result.Runs, sc) + if len(metrics) == 0 { + continue + } + fmt.Fprintf(&b, "## %s Performance\n\n", sc) + b.WriteString("| metric |") + for _, v := range result.Variants { + b.WriteString(" " + v.Name + " |") + } + b.WriteString("\n|---|") + for range result.Variants { + b.WriteString("---:|") + } + b.WriteString("\n") + for _, metric := range metrics { + b.WriteString("| " + metric + " |") + for _, v := range result.Variants { + rr, found := findRun(result.Runs, v.Name, sc) + cell := "FAIL" + if found && rr.Error == "" { + cell = "n/a" + } + if vals := rr.Metrics[metric]; len(vals) != 0 { + cell = formatPerf(vals) + } + b.WriteString(" " + cell + " |") + } + b.WriteString("\n") + } + b.WriteString("\n") + } + b.WriteString("## Binary Size\n\n| scenario |") + for _, v := range result.Variants { + b.WriteString(" " + v.Name + " |") + } + b.WriteString("\n|---|") + for range result.Variants { + b.WriteString("---:|") + } + b.WriteString("\n") + for _, sc := range result.Scenarios { + b.WriteString("| " + sc + " |") + for _, v := range result.Variants { + cell := "FAIL" + if br := findBuild(result.Builds, v.Name, sc); br.Error == "" && br.Size > 0 { + cell = formatMiB(br.Size) + } + b.WriteString(" " + cell + " |") + } + b.WriteString("\n") + } + b.WriteString("\n## Build Time\n\n| scenario |") + for _, v := range result.Variants { + b.WriteString(" " + v.Name + " |") + } + b.WriteString("\n|---|") + for range result.Variants { + b.WriteString("---:|") + } + b.WriteString("\n") + for _, sc := range result.Scenarios { + b.WriteString("| " + sc + " |") + for _, v := range result.Variants { + cell := "FAIL" + if br := findBuild(result.Builds, v.Name, sc); br.Error == "" { + cell = formatDurationMS(br.BuildMS) + } + b.WriteString(" " + cell + " |") + } + b.WriteString("\n") + } + return b.String() +} + +func metricsForScenario(runs []runResult, scenario string) []string { + set := map[string]bool{} + for _, rr := range runs { + if rr.Scenario != scenario || rr.Error != "" { + continue + } + for k := range rr.Metrics { + set[k] = true + } + } + out := make([]string, 0, len(set)) + for k := range set { + out = append(out, k) + } + sort.Strings(out) + return out +} + +func findRun(runs []runResult, variant, scenario string) (runResult, bool) { + for _, rr := range runs { + if rr.Variant == variant && rr.Scenario == scenario { + return rr, true + } + } + return runResult{Metrics: map[string][]int64{}}, false +} + +func findBuild(builds []buildResult, variant, scenario string) buildResult { + for _, br := range builds { + if br.Variant == variant && br.Scenario == scenario { + return br + } + } + return buildResult{Error: "missing"} +} + +func formatPerf(values []int64) string { + if len(values) == 0 { + return "n/a" + } + sorted := append([]int64(nil), values...) + sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) + best := sorted[0] + avgVals := sorted + if len(sorted) >= 3 { + avgVals = sorted[1 : len(sorted)-1] + } + var sum int64 + for _, v := range avgVals { + sum += v + } + avg := float64(sum) / float64(len(avgVals)) + return formatNS(float64(best)) + "/" + formatNS(avg) +} + +func formatNS(ns float64) string { + switch { + case ns >= 1e6: + return trimFloat(ns/1e6) + "ms" + case ns >= 1e3: + return trimFloat(ns/1e3) + "us" + default: + return trimFloat(ns) + "ns" + } +} + +func formatMiB(bytes int64) string { + return trimFloat(float64(bytes)/(1024*1024)) + " MiB" +} + +func formatDurationMS(ms int64) string { + if ms >= 1000 { + return trimFloat(float64(ms)/1000) + "s" + } + return strconv.FormatInt(ms, 10) + "ms" +} + +func trimFloat(v float64) string { + if math.Abs(v-math.Round(v)) < 0.05 { + return strconv.FormatInt(int64(math.Round(v)), 10) + } + return strconv.FormatFloat(v, 'f', 1, 64) +} + +func writeJSON(path string, data any) error { + raw, err := json.MarshalIndent(data, "", " ") + if err != nil { + return err + } + raw = append(raw, '\n') + return os.WriteFile(path, raw, 0644) +} + +func splitList(s string) []string { + var out []string + for _, part := range strings.Split(s, ",") { + part = strings.TrimSpace(part) + if part != "" { + out = append(out, part) + } + } + return out +} + +func parseScales(s string) ([]scenarioSize, error) { + var out []scenarioSize + for _, part := range splitList(s) { + left, right, ok := strings.Cut(part, "x") + if !ok { + left, right, ok = strings.Cut(part, "X") + } + if !ok { + return nil, fmt.Errorf("bad scale %q: want packages x methods, for example 12x12", part) + } + packages, err := strconv.Atoi(strings.TrimSpace(left)) + if err != nil || packages <= 0 { + return nil, fmt.Errorf("bad package count in scale %q", part) + } + methods, err := strconv.Atoi(strings.TrimSpace(right)) + if err != nil || methods <= 0 { + return nil, fmt.Errorf("bad method count in scale %q", part) + } + out = append(out, scenarioSize{Packages: packages, Methods: methods}) + } + return out, nil +} + +func scenarioNames(scenarios []scenario) []string { + out := make([]string, len(scenarios)) + for i, sc := range scenarios { + out[i] = sc.Name + } + return out +} + +func safeName(s string) string { + replacer := strings.NewReplacer("/", "_", "\\", "_", ":", "_", "+", "_") + return replacer.Replace(s) +} + +func executableSuffix() string { + if os.PathSeparator == '\\' { + return ".exe" + } + return "" +} + +func fatal(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} + +const commonBenchHelpers = ` +func benchIters(def int) int { + if s := getenv("BENCH_ITERS"); s != "" { + n, err := atoi(s) + if err == nil && n > 0 { + return n + } + } + return def +} + +func measure(name string, n int, fn func()) { + fn() + start := time.Now() + for i := 0; i < n; i++ { + fn() + } + elapsed := time.Since(start).Nanoseconds() + if n <= 0 { + panic("bad iterations") + } + fmt.Printf("%s=%d\n", name, elapsed/int64(n)) +} + +func getenv(k string) string { + for _, kv := range os.Environ() { + if len(kv) > len(k) && kv[:len(k)] == k && kv[len(k)] == '=' { + return kv[len(k)+1:] + } + } + return "" +} + +func atoi(s string) (int, error) { + n := 0 + for _, r := range s { + if r < '0' || r > '9' { + return 0, fmt.Errorf("bad int") + } + n = n*10 + int(r-'0') + } + return n, nil +} +` + +const hotSource = `package main + +import ( + "fmt" + "os" + "reflect" + "runtime" + "time" +) + +var sinkInt int +var sinkPC uintptr +var sinkString string + +` + commonBenchHelpers + ` + +//go:noinline +func entryTarget(x int) int { + return x + 7 +} + +//go:noinline +func caller0() { + pc, file, line, ok := runtime.Caller(0) + if !ok || pc == 0 || file == "" || line == 0 { + panic("bad caller0") + } + sinkPC = pc + sinkString = file + sinkInt += line +} + +//go:noinline +func caller1() { + caller1Helper() +} + +//go:noinline +func caller1Helper() { + pc, file, line, ok := runtime.Caller(1) + if !ok || pc == 0 || file == "" || line == 0 { + panic("bad caller1") + } + sinkPC = pc + sinkString = file + sinkInt += line +} + +//go:noinline +func returnPC() uintptr { + pc, _, _, ok := runtime.Caller(0) + if !ok || pc == 0 { + panic("bad return pc") + } + return pc +} + +//go:noinline +func callersOnly() { + var pcs [16]uintptr + n := runtime.Callers(0, pcs[:]) + if n == 0 || pcs[0] == 0 { + panic("bad callers") + } + sinkPC = pcs[0] + sinkInt += n +} + +//go:noinline +func callersFramesFirst() { + var pcs [16]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function != "" && frame.File != "" && frame.Line != 0 { + sinkString = frame.Function + sinkInt += frame.Line + return + } + if !more { + break + } + } + panic("bad frame") +} + +func callersFramesReady() (ok bool) { + defer func() { + if recover() != nil { + ok = false + } + }() + callersFramesFirst() + return true +} + +func main() { + iters := benchIters(200000) + entryPC := reflect.ValueOf(entryTarget).Pointer() + returnedPC := returnPC() + measure("hot.Caller0", iters, caller0) + measure("hot.Caller1", iters, caller1) + measure("hot.CallersOnly", iters, callersOnly) + if callersFramesReady() { + measure("hot.CallersFramesFirst", iters, callersFramesFirst) + } + if entryFn := runtime.FuncForPC(entryPC); entryFn != nil && entryFn.Name() != "" { + measure("hot.FuncForPCEntry", iters, func() { + fn := runtime.FuncForPC(entryPC) + if fn == nil { + panic("missing entry func") + } + sinkString = fn.Name() + }) + if file, line := entryFn.FileLine(entryPC); file != "" && line != 0 { + measure("hot.FuncFileLineEntry", iters, func() { + file, line := entryFn.FileLine(entryPC) + if file == "" || line == 0 { + panic("missing entry fileline") + } + sinkString = file + sinkInt += line + }) + } + } + if returnFn := runtime.FuncForPC(returnedPC); returnFn != nil && returnFn.Name() != "" { + measure("hot.FuncForPCReturnPC", iters, func() { + fn := runtime.FuncForPC(returnedPC) + if fn == nil { + panic("missing return func") + } + sinkString = fn.Name() + }) + if file, line := returnFn.FileLine(returnedPC); file != "" && line != 0 { + measure("hot.FuncFileLineReturnPC", iters, func() { + file, line := returnFn.FileLine(returnedPC) + if file == "" || line == 0 { + panic("missing return fileline") + } + sinkString = file + sinkInt += line + }) + } + } + fmt.Println("sink=", sinkInt, sinkPC, sinkString) +} +` + +const deepPrefix = `package main + +import ( + "fmt" + "os" + "runtime" + "time" +) + +var sinkInt int +var sinkPC uintptr +var sinkString string + +` + commonBenchHelpers + ` + +type callerIface interface { + call() +} + +type callerImpl struct{} + +//go:noinline +func (callerImpl) call() { + frame0() +} + +//go:noinline +func closureLayer(next func()) func() { + return func() { + next() + } +} + +//go:noinline +func callInterface(c callerIface) { + c.call() +} + +//go:noinline +func callClosure() { + closureLayer(closureLayer(frame0))() +} + +` + +const deepSuffix = `//go:noinline +func framesAll() { + frame0() + var pcs [64]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + total := 0 + for { + frame, more := frames.Next() + if frame.Function != "" { + total += len(frame.Function) + frame.Line + } + if !more { + break + } + } + if total == 0 { + panic("bad frames") + } + sinkInt += total +} + +func deepReady(fn func()) (ok bool) { + defer func() { + if recover() != nil { + ok = false + } + }() + fn() + return true +} + +func main() { + iters := benchIters(50000) + if deepReady(frame0) { + measure("deep.Direct32", iters, frame0) + } + if deepReady(func() { callInterface(callerImpl{}) }) { + measure("deep.Interface32", iters, func() { callInterface(callerImpl{}) }) + } + if deepReady(callClosure) { + measure("deep.Closure32", iters, callClosure) + } + if deepReady(framesAll) { + measure("deep.CallersFramesAll", iters, framesAll) + } + fmt.Println("sink=", sinkInt, sinkPC, sinkString) +} +` + +const stdlibSource = `package main + +import ( + "bytes" + "encoding/json" + "fmt" + "go/parser" + "go/token" + "net/netip" + "os" + "reflect" + "regexp" + "runtime" + "strings" + "text/template" + "time" +) + +var sinkInt int +var sinkString string + +` + commonBenchHelpers + ` + +type payload struct { + Name string + Items []int + Addr string +} + +//go:noinline +func stdTarget(x int) int { + return x*3 + 1 +} + +//go:noinline +func stdWork() { + p := payload{Name: "llgo", Items: []int{1, 2, 3, 5, 8}, Addr: "127.0.0.1:8080"} + raw, err := json.Marshal(p) + if err != nil { + panic(err) + } + var out payload + if err := json.Unmarshal(raw, &out); err != nil { + panic(err) + } + tmpl := template.Must(template.New("x").Funcs(template.FuncMap{"join": strings.Join}).Parse("{{.Name}}:{{join .Words \",\"}}")) + var buf bytes.Buffer + if err := tmpl.Execute(&buf, map[string]any{"Name": out.Name, "Words": []string{"a", "b", "c"}}); err != nil { + panic(err) + } + re := regexp.MustCompile("[a-z]+") + matches := re.FindAllString(buf.String(), -1) + expr, err := parser.ParseExpr("1 + 2*3") + if err != nil || expr == nil { + panic("bad parser") + } + fs := token.NewFileSet() + file := fs.AddFile("bench.go", -1, 100) + file.AddLine(10) + addr := netip.MustParseAddrPort(out.Addr) + sinkInt += len(matches) + int(addr.Port()) + int(file.Line(token.Pos(11))) + sinkString = buf.String() +} + +//go:noinline +func stdCaller() { + pc, file, line, ok := runtime.Caller(0) + if !ok || pc == 0 || file == "" || line == 0 { + panic("bad caller") + } + sinkInt += line + sinkString = file +} + +//go:noinline +func stdFrames() { + var pcs [16]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function != "" && frame.File != "" && frame.Line != 0 { + sinkInt += frame.Line + sinkString = frame.Function + return + } + if !more { + break + } + } + panic("bad frame") +} + +func stdFramesReady() (ok bool) { + defer func() { + if recover() != nil { + ok = false + } + }() + stdFrames() + return true +} + +func main() { + iters := benchIters(50000) + entryPC := reflect.ValueOf(stdTarget).Pointer() + measure("stdlib.Work", iters/10, stdWork) + measure("stdlib.Caller0", iters, stdCaller) + if stdFramesReady() { + measure("stdlib.CallersFramesFirst", iters, stdFrames) + } + if fn := runtime.FuncForPC(entryPC); fn != nil && fn.Name() != "" { + measure("stdlib.FuncForPCEntry", iters, func() { + fn := runtime.FuncForPC(entryPC) + if fn == nil { + panic("missing func") + } + sinkString = fn.Name() + }) + if file, line := fn.FileLine(entryPC); file != "" && line != 0 { + measure("stdlib.FuncFileLineEntry", iters, func() { + file, line := fn.FileLine(entryPC) + if file == "" || line == 0 { + panic("missing fileline") + } + sinkInt += line + sinkString = file + }) + } + } + fmt.Println("sink=", sinkInt, sinkString) +} +` + +// plainSource is the ordinary-code scenario: pure compute with no runtime +// introspection at all. It exists to measure what the funcinfo machinery +// costs code that never asks for it (site-asm inline/layout perturbation is +// the only expected effect; the tables themselves are free until first use). +const plainSource = `package main + +import ( + "encoding/json" + "fmt" + "os" + "sort" + "time" +) + +var sinkInt int + +` + commonBenchHelpers + ` + +//go:noinline +func fib(n int) int { + if n < 2 { + return n + } + return fib(n-1) + fib(n-2) +} + +type item struct { + Name string + Value int + Tags []string +} + +func main() { + iters := benchIters(5) + + // Read the depth from the environment so full LTO cannot constant-fold + // the whole call away. + fibN := 30 + if s := getenv("PLAIN_FIB_N"); s != "" { + if n, err := atoi(s); err == nil && n > 0 { + fibN = n + } + } + measure("plain.fib30", iters, func() { + sinkInt += fib(fibN) + }) + + items := make([]item, 2000) + for i := range items { + items[i] = item{Name: fmt.Sprintf("item-%d", i), Value: i * 7, Tags: []string{"a", "b", "c"}} + } + measure("plain.json", iters, func() { + b, err := json.Marshal(items) + if err != nil { + panic(err) + } + var out []item + if err := json.Unmarshal(b, &out); err != nil { + panic(err) + } + sinkInt += len(out) + }) + + measure("plain.sort", iters, func() { + data := make([]int, 200000) + for i := range data { + data[i] = (i*2654435761 + 12345) % 1000003 + } + sort.Ints(data) + sinkInt += data[0] + }) + + measure("plain.map", iters, func() { + m := make(map[int]int, 16) + for i := 0; i < 200000; i++ { + m[(i*2654435761)%100003]++ + } + sinkInt += len(m) + }) + + if sinkInt == 0 { + os.Exit(1) + } +} +` + +const bigfuncPrefix = `package main + +import ( + "fmt" + "os" + "runtime" + "time" +) + +var sinkInt int +var captureBig bool +var bigPCs []uintptr + +` + commonBenchHelpers + ` + +//go:noinline +func leaf(x int) int { return x + 1 } + +` + +const bigfuncMain = `func main() { + iters := benchIters(100) + + // Capture one tail-of-body pc per big function (a statement-level pc + // deep inside a large body, not a function entry). + captureBig = true + sinkInt += runAll(1) + captureBig = false + if len(bigPCs) == 0 { + panic("no big pcs") + } + + // First statement-level FileLine in this process: includes any lazy + // line-table work over funcs*stmts call sites. + t := time.Now() + fn := runtime.FuncForPC(bigPCs[0]) + if fn == nil { + panic("no func") + } + file, line := fn.FileLine(bigPCs[0]) + if file == "" || line == 0 { + panic("bad first fileline") + } + fmt.Printf("bigfunc.FirstFileLineMid=%d\n", time.Since(t).Nanoseconds()) + + measure("bigfunc.FuncForPCMid", iters*100, func() { + for _, pc := range bigPCs { + if runtime.FuncForPC(pc) == nil { + panic("no func") + } + } + }) + + measure("bigfunc.FileLineMid", iters*100, func() { + for _, pc := range bigPCs { + f := runtime.FuncForPC(pc) + if f == nil { + panic("no func") + } + _, l := f.FileLine(pc) + sinkInt += l + } + }) + + measure("bigfunc.CallersFramesMid", iters*10, func() { + var pcs [16]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + sinkInt += frame.Line + if !more { + break + } + } + }) + + // Ordinary performance of the large bodies themselves. + measure("bigfunc.Work", iters, func() { + sinkInt += runAll(1) + }) + + if sinkInt == 0 { + os.Exit(1) + } +} +` diff --git a/chore/pclnpost/main.go b/chore/pclnpost/main.go new file mode 100644 index 0000000000..22a7efe2c5 --- /dev/null +++ b/chore/pclnpost/main.go @@ -0,0 +1,43 @@ +/* + * 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. + */ + +// Command pclnpost rewrites a linked LLGo binary's funcinfo entry section +// with the link-phase prebuilt ftab/findfunctab (see +// doc/design/pclntab-linkphase.md and internal/pclnpost). llgo build runs +// the same rewrite automatically; this command exists for manual inspection +// and re-processing. +package main + +import ( + "fmt" + "os" + + "github.com/goplus/llgo/internal/pclnpost" +) + +func main() { + if len(os.Args) != 2 { + fmt.Fprintln(os.Stderr, "usage: pclnpost ") + os.Exit(2) + } + st, err := pclnpost.Rewrite(os.Args[1]) + if err != nil { + fmt.Fprintln(os.Stderr, "pclnpost:", err) + os.Exit(1) + } + fmt.Printf("%s: entry=%d stub=%d kept=%d inlineCopies=%d noSymbol=%d -> ftab=%d buckets=%d\n", + st.Format, st.EntryRecords, st.StubRecords, st.Kept, st.InlineCopies, st.NoSymbol, st.FtabEntries, st.Buckets) +} diff --git a/cl/_testdata/vargs/in.go b/cl/_testdata/vargs/in.go index 27eef21e99..7301ed3194 100644 --- a/cl/_testdata/vargs/in.go +++ b/cl/_testdata/vargs/in.go @@ -3,7 +3,6 @@ package main import "github.com/goplus/lib/c" -// CHECK: @0 = private unnamed_addr constant [3 x i8] c"int", align 1 // CHECK: @1 = private unnamed_addr constant [4 x i8] c"%d\0A\00", align 1 func main() { @@ -88,7 +87,8 @@ func test(a ...any) { // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %12, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %17 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %12, ptr @_llgo_int, ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %17) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -97,3 +97,9 @@ func test(a ...any) { // CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.memequal64"(ptr %1, ptr %2) // CHECK-NEXT: ret i1 %3 // CHECK-NEXT: } + +// CHECK-LABEL: define linkonce i1 @"__llgo_stub.{{.*}}/runtime/internal/runtime.nilinterequal"(ptr %0, ptr %1, ptr %2){{.*}} { +// CHECK-NEXT: _llgo_0: +// CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.nilinterequal"(ptr %1, ptr %2) +// CHECK-NEXT: ret i1 %3 +// CHECK-NEXT: } diff --git a/cl/_testgo/abimethod/in.go b/cl/_testgo/abimethod/in.go index c6a271eee9..f0653b936f 100644 --- a/cl/_testgo/abimethod/in.go +++ b/cl/_testgo/abimethod/in.go @@ -10,7 +10,6 @@ import ( // CHECK: {{^}}@0 = private unnamed_addr constant [45 x i8] c"{{.*}}/cl/_testgo/abimethod.T", align 1{{$}} // CHECK: {{^}}@1 = private unnamed_addr constant [5 x i8] c"Demo1", align 1{{$}} -// CHECK: {{^}}@5 = private unnamed_addr constant [3 x i8] c"int", align 1{{$}} // CHECK: {{^}}@14 = private unnamed_addr constant [20 x i8] c"testAnonymous1 error", align 1{{$}} // CHECK: {{^}}@16 = private unnamed_addr constant [20 x i8] c"testAnonymous2 error", align 1{{$}} // CHECK: {{^}}@18 = private unnamed_addr constant [20 x i8] c"testAnonymous3 error", align 1{{$}} @@ -734,7 +733,8 @@ type I2 interface { // CHECK-NEXT: br i1 %29, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %23, %"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %30 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %23, ptr @_llgo_int, ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %30) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/cgodefer/cgodefer.go b/cl/_testgo/cgodefer/cgodefer.go index 7415613677..7acf9ed437 100644 --- a/cl/_testgo/cgodefer/cgodefer.go +++ b/cl/_testgo/cgodefer/cgodefer.go @@ -6,6 +6,11 @@ package main */ import "C" +func main() { + p := C.malloc(1024) + defer C.free(p) +} + // CHECK-LABEL: define [0 x i8] @"{{.*}}/cl/_testgo/cgodefer._Cfunc_free"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %1 = call ptr @"{{.*}}/runtime/internal/runtime.AllocZ"(i64 8) @@ -15,6 +20,27 @@ import "C" // CHECK-NEXT: ret [0 x i8] %4 // CHECK-NEXT: } +// CHECK-LABEL: define ptr @"{{.*}}/cl/_testgo/cgodefer._Cgo_ptr"(ptr %0){{.*}} { +// CHECK-NEXT: _llgo_0: +// CHECK-NEXT: ret ptr %0 +// CHECK-NEXT: } + +// CHECK-LABEL: define void @"{{.*}}/cl/_testgo/cgodefer.init"(){{.*}} { +// CHECK-NEXT: _llgo_0: +// CHECK-NEXT: %0 = load i1, ptr @"{{.*}}/cl/_testgo/cgodefer.init$guard", align 1 +// CHECK-NEXT: br i1 %0, label %_llgo_2, label %_llgo_1 +// CHECK-EMPTY: +// CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 +// CHECK-NEXT: store i1 true, ptr @"{{.*}}/cl/_testgo/cgodefer.init$guard", align 1 +// CHECK-NEXT: call void @syscall.init() +// CHECK-NEXT: store ptr @_cgo_{{.*}}_Cfunc_free, ptr @"{{.*}}/cl/_testgo/cgodefer._cgo_{{.*}}_Cfunc_free", align 8 +// CHECK-NEXT: store ptr @_cgo_{{.*}}_Cfunc__Cmalloc, ptr @"{{.*}}/cl/_testgo/cgodefer._cgo_{{.*}}_Cfunc__Cmalloc", align 8 +// CHECK-NEXT: br label %_llgo_2 +// CHECK-EMPTY: +// CHECK-NEXT: _llgo_2: ; preds = %_llgo_1, %_llgo_0 +// CHECK-NEXT: ret void +// CHECK-NEXT: } + // CHECK-LABEL: define void @"{{.*}}/cl/_testgo/cgodefer.main"(){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %0 = call ptr @"{{.*}}/runtime/internal/runtime.AllocZ"(i64 8) @@ -29,104 +55,112 @@ import "C" // CHECK-NEXT: %7 = call { ptr, ptr } %6(ptr %5) // CHECK-NEXT: %8 = call ptr @"{{.*}}/runtime/internal/runtime.GetThreadDefer"() // CHECK-NEXT: %9 = alloca i8, i64 {{.*}}, align 1 -// CHECK-NEXT: %10 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 48) -// CHECK-NEXT: %11 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %10, i32 0, i32 0 -// CHECK-NEXT: store ptr %9, ptr %11, align 8 -// CHECK-NEXT: %12 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %10, i32 0, i32 1 -// CHECK-NEXT: store i64 0, ptr %12, align 8 -// CHECK-NEXT: %13 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %10, i32 0, i32 2 -// CHECK-NEXT: store ptr %8, ptr %13, align 8 -// CHECK-NEXT: %14 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %10, i32 0, i32 3 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgodefer.main", %_llgo_2), ptr %14, align 8 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %10) -// CHECK-NEXT: %15 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %10, i32 0, i32 1 -// CHECK-NEXT: %16 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %10, i32 0, i32 3 -// CHECK-NEXT: %17 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %10, i32 0, i32 4 -// CHECK-NEXT: %18 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %10, i32 0, i32 5 -// CHECK-NEXT: store ptr null, ptr %18, align 8 -// CHECK-NEXT: %19 = call i32 @{{.*}}sigsetjmp(ptr %9, i32 0) -// CHECK-NEXT: %20 = icmp eq i32 %19, 0 -// CHECK-NEXT: br i1 %20, label %_llgo_4, label %_llgo_5 +// CHECK-NEXT: %10 = call ptr @llvm.frameaddress.p0(i32 0) +// CHECK-NEXT: %11 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 56) +// CHECK-NEXT: %12 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %11, i32 0, i32 0 +// CHECK-NEXT: store ptr %9, ptr %12, align 8 +// CHECK-NEXT: %13 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %11, i32 0, i32 1 +// CHECK-NEXT: store i64 0, ptr %13, align 8 +// CHECK-NEXT: %14 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %11, i32 0, i32 2 +// CHECK-NEXT: store ptr %8, ptr %14, align 8 +// CHECK-NEXT: %15 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %11, i32 0, i32 3 +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgodefer.main", %_llgo_2), ptr %15, align 8 +// CHECK-NEXT: %16 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %11, i32 0, i32 4 +// CHECK-NEXT: store ptr null, ptr %16, align 8 +// CHECK-NEXT: %17 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %11, i32 0, i32 5 +// CHECK-NEXT: store ptr null, ptr %17, align 8 +// CHECK-NEXT: %18 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %11, i32 0, i32 6 +// CHECK-NEXT: store ptr %10, ptr %18, align 8 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %11) +// CHECK-NEXT: %19 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %11, i32 0, i32 1 +// CHECK-NEXT: %20 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %11, i32 0, i32 3 +// CHECK-NEXT: %21 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %11, i32 0, i32 4 +// CHECK-NEXT: %22 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %11, i32 0, i32 5 +// CHECK-NEXT: store ptr null, ptr %22, align 8 +// CHECK-NEXT: %23 = call i32 @{{.*}}sigsetjmp(ptr %9, i32 0) +// CHECK-NEXT: %24 = icmp eq i32 %23, 0 +// CHECK-NEXT: br i1 %24, label %_llgo_4, label %_llgo_5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_3 // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_5, %_llgo_4 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgodefer.main", %_llgo_3), ptr %16, align 8 -// CHECK-NEXT: %21 = load i64, ptr %15, align 8 -// CHECK-NEXT: %22 = load ptr, ptr %18, align 8 -// CHECK-NEXT: %23 = icmp ne ptr %22, null -// CHECK-NEXT: br i1 %23, label %_llgo_7, label %_llgo_8 +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgodefer.main", %_llgo_3), ptr %20, align 8 +// CHECK-NEXT: %25 = load i64, ptr %19, align 8 +// CHECK-NEXT: %26 = load ptr, ptr %22, align 8 +// CHECK-NEXT: %27 = icmp ne ptr %26, null +// CHECK-NEXT: br i1 %27, label %_llgo_7, label %_llgo_8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_5, %_llgo_8 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Rethrow"(ptr %8) // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_0 -// CHECK-NEXT: %24 = load ptr, ptr %18, align 8 -// CHECK-NEXT: %25 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 32) -// CHECK-NEXT: %26 = getelementptr inbounds { ptr, i64, { ptr, ptr } }, ptr %25, i32 0, i32 0 -// CHECK-NEXT: store ptr %24, ptr %26, align 8 -// CHECK-NEXT: %27 = getelementptr inbounds { ptr, i64, { ptr, ptr } }, ptr %25, i32 0, i32 1 -// CHECK-NEXT: store i64 0, ptr %27, align 8 -// CHECK-NEXT: %28 = getelementptr inbounds { ptr, i64, { ptr, ptr } }, ptr %25, i32 0, i32 2 -// CHECK-NEXT: store { ptr, ptr } %7, ptr %28, align 8 -// CHECK-NEXT: store ptr %25, ptr %18, align 8 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgodefer.main", %_llgo_6), ptr %17, align 8 +// CHECK-NEXT: %28 = load ptr, ptr %22, align 8 +// CHECK-NEXT: %29 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 32) +// CHECK-NEXT: %30 = getelementptr inbounds { ptr, i64, { ptr, ptr } }, ptr %29, i32 0, i32 0 +// CHECK-NEXT: store ptr %28, ptr %30, align 8 +// CHECK-NEXT: %31 = getelementptr inbounds { ptr, i64, { ptr, ptr } }, ptr %29, i32 0, i32 1 +// CHECK-NEXT: store i64 0, ptr %31, align 8 +// CHECK-NEXT: %32 = getelementptr inbounds { ptr, i64, { ptr, ptr } }, ptr %29, i32 0, i32 2 +// CHECK-NEXT: store { ptr, ptr } %7, ptr %32, align 8 +// CHECK-NEXT: store ptr %29, ptr %22, align 8 +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgodefer.main", %_llgo_6), ptr %21, align 8 // CHECK-NEXT: br label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_0 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgodefer.main", %_llgo_3), ptr %17, align 8 -// CHECK-NEXT: %29 = load ptr, ptr %16, align 8 -// CHECK-NEXT: indirectbr ptr %29, [label %_llgo_3, label %_llgo_2] +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgodefer.main", %_llgo_3), ptr %21, align 8 +// CHECK-NEXT: %33 = load ptr, ptr %20, align 8 +// CHECK-NEXT: indirectbr ptr %33, [label %_llgo_3, label %_llgo_2] // CHECK-EMPTY: // CHECK-NEXT: _llgo_6: ; preds = %_llgo_8 // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_7: ; preds = %_llgo_2 -// CHECK-NEXT: %30 = load ptr, ptr %18, align 8 -// CHECK-NEXT: %31 = load { ptr, i64, { ptr, ptr } }, ptr %30, align 8 -// CHECK-NEXT: %32 = extractvalue { ptr, i64, { ptr, ptr } } %31, 0 -// CHECK-NEXT: store ptr %32, ptr %18, align 8 -// CHECK-NEXT: %33 = extractvalue { ptr, i64, { ptr, ptr } } %31, 2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.FreeDeferNode"(ptr %30) -// CHECK-NEXT: %34 = extractvalue { ptr, ptr } %33, 1 -// CHECK-NEXT: %35 = extractvalue { ptr, ptr } %33, 0 -// CHECK-NEXT: call void %35(ptr %34) +// CHECK-NEXT: %34 = load ptr, ptr %22, align 8 +// CHECK-NEXT: %35 = load { ptr, i64, { ptr, ptr } }, ptr %34, align 8 +// CHECK-NEXT: %36 = extractvalue { ptr, i64, { ptr, ptr } } %35, 0 +// CHECK-NEXT: store ptr %36, ptr %22, align 8 +// CHECK-NEXT: %37 = extractvalue { ptr, i64, { ptr, ptr } } %35, 2 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.FreeDeferNode"(ptr %34) +// CHECK-NEXT: %38 = call ptr @llvm.frameaddress.p0(i32 0) +// CHECK-NEXT: %39 = call ptr @"{{.*}}/runtime/internal/runtime.StartRecoverFrame"(ptr %38) +// CHECK-NEXT: %40 = extractvalue { ptr, ptr } %37, 1 +// CHECK-NEXT: %41 = extractvalue { ptr, ptr } %37, 0 +// CHECK-NEXT: call void %41(ptr %40) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.EndRecoverFrame"(ptr %39) // CHECK-NEXT: br label %_llgo_8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_7, %_llgo_2 -// CHECK-NEXT: %36 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %10, align 8 -// CHECK-NEXT: %37 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %36, 2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %37) -// CHECK-NEXT: %38 = load ptr, ptr %17, align 8 -// CHECK-NEXT: indirectbr ptr %38, [label %_llgo_3, label %_llgo_6] +// CHECK-NEXT: %42 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %11, align 8 +// CHECK-NEXT: %43 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %42, 2 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %43) +// CHECK-NEXT: %44 = load ptr, ptr %21, align 8 +// CHECK-NEXT: indirectbr ptr %44, [label %_llgo_3, label %_llgo_6] +// CHECK-NEXT: } + +// CHECK-LABEL: define { ptr, ptr } @"{{.*}}/cl/_testgo/cgodefer.main$1"(ptr %0){{.*}} { +// CHECK-NEXT: _llgo_0: +// CHECK-NEXT: %1 = call ptr @"{{.*}}/runtime/internal/runtime.AllocZ"(i64 8) +// CHECK-NEXT: %2 = load { ptr }, ptr %0, align 8 +// CHECK-NEXT: %3 = extractvalue { ptr } %2, 0 +// CHECK-NEXT: %4 = load ptr, ptr %3, align 8 +// CHECK-NEXT: store ptr %4, ptr %1, align 8 +// CHECK-NEXT: %5 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 8) +// CHECK-NEXT: %6 = getelementptr inbounds { ptr }, ptr %5, i32 0, i32 0 +// CHECK-NEXT: store ptr %1, ptr %6, align 8 +// CHECK-NEXT: %7 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/cgodefer.main$1$1", ptr undef }, ptr %5, 1 +// CHECK-NEXT: ret { ptr, ptr } %7 +// CHECK-NEXT: } + +// CHECK-LABEL: define void @"{{.*}}/cl/_testgo/cgodefer.main$1$1"(ptr %0){{.*}} { +// CHECK-NEXT: _llgo_0: +// CHECK-NEXT: %1 = load { ptr }, ptr %0, align 8 +// CHECK-NEXT: %2 = extractvalue { ptr } %1, 0 +// CHECK-NEXT: %3 = load ptr, ptr %2, align 8 +// CHECK-NEXT: %4 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_Pointer, ptr undef }, ptr %3, 1 +// CHECK-NEXT: %5 = extractvalue { ptr } %1, 0 +// CHECK-NEXT: %6 = load ptr, ptr %5, align 8 +// CHECK-NEXT: %7 = call [0 x i8] @"{{.*}}/cl/_testgo/cgodefer._Cfunc_free"(ptr %6) +// CHECK-NEXT: ret void // CHECK-NEXT: } -func main() { - // CHECK-LABEL: define { ptr, ptr } @"{{.*}}/cl/_testgo/cgodefer.main$1"(ptr %0){{.*}} { - // CHECK-NEXT: _llgo_0: - // CHECK-NEXT: %1 = call ptr @"{{.*}}/runtime/internal/runtime.AllocZ"(i64 8) - // CHECK-NEXT: %2 = load { ptr }, ptr %0, align 8 - // CHECK-NEXT: %3 = extractvalue { ptr } %2, 0 - // CHECK-NEXT: %4 = load ptr, ptr %3, align 8 - // CHECK-NEXT: store ptr %4, ptr %1, align 8 - // CHECK-NEXT: %5 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 8) - // CHECK-NEXT: %6 = getelementptr inbounds { ptr }, ptr %5, i32 0, i32 0 - // CHECK-NEXT: store ptr %1, ptr %6, align 8 - // CHECK-NEXT: %7 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/cgodefer.main$1$1", ptr undef }, ptr %5, 1 - // CHECK-NEXT: ret { ptr, ptr } %7 - // CHECK-NEXT: } - p := C.malloc(1024) - // CHECK-LABEL: define void @"{{.*}}/cl/_testgo/cgodefer.main$1$1"(ptr %0){{.*}} { - // CHECK-NEXT: _llgo_0: - // CHECK-NEXT: %1 = load { ptr }, ptr %0, align 8 - // CHECK-NEXT: %2 = extractvalue { ptr } %1, 0 - // CHECK-NEXT: %3 = load ptr, ptr %2, align 8 - // CHECK-NEXT: %4 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_Pointer, ptr undef }, ptr %3, 1 - // CHECK-NEXT: %5 = extractvalue { ptr } %1, 0 - // CHECK-NEXT: %6 = load ptr, ptr %5, align 8 - // CHECK-NEXT: %7 = call [0 x i8] @"{{.*}}/cl/_testgo/cgodefer._Cfunc_free"(ptr %6) - // CHECK-NEXT: ret void - // CHECK-NEXT: } - defer C.free(p) -} diff --git a/cl/_testgo/cgomacro/cgomacro.go b/cl/_testgo/cgomacro/cgomacro.go index c09837ea25..dc69c96562 100644 --- a/cl/_testgo/cgomacro/cgomacro.go +++ b/cl/_testgo/cgomacro/cgomacro.go @@ -122,56 +122,66 @@ import ( // CHECK-NEXT: %2 = call [0 x i8] @"{{.*}}/cl/_testgo/cgomacro._Cfunc_Py_Initialize"() // CHECK-NEXT: %3 = call ptr @"{{.*}}/runtime/internal/runtime.GetThreadDefer"() // CHECK-NEXT: %4 = alloca i8, i64 {{.*}}, align 1 -// CHECK-NEXT: %5 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 48) -// CHECK-NEXT: %6 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 0 -// CHECK-NEXT: store ptr %4, ptr %6, align 8 -// CHECK-NEXT: %7 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 1 -// CHECK-NEXT: store i64 0, ptr %7, align 8 -// CHECK-NEXT: %8 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 2 -// CHECK-NEXT: store ptr %3, ptr %8, align 8 -// CHECK-NEXT: %9 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 3 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgomacro.main", %_llgo_2), ptr %9, align 8 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %5) -// CHECK-NEXT: %10 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 1 -// CHECK-NEXT: %11 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 3 -// CHECK-NEXT: %12 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 4 -// CHECK-NEXT: %13 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 5 -// CHECK-NEXT: store ptr null, ptr %13, align 8 -// CHECK-NEXT: %14 = call i32 @{{.*}}sigsetjmp(ptr %4, i32 0) -// CHECK-NEXT: %15 = icmp eq i32 %14, 0 -// CHECK-NEXT: br i1 %15, label %_llgo_4, label %_llgo_5 +// CHECK-NEXT: %5 = call ptr @llvm.frameaddress.p0(i32 0) +// CHECK-NEXT: %6 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 56) +// CHECK-NEXT: %7 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %6, i32 0, i32 0 +// CHECK-NEXT: store ptr %4, ptr %7, align 8 +// CHECK-NEXT: %8 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %6, i32 0, i32 1 +// CHECK-NEXT: store i64 0, ptr %8, align 8 +// CHECK-NEXT: %9 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %6, i32 0, i32 2 +// CHECK-NEXT: store ptr %3, ptr %9, align 8 +// CHECK-NEXT: %10 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %6, i32 0, i32 3 +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgomacro.main", %_llgo_2), ptr %10, align 8 +// CHECK-NEXT: %11 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %6, i32 0, i32 4 +// CHECK-NEXT: store ptr null, ptr %11, align 8 +// CHECK-NEXT: %12 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %6, i32 0, i32 5 +// CHECK-NEXT: store ptr null, ptr %12, align 8 +// CHECK-NEXT: %13 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %6, i32 0, i32 6 +// CHECK-NEXT: store ptr %5, ptr %13, align 8 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %6) +// CHECK-NEXT: %14 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %6, i32 0, i32 1 +// CHECK-NEXT: %15 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %6, i32 0, i32 3 +// CHECK-NEXT: %16 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %6, i32 0, i32 4 +// CHECK-NEXT: %17 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %6, i32 0, i32 5 +// CHECK-NEXT: store ptr null, ptr %17, align 8 +// CHECK-NEXT: %18 = call i32 @{{.*}}sigsetjmp(ptr %4, i32 0) +// CHECK-NEXT: %19 = icmp eq i32 %18, 0 +// CHECK-NEXT: br i1 %19, label %_llgo_4, label %_llgo_5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_3 // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_5, %_llgo_4 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgomacro.main", %_llgo_3), ptr %11, align 8 -// CHECK-NEXT: %16 = load i64, ptr %10, align 8 -// CHECK-NEXT: %17 = call [0 x i8] @"{{.*}}/cl/_testgo/cgomacro._Cfunc_Py_Finalize"() -// CHECK-NEXT: %18 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, align 8 -// CHECK-NEXT: %19 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %18, 2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %19) -// CHECK-NEXT: %20 = load ptr, ptr %12, align 8 -// CHECK-NEXT: indirectbr ptr %20, [label %_llgo_3, label %_llgo_6] +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgomacro.main", %_llgo_3), ptr %15, align 8 +// CHECK-NEXT: %20 = load i64, ptr %14, align 8 +// CHECK-NEXT: %21 = call ptr @llvm.frameaddress.p0(i32 0) +// CHECK-NEXT: %22 = call ptr @"{{.*}}/runtime/internal/runtime.StartRecoverFrame"(ptr %21) +// CHECK-NEXT: %23 = call [0 x i8] @"{{.*}}/cl/_testgo/cgomacro._Cfunc_Py_Finalize"() +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.EndRecoverFrame"(ptr %22) +// CHECK-NEXT: %24 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %6, align 8 +// CHECK-NEXT: %25 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %24, 2 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %25) +// CHECK-NEXT: %26 = load ptr, ptr %16, align 8 +// CHECK-NEXT: indirectbr ptr %26, [label %_llgo_3, label %_llgo_6] // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_5, %_llgo_2 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Rethrow"(ptr %3) // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_0 -// CHECK-NEXT: %21 = call i32 @"{{.*}}/cl/_testgo/cgomacro.main$2"() -// CHECK-NEXT: %22 = call i32 @"{{.*}}/cl/_testgo/cgomacro.main$3"() -// CHECK-NEXT: %23 = call i32 @"{{.*}}/cl/_testgo/cgomacro.main$4"() -// CHECK-NEXT: %24 = call i32 @"{{.*}}/cl/_testgo/cgomacro.main$5"() -// CHECK-NEXT: %25 = call i32 @"{{.*}}/cl/_testgo/cgomacro.main$6"() -// CHECK-NEXT: %26 = call i32 @"{{.*}}/cl/_testgo/cgomacro.main$7"() -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgomacro.main", %_llgo_6), ptr %12, align 8 +// CHECK-NEXT: %27 = call i32 @"{{.*}}/cl/_testgo/cgomacro.main$2"() +// CHECK-NEXT: %28 = call i32 @"{{.*}}/cl/_testgo/cgomacro.main$3"() +// CHECK-NEXT: %29 = call i32 @"{{.*}}/cl/_testgo/cgomacro.main$4"() +// CHECK-NEXT: %30 = call i32 @"{{.*}}/cl/_testgo/cgomacro.main$5"() +// CHECK-NEXT: %31 = call i32 @"{{.*}}/cl/_testgo/cgomacro.main$6"() +// CHECK-NEXT: %32 = call i32 @"{{.*}}/cl/_testgo/cgomacro.main$7"() +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgomacro.main", %_llgo_6), ptr %16, align 8 // CHECK-NEXT: br label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_0 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgomacro.main", %_llgo_3), ptr %12, align 8 -// CHECK-NEXT: %27 = load ptr, ptr %11, align 8 -// CHECK-NEXT: indirectbr ptr %27, [label %_llgo_3, label %_llgo_2] +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgomacro.main", %_llgo_3), ptr %16, align 8 +// CHECK-NEXT: %33 = load ptr, ptr %15, align 8 +// CHECK-NEXT: indirectbr ptr %33, [label %_llgo_3, label %_llgo_2] // CHECK-EMPTY: // CHECK-NEXT: _llgo_6: ; preds = %_llgo_2 // CHECK-NEXT: ret void diff --git a/cl/_testgo/cgopython/cgopython.go b/cl/_testgo/cgopython/cgopython.go index 7df1b38eeb..dacd3c1dea 100644 --- a/cl/_testgo/cgopython/cgopython.go +++ b/cl/_testgo/cgopython/cgopython.go @@ -55,52 +55,62 @@ import "C" // CHECK-NEXT: %0 = call [0 x i8] @"{{.*}}/cl/_testgo/cgopython._Cfunc_Py_Initialize"() // CHECK-NEXT: %1 = call ptr @"{{.*}}/runtime/internal/runtime.GetThreadDefer"() // CHECK-NEXT: %2 = alloca i8, i64 {{.*}}, align 1 -// CHECK-NEXT: %3 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 48) -// CHECK-NEXT: %4 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 0 -// CHECK-NEXT: store ptr %2, ptr %4, align 8 -// CHECK-NEXT: %5 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 1 -// CHECK-NEXT: store i64 0, ptr %5, align 8 -// CHECK-NEXT: %6 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 2 -// CHECK-NEXT: store ptr %1, ptr %6, align 8 -// CHECK-NEXT: %7 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 3 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgopython.main", %_llgo_2), ptr %7, align 8 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %3) -// CHECK-NEXT: %8 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 1 -// CHECK-NEXT: %9 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 3 -// CHECK-NEXT: %10 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 4 -// CHECK-NEXT: %11 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 5 -// CHECK-NEXT: store ptr null, ptr %11, align 8 -// CHECK-NEXT: %12 = call i32 @{{.*}}sigsetjmp(ptr %2, i32 0) -// CHECK-NEXT: %13 = icmp eq i32 %12, 0 -// CHECK-NEXT: br i1 %13, label %_llgo_4, label %_llgo_5 +// CHECK-NEXT: %3 = call ptr @llvm.frameaddress.p0(i32 0) +// CHECK-NEXT: %4 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 56) +// CHECK-NEXT: %5 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %4, i32 0, i32 0 +// CHECK-NEXT: store ptr %2, ptr %5, align 8 +// CHECK-NEXT: %6 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %4, i32 0, i32 1 +// CHECK-NEXT: store i64 0, ptr %6, align 8 +// CHECK-NEXT: %7 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %4, i32 0, i32 2 +// CHECK-NEXT: store ptr %1, ptr %7, align 8 +// CHECK-NEXT: %8 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %4, i32 0, i32 3 +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgopython.main", %_llgo_2), ptr %8, align 8 +// CHECK-NEXT: %9 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %4, i32 0, i32 4 +// CHECK-NEXT: store ptr null, ptr %9, align 8 +// CHECK-NEXT: %10 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %4, i32 0, i32 5 +// CHECK-NEXT: store ptr null, ptr %10, align 8 +// CHECK-NEXT: %11 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %4, i32 0, i32 6 +// CHECK-NEXT: store ptr %3, ptr %11, align 8 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %4) +// CHECK-NEXT: %12 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %4, i32 0, i32 1 +// CHECK-NEXT: %13 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %4, i32 0, i32 3 +// CHECK-NEXT: %14 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %4, i32 0, i32 4 +// CHECK-NEXT: %15 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %4, i32 0, i32 5 +// CHECK-NEXT: store ptr null, ptr %15, align 8 +// CHECK-NEXT: %16 = call i32 @{{.*}}sigsetjmp(ptr %2, i32 0) +// CHECK-NEXT: %17 = icmp eq i32 %16, 0 +// CHECK-NEXT: br i1 %17, label %_llgo_4, label %_llgo_5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_3 // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_5, %_llgo_4 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgopython.main", %_llgo_3), ptr %9, align 8 -// CHECK-NEXT: %14 = load i64, ptr %8, align 8 -// CHECK-NEXT: %15 = call [0 x i8] @"{{.*}}/cl/_testgo/cgopython._Cfunc_Py_Finalize"() -// CHECK-NEXT: %16 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, align 8 -// CHECK-NEXT: %17 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %16, 2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %17) -// CHECK-NEXT: %18 = load ptr, ptr %10, align 8 -// CHECK-NEXT: indirectbr ptr %18, [label %_llgo_3, label %_llgo_6] +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgopython.main", %_llgo_3), ptr %13, align 8 +// CHECK-NEXT: %18 = load i64, ptr %12, align 8 +// CHECK-NEXT: %19 = call ptr @llvm.frameaddress.p0(i32 0) +// CHECK-NEXT: %20 = call ptr @"{{.*}}/runtime/internal/runtime.StartRecoverFrame"(ptr %19) +// CHECK-NEXT: %21 = call [0 x i8] @"{{.*}}/cl/_testgo/cgopython._Cfunc_Py_Finalize"() +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.EndRecoverFrame"(ptr %20) +// CHECK-NEXT: %22 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %4, align 8 +// CHECK-NEXT: %23 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %22, 2 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %23) +// CHECK-NEXT: %24 = load ptr, ptr %14, align 8 +// CHECK-NEXT: indirectbr ptr %24, [label %_llgo_3, label %_llgo_6] // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_5, %_llgo_2 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Rethrow"(ptr %1) // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_0 -// CHECK-NEXT: %19 = call ptr @"{{.*}}/runtime/internal/runtime.CString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 23 }) -// CHECK-NEXT: %20 = call i32 @"{{.*}}/cl/_testgo/cgopython._Cfunc_PyRun_SimpleString"(ptr %19) -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgopython.main", %_llgo_6), ptr %10, align 8 +// CHECK-NEXT: %25 = call ptr @"{{.*}}/runtime/internal/runtime.CString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 23 }) +// CHECK-NEXT: %26 = call i32 @"{{.*}}/cl/_testgo/cgopython._Cfunc_PyRun_SimpleString"(ptr %25) +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgopython.main", %_llgo_6), ptr %14, align 8 // CHECK-NEXT: br label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_0 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgopython.main", %_llgo_3), ptr %10, align 8 -// CHECK-NEXT: %21 = load ptr, ptr %9, align 8 -// CHECK-NEXT: indirectbr ptr %21, [label %_llgo_3, label %_llgo_2] +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/cgopython.main", %_llgo_3), ptr %14, align 8 +// CHECK-NEXT: %27 = load ptr, ptr %13, align 8 +// CHECK-NEXT: indirectbr ptr %27, [label %_llgo_3, label %_llgo_2] // CHECK-EMPTY: // CHECK-NEXT: _llgo_6: ; preds = %_llgo_2 // CHECK-NEXT: ret void diff --git a/cl/_testgo/closureall/in.go b/cl/_testgo/closureall/in.go index a9dedf95bb..31eabd33c0 100644 --- a/cl/_testgo/closureall/in.go +++ b/cl/_testgo/closureall/in.go @@ -7,8 +7,6 @@ import "github.com/goplus/lib/c" // CHECK: @0 = private unnamed_addr constant [46 x i8] c"{{.*}}/cl/_testgo/closureall.S", align 1 // CHECK: @1 = private unnamed_addr constant [3 x i8] c"Inc", align 1 -// CHECK: @7 = private unnamed_addr constant [3 x i8] c"Add", align 1 -// CHECK: @9 = private unnamed_addr constant [23 x i8] c"interface{Add(int) int}", align 1 //go:linkname cSqrt C.sqrt func cSqrt(x c.Double) c.Double @@ -173,7 +171,8 @@ func makeWithFree(base int) Fn { // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %21, %"{{.*}}/runtime/internal/runtime.String" { ptr @9, i64 23 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @7, i64 3 }) +// CHECK-NEXT: %32 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %21, ptr @"_llgo_iface$VdBKYV8-gcMjZtZfcf-u2oKoj9Lu3VXwuG8TGCW2S4A", ptr @"_llgo_iface$VdBKYV8-gcMjZtZfcf-u2oKoj9Lu3VXwuG8TGCW2S4A") +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %32) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/genericembediface/in.go b/cl/_testgo/genericembediface/in.go index c83bc68087..0f8cee9628 100644 --- a/cl/_testgo/genericembediface/in.go +++ b/cl/_testgo/genericembediface/in.go @@ -7,7 +7,6 @@ import ( // CHECK: {{^}}@2 = private unnamed_addr constant [20 x i8] c"ServerReflectionInfo", align 1{{$}} // CHECK: {{^}}@5 = private unnamed_addr constant [7 x i8] c"Context", align 1{{$}} -// CHECK: {{^}}@11 = private unnamed_addr constant [68 x i8] c"{{.*}}/cl/_testgo/genericembediface.ReflectionServer", align 1{{$}} // CHECK: {{^}}@19 = private unnamed_addr constant [4 x i8] c"pass", align 1{{$}} // CHECK: {{^}}@20 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/genericembediface.server", align 1{{$}} // CHECK: {{^}}@21 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/genericembediface.stream", align 1{{$}} @@ -48,7 +47,8 @@ type ReflectionServer interface { // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %21 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @11, i64 68 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 20 }) +// CHECK-NEXT: %22 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %2, ptr @"_llgo_{{.*}}/cl/_testgo/genericembediface.ReflectionServer", ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %22) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -139,6 +139,12 @@ func main() { // CHECK-NEXT: ret i1 %3 // CHECK-NEXT: } +// CHECK-LABEL: define linkonce i1 @"__llgo_stub.{{.*}}/runtime/internal/runtime.nilinterequal"(ptr %0, ptr %1, ptr %2){{.*}} { +// CHECK-NEXT: _llgo_0: +// CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.nilinterequal"(ptr %1, ptr %2) +// CHECK-NEXT: ret i1 %3 +// CHECK-NEXT: } + // CHECK-LABEL: define linkonce %"{{.*}}/runtime/internal/runtime.String" @"{{.*}}/cl/_testgo/genericembediface/streamlib.(*GenericServerStream[{{.*}}/cl/_testgo/genericembediface.Request,{{.*}}/cl/_testgo/genericembediface.Response]).Context"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %1 = getelementptr inbounds %"{{.*}}/cl/_testgo/genericembediface/streamlib.GenericServerStream[{{.*}}/cl/_testgo/genericembediface.Request,{{.*}}/cl/_testgo/genericembediface.Response]", ptr %0, i32 0, i32 0 diff --git a/cl/_testgo/ifaceprom/in.go b/cl/_testgo/ifaceprom/in.go index b84e4a3a35..a3ed341b71 100644 --- a/cl/_testgo/ifaceprom/in.go +++ b/cl/_testgo/ifaceprom/in.go @@ -8,8 +8,7 @@ package main // CHECK: @0 = private unnamed_addr constant [3 x i8] c"two", align 1 // CHECK: @1 = private unnamed_addr constant [48 x i8] c"{{.*}}/cl/_testgo/ifaceprom.impl", align 1 // CHECK: @2 = private unnamed_addr constant [3 x i8] c"one", align 1 -// CHECK: @13 = private unnamed_addr constant [45 x i8] c"{{.*}}/cl/_testgo/ifaceprom.I", align 1 -// CHECK: @14 = private unnamed_addr constant [4 x i8] c"pass", align 1 +// CHECK: @13 = private unnamed_addr constant [4 x i8] c"pass", align 1 type I interface { one() int @@ -256,7 +255,7 @@ func main() { // CHECK-EMPTY: // CHECK-NEXT: _llgo_7: ; preds = %_llgo_19 // CHECK-NEXT: %44 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 8) -// CHECK-NEXT: store i64 %100, ptr %44, align 8 +// CHECK-NEXT: store i64 %101, ptr %44, align 8 // CHECK-NEXT: %45 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_int, ptr undef }, ptr %44, 1 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %45) // CHECK-NEXT: unreachable @@ -316,7 +315,7 @@ func main() { // CHECK-EMPTY: // CHECK-NEXT: _llgo_13: ; preds = %_llgo_21 // CHECK-NEXT: %80 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" %107, ptr %80, align 8 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" %109, ptr %80, align 8 // CHECK-NEXT: %81 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %80, 1 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %81) // CHECK-NEXT: unreachable @@ -330,13 +329,13 @@ func main() { // CHECK-EMPTY: // CHECK-NEXT: _llgo_15: ; preds = %_llgo_23 // CHECK-NEXT: %86 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" %115, ptr %86, align 8 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" %118, ptr %86, align 8 // CHECK-NEXT: %87 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %86, 1 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %87) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_16: ; preds = %_llgo_23 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @14, i64 4 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 4 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-EMPTY: @@ -352,54 +351,58 @@ func main() { // CHECK-NEXT: br i1 %94, label %_llgo_5, label %_llgo_6 // CHECK-EMPTY: // CHECK-NEXT: _llgo_18: ; preds = %_llgo_4 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %36, %"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 45 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }) +// CHECK-NEXT: %95 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %36, ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I", ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I") +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %95) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_19: ; preds = %_llgo_6 -// CHECK-NEXT: %95 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: %96 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %95, i32 0, i32 0 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %41, ptr %96, align 8 -// CHECK-NEXT: %97 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.one$bound", ptr undef }, ptr %95, 1 -// CHECK-NEXT: %98 = extractvalue { ptr, ptr } %97, 1 -// CHECK-NEXT: %99 = extractvalue { ptr, ptr } %97, 0 -// CHECK-NEXT: %100 = call i64 %99(ptr %98) -// CHECK-NEXT: %101 = icmp ne i64 %100, 1 -// CHECK-NEXT: br i1 %101, label %_llgo_7, label %_llgo_8 +// CHECK-NEXT: %96 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: %97 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %96, i32 0, i32 0 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %41, ptr %97, align 8 +// CHECK-NEXT: %98 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.one$bound", ptr undef }, ptr %96, 1 +// CHECK-NEXT: %99 = extractvalue { ptr, ptr } %98, 1 +// CHECK-NEXT: %100 = extractvalue { ptr, ptr } %98, 0 +// CHECK-NEXT: %101 = call i64 %100(ptr %99) +// CHECK-NEXT: %102 = icmp ne i64 %101, 1 +// CHECK-NEXT: br i1 %102, label %_llgo_7, label %_llgo_8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_20: ; preds = %_llgo_6 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %42, %"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 45 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }) +// CHECK-NEXT: %103 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %42, ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I", ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I") +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %103) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_21: ; preds = %_llgo_12 -// CHECK-NEXT: %102 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: %103 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %102, i32 0, i32 0 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %77, ptr %103, align 8 -// CHECK-NEXT: %104 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.two$bound", ptr undef }, ptr %102, 1 -// CHECK-NEXT: %105 = extractvalue { ptr, ptr } %104, 1 -// CHECK-NEXT: %106 = extractvalue { ptr, ptr } %104, 0 -// CHECK-NEXT: %107 = call %"{{.*}}/runtime/internal/runtime.String" %106(ptr %105) -// CHECK-NEXT: %108 = call i1 @"{{.*}}/runtime/internal/runtime.StringEqual"(%"{{.*}}/runtime/internal/runtime.String" %107, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }) -// CHECK-NEXT: %109 = xor i1 %108, true -// CHECK-NEXT: br i1 %109, label %_llgo_13, label %_llgo_14 +// CHECK-NEXT: %104 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: %105 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %104, i32 0, i32 0 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %77, ptr %105, align 8 +// CHECK-NEXT: %106 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.two$bound", ptr undef }, ptr %104, 1 +// CHECK-NEXT: %107 = extractvalue { ptr, ptr } %106, 1 +// CHECK-NEXT: %108 = extractvalue { ptr, ptr } %106, 0 +// CHECK-NEXT: %109 = call %"{{.*}}/runtime/internal/runtime.String" %108(ptr %107) +// CHECK-NEXT: %110 = call i1 @"{{.*}}/runtime/internal/runtime.StringEqual"(%"{{.*}}/runtime/internal/runtime.String" %109, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }) +// CHECK-NEXT: %111 = xor i1 %110, true +// CHECK-NEXT: br i1 %111, label %_llgo_13, label %_llgo_14 // CHECK-EMPTY: // CHECK-NEXT: _llgo_22: ; preds = %_llgo_12 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %78, %"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 45 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }) +// CHECK-NEXT: %112 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %78, ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I", ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I") +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %112) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_23: ; preds = %_llgo_14 -// CHECK-NEXT: %110 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: %111 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %110, i32 0, i32 0 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %83, ptr %111, align 8 -// CHECK-NEXT: %112 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.two$bound", ptr undef }, ptr %110, 1 -// CHECK-NEXT: %113 = extractvalue { ptr, ptr } %112, 1 -// CHECK-NEXT: %114 = extractvalue { ptr, ptr } %112, 0 -// CHECK-NEXT: %115 = call %"{{.*}}/runtime/internal/runtime.String" %114(ptr %113) -// CHECK-NEXT: %116 = call i1 @"{{.*}}/runtime/internal/runtime.StringEqual"(%"{{.*}}/runtime/internal/runtime.String" %115, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }) -// CHECK-NEXT: %117 = xor i1 %116, true -// CHECK-NEXT: br i1 %117, label %_llgo_15, label %_llgo_16 +// CHECK-NEXT: %113 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: %114 = getelementptr inbounds { %"{{.*}}/runtime/internal/runtime.iface" }, ptr %113, i32 0, i32 0 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.iface" %83, ptr %114, align 8 +// CHECK-NEXT: %115 = insertvalue { ptr, ptr } { ptr @"{{.*}}/cl/_testgo/ifaceprom.I.two$bound", ptr undef }, ptr %113, 1 +// CHECK-NEXT: %116 = extractvalue { ptr, ptr } %115, 1 +// CHECK-NEXT: %117 = extractvalue { ptr, ptr } %115, 0 +// CHECK-NEXT: %118 = call %"{{.*}}/runtime/internal/runtime.String" %117(ptr %116) +// CHECK-NEXT: %119 = call i1 @"{{.*}}/runtime/internal/runtime.StringEqual"(%"{{.*}}/runtime/internal/runtime.String" %118, %"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 3 }) +// CHECK-NEXT: %120 = xor i1 %119, true +// CHECK-NEXT: br i1 %120, label %_llgo_15, label %_llgo_16 // CHECK-EMPTY: // CHECK-NEXT: _llgo_24: ; preds = %_llgo_14 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %84, %"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 45 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }) +// CHECK-NEXT: %121 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %84, ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I", ptr @"_llgo_{{.*}}/cl/_testgo/ifaceprom.I") +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %121) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/invoke/in.go b/cl/_testgo/invoke/in.go index 6c7f8377f7..e5d981da2d 100644 --- a/cl/_testgo/invoke/in.go +++ b/cl/_testgo/invoke/in.go @@ -17,9 +17,6 @@ package main // CHECK: {{^}}@13 = private unnamed_addr constant [43 x i8] c"{{.*}}/cl/_testgo/invoke.T6", align 1{{$}} // CHECK: {{^}}@14 = private unnamed_addr constant [5 x i8] c"hello", align 1{{$}} // CHECK: {{^}}@36 = private unnamed_addr constant [5 x i8] c"world", align 1{{$}} -// CHECK: {{^}}@38 = private unnamed_addr constant [42 x i8] c"{{.*}}/cl/_testgo/invoke.I", align 1{{$}} -// CHECK: {{^}}@40 = private unnamed_addr constant [3 x i8] c"any", align 1{{$}} -// CHECK: {{^}}@41 = private unnamed_addr constant [23 x i8] c"interface{Invoke() int}", align 1{{$}} type T struct { s string @@ -445,28 +442,31 @@ type M interface { // CHECK-NEXT: br i1 %85, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %78, %"{{.*}}/runtime/internal/runtime.String" { ptr @38, i64 42 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 6 }) +// CHECK-NEXT: %86 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %78, ptr @"_llgo_{{.*}}/cl/_testgo/invoke.I", ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %86) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_1 -// CHECK-NEXT: %86 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %77, 0 -// CHECK-NEXT: %87 = call i1 @"{{.*}}/runtime/internal/runtime.Implements"(ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr %86) -// CHECK-NEXT: br i1 %87, label %_llgo_5, label %_llgo_6 +// CHECK-NEXT: %87 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %77, 0 +// CHECK-NEXT: %88 = call i1 @"{{.*}}/runtime/internal/runtime.Implements"(ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr %87) +// CHECK-NEXT: br i1 %88, label %_llgo_5, label %_llgo_6 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %84, %"{{.*}}/runtime/internal/runtime.String" { ptr @40, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %89 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %84, ptr @_llgo_any, ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %89) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_3 -// CHECK-NEXT: %88 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %77, 1 -// CHECK-NEXT: %89 = call ptr @"{{.*}}/runtime/internal/runtime.NewItab"(ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr %86) -// CHECK-NEXT: %90 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" undef, ptr %89, 0 -// CHECK-NEXT: %91 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" %90, ptr %88, 1 -// CHECK-NEXT: call void @"{{.*}}/cl/_testgo/invoke.invoke"(%"{{.*}}/runtime/internal/runtime.iface" %91) +// CHECK-NEXT: %90 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %77, 1 +// CHECK-NEXT: %91 = call ptr @"{{.*}}/runtime/internal/runtime.NewItab"(ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr %87) +// CHECK-NEXT: %92 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" undef, ptr %91, 0 +// CHECK-NEXT: %93 = insertvalue %"{{.*}}/runtime/internal/runtime.iface" %92, ptr %90, 1 +// CHECK-NEXT: call void @"{{.*}}/cl/_testgo/invoke.invoke"(%"{{.*}}/runtime/internal/runtime.iface" %93) // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_6: ; preds = %_llgo_3 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %86, %"{{.*}}/runtime/internal/runtime.String" { ptr @41, i64 23 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 6 }) +// CHECK-NEXT: %94 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %87, ptr @"_llgo_iface$uRUteI7wmSy7y7ODhGzk0FdDaxGKMhVSSu6HZEv9aa0", ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %94) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/reader/in.go b/cl/_testgo/reader/in.go index 5aa0a21155..ee233843f1 100644 --- a/cl/_testgo/reader/in.go +++ b/cl/_testgo/reader/in.go @@ -11,15 +11,14 @@ import ( // CHECK: @29 = private unnamed_addr constant [11 x i8] c"short write", align 1 // CHECK: @30 = private unnamed_addr constant [11 x i8] c"hello world", align 1 // CHECK: @53 = private unnamed_addr constant [50 x i8] c"{{.*}}/cl/_testgo/reader.nopCloser", align 1 -// CHECK: @54 = private unnamed_addr constant [49 x i8] c"{{.*}}/cl/_testgo/reader.WriterTo", align 1 -// CHECK: @55 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/reader.nopCloserWriterTo", align 1 -// CHECK: @56 = private unnamed_addr constant [37 x i8] c"stringsReader.ReadAt: negative offset", align 1 -// CHECK: @57 = private unnamed_addr constant [34 x i8] c"stringsReader.Seek: invalid whence", align 1 -// CHECK: @58 = private unnamed_addr constant [37 x i8] c"stringsReader.Seek: negative position", align 1 -// CHECK: @59 = private unnamed_addr constant [48 x i8] c"stringsReader.UnreadByte: at beginning of string", align 1 -// CHECK: @60 = private unnamed_addr constant [49 x i8] c"strings.Reader.UnreadRune: at beginning of string", align 1 -// CHECK: @61 = private unnamed_addr constant [62 x i8] c"strings.Reader.UnreadRune: previous operation was not ReadRune", align 1 -// CHECK: @62 = private unnamed_addr constant [48 x i8] c"stringsReader.WriteTo: invalid WriteString count", align 1 +// CHECK: @54 = private unnamed_addr constant [58 x i8] c"{{.*}}/cl/_testgo/reader.nopCloserWriterTo", align 1 +// CHECK: @55 = private unnamed_addr constant [37 x i8] c"stringsReader.ReadAt: negative offset", align 1 +// CHECK: @56 = private unnamed_addr constant [34 x i8] c"stringsReader.Seek: invalid whence", align 1 +// CHECK: @57 = private unnamed_addr constant [37 x i8] c"stringsReader.Seek: negative position", align 1 +// CHECK: @58 = private unnamed_addr constant [48 x i8] c"stringsReader.UnreadByte: at beginning of string", align 1 +// CHECK: @59 = private unnamed_addr constant [49 x i8] c"strings.Reader.UnreadRune: at beginning of string", align 1 +// CHECK: @60 = private unnamed_addr constant [62 x i8] c"strings.Reader.UnreadRune: previous operation was not ReadRune", align 1 +// CHECK: @61 = private unnamed_addr constant [48 x i8] c"stringsReader.WriteTo: invalid WriteString count", align 1 type Reader interface { Read(p []byte) (n int, err error) @@ -689,14 +688,15 @@ func main() { // CHECK-NEXT: ret { i64, %"{{.*}}/runtime/internal/runtime.iface" } %23 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %5, %"{{.*}}/runtime/internal/runtime.String" { ptr @54, i64 49 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 7 }) +// CHECK-NEXT: %24 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %5, ptr @"_llgo_{{.*}}/cl/_testgo/reader.WriterTo", ptr @"_llgo_{{.*}}/cl/_testgo/reader.Reader") +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %24) // CHECK-NEXT: unreachable // CHECK-NEXT: } // CHECK-LABEL: define %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.(*nopCloserWriterTo).Close"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %1 = icmp eq ptr %0, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @55, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @17, i64 5 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @54, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @17, i64 5 }) // CHECK-NEXT: %2 = load %"{{.*}}/cl/_testgo/reader.nopCloserWriterTo", ptr %0, align 8 // CHECK-NEXT: %3 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.nopCloserWriterTo.Close"(%"{{.*}}/cl/_testgo/reader.nopCloserWriterTo" %2) // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %3 @@ -725,7 +725,7 @@ func main() { // CHECK-LABEL: define { i64, %"{{.*}}/runtime/internal/runtime.iface" } @"{{.*}}/cl/_testgo/reader.(*nopCloserWriterTo).WriteTo"(ptr %0, %"{{.*}}/runtime/internal/runtime.iface" %1){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %2 = icmp eq ptr %0, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @55, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 7 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @54, i64 58 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 7 }) // CHECK-NEXT: %3 = load %"{{.*}}/cl/_testgo/reader.nopCloserWriterTo", ptr %0, align 8 // CHECK-NEXT: %4 = call { i64, %"{{.*}}/runtime/internal/runtime.iface" } @"{{.*}}/cl/_testgo/reader.nopCloserWriterTo.WriteTo"(%"{{.*}}/cl/_testgo/reader.nopCloserWriterTo" %3, %"{{.*}}/runtime/internal/runtime.iface" %1) // CHECK-NEXT: %5 = extractvalue { i64, %"{{.*}}/runtime/internal/runtime.iface" } %4, 0 @@ -801,7 +801,7 @@ func main() { // CHECK-NEXT: br i1 %3, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 -// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @56, i64 37 }) +// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @55, i64 37 }) // CHECK-NEXT: %5 = insertvalue { i64, %"{{.*}}/runtime/internal/runtime.iface" } { i64 0, %"{{.*}}/runtime/internal/runtime.iface" undef }, %"{{.*}}/runtime/internal/runtime.iface" %4, 1 // CHECK-NEXT: ret { i64, %"{{.*}}/runtime/internal/runtime.iface" } %5 // CHECK-EMPTY: @@ -987,12 +987,12 @@ func main() { // CHECK-NEXT: br i1 %15, label %_llgo_5, label %_llgo_7 // CHECK-EMPTY: // CHECK-NEXT: _llgo_7: ; preds = %_llgo_6 -// CHECK-NEXT: %16 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @57, i64 34 }) +// CHECK-NEXT: %16 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @56, i64 34 }) // CHECK-NEXT: %17 = insertvalue { i64, %"{{.*}}/runtime/internal/runtime.iface" } { i64 0, %"{{.*}}/runtime/internal/runtime.iface" undef }, %"{{.*}}/runtime/internal/runtime.iface" %16, 1 // CHECK-NEXT: ret { i64, %"{{.*}}/runtime/internal/runtime.iface" } %17 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_1 -// CHECK-NEXT: %18 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @58, i64 37 }) +// CHECK-NEXT: %18 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @57, i64 37 }) // CHECK-NEXT: %19 = insertvalue { i64, %"{{.*}}/runtime/internal/runtime.iface" } { i64 0, %"{{.*}}/runtime/internal/runtime.iface" undef }, %"{{.*}}/runtime/internal/runtime.iface" %18, 1 // CHECK-NEXT: ret { i64, %"{{.*}}/runtime/internal/runtime.iface" } %19 // CHECK-EMPTY: @@ -1020,7 +1020,7 @@ func main() { // CHECK-NEXT: br i1 %3, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 -// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @59, i64 48 }) +// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @58, i64 48 }) // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 @@ -1042,7 +1042,7 @@ func main() { // CHECK-NEXT: br i1 %3, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 -// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @60, i64 49 }) +// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @59, i64 49 }) // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 @@ -1052,7 +1052,7 @@ func main() { // CHECK-NEXT: br i1 %7, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_2 -// CHECK-NEXT: %8 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @61, i64 62 }) +// CHECK-NEXT: %8 = call %"{{.*}}/runtime/internal/runtime.iface" @"{{.*}}/cl/_testgo/reader.newError"(%"{{.*}}/runtime/internal/runtime.String" { ptr @60, i64 62 }) // CHECK-NEXT: ret %"{{.*}}/runtime/internal/runtime.iface" %8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_2 @@ -1096,7 +1096,7 @@ func main() { // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_2 // CHECK-NEXT: %20 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @62, i64 48 }, ptr %20, align 8 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @61, i64 48 }, ptr %20, align 8 // CHECK-NEXT: %21 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %20, 1 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %21) // CHECK-NEXT: unreachable diff --git a/cl/_testgo/recoverthenpanic/in.go b/cl/_testgo/recoverthenpanic/in.go index bee54a59bb..e747b0baf6 100644 --- a/cl/_testgo/recoverthenpanic/in.go +++ b/cl/_testgo/recoverthenpanic/in.go @@ -7,43 +7,51 @@ package main // CHECK-LABEL: define void @"{{.*}}/cl/_testgo/recoverthenpanic.End"(){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %0 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.Recover"() -// CHECK-NEXT: %1 = call i1 @"{{.*}}/runtime/internal/runtime.EfaceEqual"(%"{{.*}}/runtime/internal/runtime.eface" %0, %"{{.*}}/runtime/internal/runtime.eface" zeroinitializer) -// CHECK-NEXT: %2 = xor i1 %1, true -// CHECK-NEXT: %3 = call ptr @"{{.*}}/runtime/internal/runtime.GetThreadDefer"() -// CHECK-NEXT: %4 = alloca i8, i64 {{.*}}, align 1 -// CHECK-NEXT: %5 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 48) -// CHECK-NEXT: %6 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 0 -// CHECK-NEXT: store ptr %4, ptr %6, align 8 -// CHECK-NEXT: %7 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 1 -// CHECK-NEXT: store i64 0, ptr %7, align 8 -// CHECK-NEXT: %8 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 2 -// CHECK-NEXT: store ptr %3, ptr %8, align 8 -// CHECK-NEXT: %9 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 3 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.End", %_llgo_5), ptr %9, align 8 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %5) -// CHECK-NEXT: %10 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 1 -// CHECK-NEXT: %11 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 3 -// CHECK-NEXT: %12 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 4 -// CHECK-NEXT: %13 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, i32 0, i32 5 +// CHECK-NEXT: %0 = call ptr @llvm.frameaddress.p0(i32 1) +// CHECK-NEXT: %1 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.Recover"(ptr %0) +// CHECK-NEXT: %2 = call i1 @"{{.*}}/runtime/internal/runtime.EfaceEqual"(%"{{.*}}/runtime/internal/runtime.eface" %1, %"{{.*}}/runtime/internal/runtime.eface" zeroinitializer) +// CHECK-NEXT: %3 = xor i1 %2, true +// CHECK-NEXT: %4 = call ptr @"{{.*}}/runtime/internal/runtime.GetThreadDefer"() +// CHECK-NEXT: %5 = alloca i8, i64 {{.*}}, align 1 +// CHECK-NEXT: %6 = call ptr @llvm.frameaddress.p0(i32 0) +// CHECK-NEXT: %7 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 56) +// CHECK-NEXT: %8 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %7, i32 0, i32 0 +// CHECK-NEXT: store ptr %5, ptr %8, align 8 +// CHECK-NEXT: %9 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %7, i32 0, i32 1 +// CHECK-NEXT: store i64 0, ptr %9, align 8 +// CHECK-NEXT: %10 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %7, i32 0, i32 2 +// CHECK-NEXT: store ptr %4, ptr %10, align 8 +// CHECK-NEXT: %11 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %7, i32 0, i32 3 +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.End", %_llgo_5), ptr %11, align 8 +// CHECK-NEXT: %12 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %7, i32 0, i32 4 +// CHECK-NEXT: store ptr null, ptr %12, align 8 +// CHECK-NEXT: %13 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %7, i32 0, i32 5 // CHECK-NEXT: store ptr null, ptr %13, align 8 -// CHECK-NEXT: %14 = call i32 @{{.*}}sigsetjmp(ptr %4, i32 0) -// CHECK-NEXT: %15 = icmp eq i32 %14, 0 -// CHECK-NEXT: br i1 %15, label %_llgo_4, label %_llgo_7 +// CHECK-NEXT: %14 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %7, i32 0, i32 6 +// CHECK-NEXT: store ptr %6, ptr %14, align 8 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %7) +// CHECK-NEXT: %15 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %7, i32 0, i32 1 +// CHECK-NEXT: %16 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %7, i32 0, i32 3 +// CHECK-NEXT: %17 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %7, i32 0, i32 4 +// CHECK-NEXT: %18 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %7, i32 0, i32 5 +// CHECK-NEXT: store ptr null, ptr %18, align 8 +// CHECK-NEXT: %19 = call i32 @{{.*}}sigsetjmp(ptr %5, i32 0) +// CHECK-NEXT: %20 = icmp eq i32 %19, 0 +// CHECK-NEXT: br i1 %20, label %_llgo_4, label %_llgo_7 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_4 -// CHECK-NEXT: %16 = load i64, ptr %10, align 8 -// CHECK-NEXT: %17 = or i64 %16, 1 -// CHECK-NEXT: store i64 %17, ptr %10, align 8 -// CHECK-NEXT: %18 = load ptr, ptr %13, align 8 -// CHECK-NEXT: %19 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 32) -// CHECK-NEXT: %20 = getelementptr inbounds { ptr, i64, %"{{.*}}/runtime/internal/runtime.eface" }, ptr %19, i32 0, i32 0 -// CHECK-NEXT: store ptr %18, ptr %20, align 8 -// CHECK-NEXT: %21 = getelementptr inbounds { ptr, i64, %"{{.*}}/runtime/internal/runtime.eface" }, ptr %19, i32 0, i32 1 -// CHECK-NEXT: store i64 0, ptr %21, align 8 -// CHECK-NEXT: %22 = getelementptr inbounds { ptr, i64, %"{{.*}}/runtime/internal/runtime.eface" }, ptr %19, i32 0, i32 2 -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.eface" %0, ptr %22, align 8 -// CHECK-NEXT: store ptr %19, ptr %13, align 8 +// CHECK-NEXT: %21 = load i64, ptr %15, align 8 +// CHECK-NEXT: %22 = or i64 %21, 1 +// CHECK-NEXT: store i64 %22, ptr %15, align 8 +// CHECK-NEXT: %23 = load ptr, ptr %18, align 8 +// CHECK-NEXT: %24 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 32) +// CHECK-NEXT: %25 = getelementptr inbounds { ptr, i64, %"{{.*}}/runtime/internal/runtime.eface" }, ptr %24, i32 0, i32 0 +// CHECK-NEXT: store ptr %23, ptr %25, align 8 +// CHECK-NEXT: %26 = getelementptr inbounds { ptr, i64, %"{{.*}}/runtime/internal/runtime.eface" }, ptr %24, i32 0, i32 1 +// CHECK-NEXT: store i64 0, ptr %26, align 8 +// CHECK-NEXT: %27 = getelementptr inbounds { ptr, i64, %"{{.*}}/runtime/internal/runtime.eface" }, ptr %24, i32 0, i32 2 +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.eface" %1, ptr %27, align 8 +// CHECK-NEXT: store ptr %24, ptr %18, align 8 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 19 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: br label %_llgo_2 @@ -51,54 +59,56 @@ package main // CHECK-NEXT: _llgo_2: ; preds = %_llgo_1, %_llgo_4 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @1, i64 3 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.End", %_llgo_8), ptr %12, align 8 +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.End", %_llgo_8), ptr %17, align 8 // CHECK-NEXT: br label %_llgo_5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_6 // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_0 -// CHECK-NEXT: br i1 %2, label %_llgo_1, label %_llgo_2 +// CHECK-NEXT: br i1 %3, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_7, %_llgo_2 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.End", %_llgo_6), ptr %11, align 8 -// CHECK-NEXT: %23 = load i64, ptr %10, align 8 -// CHECK-NEXT: %24 = and i64 %23, 1 -// CHECK-NEXT: %25 = icmp ne i64 %24, 0 -// CHECK-NEXT: br i1 %25, label %_llgo_9, label %_llgo_10 +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.End", %_llgo_6), ptr %16, align 8 +// CHECK-NEXT: %28 = load i64, ptr %15, align 8 +// CHECK-NEXT: %29 = and i64 %28, 1 +// CHECK-NEXT: %30 = icmp ne i64 %29, 0 +// CHECK-NEXT: br i1 %30, label %_llgo_9, label %_llgo_10 // CHECK-EMPTY: // CHECK-NEXT: _llgo_6: ; preds = %_llgo_7, %_llgo_10 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Rethrow"(ptr %3) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Rethrow"(ptr %4) // CHECK-NEXT: br label %_llgo_3 // CHECK-EMPTY: // CHECK-NEXT: _llgo_7: ; preds = %_llgo_0 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.End", %_llgo_6), ptr %12, align 8 -// CHECK-NEXT: %26 = load ptr, ptr %11, align 8 -// CHECK-NEXT: indirectbr ptr %26, [label %_llgo_6, label %_llgo_5] +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.End", %_llgo_6), ptr %17, align 8 +// CHECK-NEXT: %31 = load ptr, ptr %16, align 8 +// CHECK-NEXT: indirectbr ptr %31, [label %_llgo_6, label %_llgo_5] // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_10 // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_9: ; preds = %_llgo_5 -// CHECK-NEXT: %27 = load ptr, ptr %13, align 8 -// CHECK-NEXT: %28 = icmp ne ptr %27, null -// CHECK-NEXT: br i1 %28, label %_llgo_11, label %_llgo_12 +// CHECK-NEXT: %32 = load ptr, ptr %18, align 8 +// CHECK-NEXT: %33 = icmp ne ptr %32, null +// CHECK-NEXT: br i1 %33, label %_llgo_11, label %_llgo_12 // CHECK-EMPTY: // CHECK-NEXT: _llgo_10: ; preds = %_llgo_12, %_llgo_5 -// CHECK-NEXT: %29 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %5, align 8 -// CHECK-NEXT: %30 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %29, 2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %30) -// CHECK-NEXT: %31 = load ptr, ptr %12, align 8 -// CHECK-NEXT: indirectbr ptr %31, [label %_llgo_6, label %_llgo_8] +// CHECK-NEXT: %34 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %7, align 8 +// CHECK-NEXT: %35 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %34, 2 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %35) +// CHECK-NEXT: %36 = load ptr, ptr %17, align 8 +// CHECK-NEXT: indirectbr ptr %36, [label %_llgo_6, label %_llgo_8] // CHECK-EMPTY: // CHECK-NEXT: _llgo_11: ; preds = %_llgo_9 -// CHECK-NEXT: %32 = load ptr, ptr %13, align 8 -// CHECK-NEXT: %33 = load { ptr, i64, %"{{.*}}/runtime/internal/runtime.eface" }, ptr %32, align 8 -// CHECK-NEXT: %34 = extractvalue { ptr, i64, %"{{.*}}/runtime/internal/runtime.eface" } %33, 0 -// CHECK-NEXT: store ptr %34, ptr %13, align 8 -// CHECK-NEXT: %35 = extractvalue { ptr, i64, %"{{.*}}/runtime/internal/runtime.eface" } %33, 2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.FreeDeferNode"(ptr %32) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %35) +// CHECK-NEXT: %37 = load ptr, ptr %18, align 8 +// CHECK-NEXT: %38 = load { ptr, i64, %"{{.*}}/runtime/internal/runtime.eface" }, ptr %37, align 8 +// CHECK-NEXT: %39 = extractvalue { ptr, i64, %"{{.*}}/runtime/internal/runtime.eface" } %38, 0 +// CHECK-NEXT: store ptr %39, ptr %18, align 8 +// CHECK-NEXT: %40 = extractvalue { ptr, i64, %"{{.*}}/runtime/internal/runtime.eface" } %38, 2 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.FreeDeferNode"(ptr %37) +// CHECK-NEXT: %41 = call ptr @llvm.frameaddress.p0(i32 0) +// CHECK-NEXT: %42 = call ptr @"{{.*}}/runtime/internal/runtime.StartRecoverFrame"(ptr %41) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %40) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_12: ; preds = %_llgo_9 @@ -136,51 +146,61 @@ func main() { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %0 = call ptr @"{{.*}}/runtime/internal/runtime.GetThreadDefer"() // CHECK-NEXT: %1 = alloca i8, i64 {{.*}}, align 1 -// CHECK-NEXT: %2 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 48) -// CHECK-NEXT: %3 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %2, i32 0, i32 0 -// CHECK-NEXT: store ptr %1, ptr %3, align 8 -// CHECK-NEXT: %4 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %2, i32 0, i32 1 -// CHECK-NEXT: store i64 0, ptr %4, align 8 -// CHECK-NEXT: %5 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %2, i32 0, i32 2 -// CHECK-NEXT: store ptr %0, ptr %5, align 8 -// CHECK-NEXT: %6 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %2, i32 0, i32 3 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.main", %_llgo_2), ptr %6, align 8 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %2) -// CHECK-NEXT: %7 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %2, i32 0, i32 1 -// CHECK-NEXT: %8 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %2, i32 0, i32 3 -// CHECK-NEXT: %9 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %2, i32 0, i32 4 -// CHECK-NEXT: %10 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %2, i32 0, i32 5 -// CHECK-NEXT: store ptr null, ptr %10, align 8 -// CHECK-NEXT: %11 = call i32 @{{.*}}sigsetjmp(ptr %1, i32 0) -// CHECK-NEXT: %12 = icmp eq i32 %11, 0 -// CHECK-NEXT: br i1 %12, label %_llgo_4, label %_llgo_5 +// CHECK-NEXT: %2 = call ptr @llvm.frameaddress.p0(i32 0) +// CHECK-NEXT: %3 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 56) +// CHECK-NEXT: %4 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 0 +// CHECK-NEXT: store ptr %1, ptr %4, align 8 +// CHECK-NEXT: %5 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 1 +// CHECK-NEXT: store i64 0, ptr %5, align 8 +// CHECK-NEXT: %6 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 2 +// CHECK-NEXT: store ptr %0, ptr %6, align 8 +// CHECK-NEXT: %7 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 3 +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.main", %_llgo_2), ptr %7, align 8 +// CHECK-NEXT: %8 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 4 +// CHECK-NEXT: store ptr null, ptr %8, align 8 +// CHECK-NEXT: %9 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 5 +// CHECK-NEXT: store ptr null, ptr %9, align 8 +// CHECK-NEXT: %10 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 6 +// CHECK-NEXT: store ptr %2, ptr %10, align 8 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %3) +// CHECK-NEXT: %11 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 1 +// CHECK-NEXT: %12 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 3 +// CHECK-NEXT: %13 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 4 +// CHECK-NEXT: %14 = getelementptr inbounds %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, i32 0, i32 5 +// CHECK-NEXT: store ptr null, ptr %14, align 8 +// CHECK-NEXT: %15 = call i32 @{{.*}}sigsetjmp(ptr %1, i32 0) +// CHECK-NEXT: %16 = icmp eq i32 %15, 0 +// CHECK-NEXT: br i1 %16, label %_llgo_4, label %_llgo_5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_3 // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_5 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.main", %_llgo_3), ptr %8, align 8 -// CHECK-NEXT: %13 = load i64, ptr %7, align 8 +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.main", %_llgo_3), ptr %12, align 8 +// CHECK-NEXT: %17 = load i64, ptr %11, align 8 +// CHECK-NEXT: %18 = call ptr @llvm.frameaddress.p0(i32 0) +// CHECK-NEXT: %19 = call ptr @"{{.*}}/runtime/internal/runtime.StartRecoverFrame"(ptr %18) // CHECK-NEXT: call void @"{{.*}}/cl/_testgo/recoverthenpanic.End"() -// CHECK-NEXT: %14 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %2, align 8 -// CHECK-NEXT: %15 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %14, 2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %15) -// CHECK-NEXT: %16 = load ptr, ptr %9, align 8 -// CHECK-NEXT: indirectbr ptr %16, [label %_llgo_3] +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.EndRecoverFrame"(ptr %19) +// CHECK-NEXT: %20 = load %"{{.*}}/runtime/internal/runtime.Defer", ptr %3, align 8 +// CHECK-NEXT: %21 = extractvalue %"{{.*}}/runtime/internal/runtime.Defer" %20, 2 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.SetThreadDefer"(ptr %21) +// CHECK-NEXT: %22 = load ptr, ptr %13, align 8 +// CHECK-NEXT: indirectbr ptr %22, [label %_llgo_3] // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_5, %_llgo_2 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Rethrow"(ptr %0) // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_0 -// CHECK-NEXT: %17 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 13 }, ptr %17, align 8 -// CHECK-NEXT: %18 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %17, 1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %18) +// CHECK-NEXT: %23 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 13 }, ptr %23, align 8 +// CHECK-NEXT: %24 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %23, 1 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %24) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_0 -// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.main", %_llgo_3), ptr %9, align 8 -// CHECK-NEXT: %19 = load ptr, ptr %8, align 8 -// CHECK-NEXT: indirectbr ptr %19, [label %_llgo_3, label %_llgo_2] +// CHECK-NEXT: store ptr blockaddress(@"{{.*}}/cl/_testgo/recoverthenpanic.main", %_llgo_3), ptr %13, align 8 +// CHECK-NEXT: %25 = load ptr, ptr %12, align 8 +// CHECK-NEXT: indirectbr ptr %25, [label %_llgo_3, label %_llgo_2] // CHECK-NEXT: } diff --git a/cl/_testgo/reflect/in.go b/cl/_testgo/reflect/in.go index fa7b800830..6a860eaaa2 100644 --- a/cl/_testgo/reflect/in.go +++ b/cl/_testgo/reflect/in.go @@ -7,7 +7,6 @@ import ( ) // CHECK: @0 = private unnamed_addr constant [11 x i8] c"call.method", align 1 -// CHECK: @2 = private unnamed_addr constant [3 x i8] c"int", align 1 // CHECK: @6 = private unnamed_addr constant [7 x i8] c"closure", align 1 // CHECK: @7 = private unnamed_addr constant [5 x i8] c"error", align 1 // CHECK: @9 = private unnamed_addr constant [12 x i8] c"call.closure", align 1 @@ -724,7 +723,8 @@ func callMethod() { // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %22, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %37 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %22, ptr @_llgo_int, ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %37) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testgo/tpinst/main.go b/cl/_testgo/tpinst/main.go index 9ceeee8ef5..5a79eedd86 100644 --- a/cl/_testgo/tpinst/main.go +++ b/cl/_testgo/tpinst/main.go @@ -1,9 +1,7 @@ // LITTEST package main -// CHECK: @6 = private unnamed_addr constant [5 x i8] c"value", align 1 // CHECK: @9 = private unnamed_addr constant [5 x i8] c"error", align 1 -// CHECK: @16 = private unnamed_addr constant [22 x i8] c"interface{value() int}", align 1 type M[T interface{}] struct { v T @@ -99,7 +97,8 @@ type I[T interface{}] interface { // CHECK-NEXT: br i1 %51, label %_llgo_5, label %_llgo_6 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_4 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %34, %"{{.*}}/runtime/internal/runtime.String" { ptr @16, i64 22 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @6, i64 5 }) +// CHECK-NEXT: %52 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %34, ptr @"{{.*}}/cl/_testgo/tpinst.iface$2sV9fFeqOv1SzesvwIdhTqCFzDT8ZX5buKUSAoHNSww", ptr @"_llgo_{{.*}}/cl/_testgo/tpinst.I[int]") +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %52) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testrt/any/in.go b/cl/_testrt/any/in.go index c498abc180..44210a0cb3 100644 --- a/cl/_testrt/any/in.go +++ b/cl/_testrt/any/in.go @@ -5,10 +5,8 @@ import ( "github.com/goplus/lib/c" ) -// CHECK: @1 = private unnamed_addr constant [29 x i8] c"*github.com/goplus/lib/c.Char", align 1 -// CHECK: @2 = private unnamed_addr constant [3 x i8] c"int", align 1 -// CHECK: @3 = private unnamed_addr constant [7 x i8] c"%s %d\0A\00", align 1 -// CHECK: @4 = private unnamed_addr constant [6 x i8] c"Hello\00", align 1 +// CHECK: @4 = private unnamed_addr constant [7 x i8] c"%s %d\0A\00", align 1 +// CHECK: @5 = private unnamed_addr constant [6 x i8] c"Hello\00", align 1 // CHECK-LABEL: define ptr @"{{.*}}/cl/_testrt/any.hi"(%"{{.*}}/runtime/internal/runtime.eface" %0){{.*}} { // CHECK-NEXT: _llgo_0: @@ -21,7 +19,8 @@ import ( // CHECK-NEXT: ret ptr %3 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @1, i64 29 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %4 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %1, ptr @"*_llgo_int8", ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %4) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -42,7 +41,8 @@ func hi(a any) *c.Char { // CHECK-NEXT: ret i64 %5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 3 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %6 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %1, ptr @_llgo_int, ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %6) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -69,12 +69,12 @@ func main() { // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/any.main"(){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %0 = call ptr @"{{.*}}/cl/_testrt/any.hi"(%"{{.*}}/runtime/internal/runtime.eface" { ptr @"*_llgo_int8", ptr @4 }) +// CHECK-NEXT: %0 = call ptr @"{{.*}}/cl/_testrt/any.hi"(%"{{.*}}/runtime/internal/runtime.eface" { ptr @"*_llgo_int8", ptr @5 }) // CHECK-NEXT: %1 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 8) // CHECK-NEXT: store i64 100, ptr %1, align 8 // CHECK-NEXT: %2 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_int, ptr undef }, ptr %1, 1 // CHECK-NEXT: %3 = call i64 @"{{.*}}/cl/_testrt/any.incVal"(%"{{.*}}/runtime/internal/runtime.eface" %2) -// CHECK-NEXT: %4 = call i32 (ptr, ...) @printf(ptr @3, ptr %0, i64 %3) +// CHECK-NEXT: %4 = call i32 (ptr, ...) @printf(ptr @4, ptr %0, i64 %3) // CHECK-NEXT: ret void // CHECK-NEXT: } @@ -84,6 +84,12 @@ func main() { // CHECK-NEXT: ret i1 %3 // CHECK-NEXT: } +// CHECK-LABEL: define linkonce i1 @"__llgo_stub.{{.*}}/runtime/internal/runtime.nilinterequal"(ptr %0, ptr %1, ptr %2){{.*}} { +// CHECK-NEXT: _llgo_0: +// CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.nilinterequal"(ptr %1, ptr %2) +// CHECK-NEXT: ret i1 %3 +// CHECK-NEXT: } + // CHECK-LABEL: define linkonce i1 @"__llgo_stub.{{.*}}/runtime/internal/runtime.memequal64"(ptr %0, ptr %1, ptr %2){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.memequal64"(ptr %1, ptr %2) diff --git a/cl/_testrt/funcdecl/in.go b/cl/_testrt/funcdecl/in.go index 45f6f0cc67..b7232d9c5c 100644 --- a/cl/_testrt/funcdecl/in.go +++ b/cl/_testrt/funcdecl/in.go @@ -5,9 +5,8 @@ import ( "unsafe" ) -// CHECK: @4 = private unnamed_addr constant [39 x i8] c"struct{$f func(); $data unsafe.Pointer}", align 1 -// CHECK: @5 = private unnamed_addr constant [4 x i8] c"demo", align 1 -// CHECK: @6 = private unnamed_addr constant [5 x i8] c"hello", align 1 +// CHECK: @6 = private unnamed_addr constant [4 x i8] c"demo", align 1 +// CHECK: @7 = private unnamed_addr constant [5 x i8] c"hello", align 1 // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/funcdecl.check"({ ptr, ptr } %0){{.*}} { // CHECK-NEXT: _llgo_0: @@ -29,36 +28,38 @@ import ( // CHECK-NEXT: br i1 %10, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %5, %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 39 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %11 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %5, ptr @"_llgo_closure$b7Su1hWaFih-M0M9hMk6nO_RD1K_GQu5WjIXQp6Q2e8", ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %11) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_1 -// CHECK-NEXT: %11 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %4, 1 -// CHECK-NEXT: %12 = load { ptr, ptr }, ptr %11, align 8 +// CHECK-NEXT: %12 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %4, 1 +// CHECK-NEXT: %13 = load { ptr, ptr }, ptr %12, align 8 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintEface"(%"{{.*}}/runtime/internal/runtime.eface" %2) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintEface"(%"{{.*}}/runtime/internal/runtime.eface" %4) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: %13 = extractvalue { ptr, ptr } %0, 0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr %13) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: %14 = extractvalue { ptr, ptr } %8, 0 +// CHECK-NEXT: %14 = extractvalue { ptr, ptr } %0, 0 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr %14) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: %15 = extractvalue { ptr, ptr } %12, 0 +// CHECK-NEXT: %15 = extractvalue { ptr, ptr } %8, 0 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr %15) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) +// CHECK-NEXT: %16 = extractvalue { ptr, ptr } %13, 0 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr %16) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintPointer"(ptr @"{{.*}}/cl/_testrt/funcdecl.demo") // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: %16 = call ptr @"{{.*}}/cl/_testrt/funcdecl.closurePtr"(%"{{.*}}/runtime/internal/runtime.eface" %2) -// CHECK-NEXT: %17 = call ptr @"{{.*}}/cl/_testrt/funcdecl.closurePtr"(%"{{.*}}/runtime/internal/runtime.eface" %4) -// CHECK-NEXT: %18 = icmp eq ptr %16, %17 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %18) +// CHECK-NEXT: %17 = call ptr @"{{.*}}/cl/_testrt/funcdecl.closurePtr"(%"{{.*}}/runtime/internal/runtime.eface" %2) +// CHECK-NEXT: %18 = call ptr @"{{.*}}/cl/_testrt/funcdecl.closurePtr"(%"{{.*}}/runtime/internal/runtime.eface" %4) +// CHECK-NEXT: %19 = icmp eq ptr %17, %18 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %19) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %9, %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 39 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %20 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %9, ptr @"_llgo_closure$b7Su1hWaFih-M0M9hMk6nO_RD1K_GQu5WjIXQp6Q2e8", ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %20) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -96,7 +97,7 @@ type rtype struct { // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/funcdecl.demo"(){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 4 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @6, i64 4 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-NEXT: } @@ -120,7 +121,7 @@ func demo() { // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/funcdecl.main"(){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @6, i64 5 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @7, i64 5 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: call void @"{{.*}}/cl/_testrt/funcdecl.check"({ ptr, ptr } { ptr @"__llgo_stub.{{.*}}/cl/_testrt/funcdecl.demo", ptr null }) // CHECK-NEXT: ret void @@ -136,3 +137,9 @@ func main() { // CHECK-NEXT: tail call void @"{{.*}}/cl/_testrt/funcdecl.demo"() // CHECK-NEXT: ret void // CHECK-NEXT: } + +// CHECK-LABEL: define linkonce i1 @"__llgo_stub.{{.*}}/runtime/internal/runtime.nilinterequal"(ptr %0, ptr %1, ptr %2){{.*}} { +// CHECK-NEXT: _llgo_0: +// CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.nilinterequal"(ptr %1, ptr %2) +// CHECK-NEXT: ret i1 %3 +// CHECK-NEXT: } diff --git a/cl/_testrt/makemap/in.go b/cl/_testrt/makemap/in.go index 69fce2a770..ee4861d87c 100644 --- a/cl/_testrt/makemap/in.go +++ b/cl/_testrt/makemap/in.go @@ -8,9 +8,6 @@ package main // CHECK: @22 = private unnamed_addr constant [2 x i8] c"go", align 1 // CHECK: @23 = private unnamed_addr constant [7 x i8] c"bad key", align 1 // CHECK: @24 = private unnamed_addr constant [7 x i8] c"bad len", align 1 -// CHECK: @32 = private unnamed_addr constant [44 x i8] c"{{.*}}/cl/_testrt/makemap.N1", align 1 -// CHECK: @39 = private unnamed_addr constant [43 x i8] c"{{.*}}/cl/_testrt/makemap.K", align 1 -// CHECK: @42 = private unnamed_addr constant [44 x i8] c"{{.*}}/cl/_testrt/makemap.K2", align 1 func main() { make1() @@ -377,7 +374,8 @@ type N1 [1]int // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %39, %"{{.*}}/runtime/internal/runtime.String" { ptr @32, i64 44 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %54 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %39, ptr @"_llgo_{{.*}}/cl/_testrt/makemap.N1", ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %54) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -514,7 +512,8 @@ type K2 [1]*N // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %39, %"{{.*}}/runtime/internal/runtime.String" { ptr @39, i64 43 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %56 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %39, ptr @"_llgo_{{.*}}/cl/_testrt/makemap.K", ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %56) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -646,7 +645,8 @@ func make3() { // CHECK-NEXT: br label %_llgo_1 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_2 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %43, %"{{.*}}/runtime/internal/runtime.String" { ptr @42, i64 44 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %61 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %43, ptr @"_llgo_{{.*}}/cl/_testrt/makemap.K2", ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %61) // CHECK-NEXT: unreachable // CHECK-NEXT: } diff --git a/cl/_testrt/slice2array/in.go b/cl/_testrt/slice2array/in.go index 78af725c11..5c2a7b1438 100644 --- a/cl/_testrt/slice2array/in.go +++ b/cl/_testrt/slice2array/in.go @@ -1,6 +1,26 @@ // LITTEST package main +func main() { + array := [4]byte{1, 2, 3, 4} + ptr := (*[4]byte)(array[:]) + println(array == *ptr) + println(*(*[2]byte)(array[:]) == [2]byte{1, 2}) +} + +// CHECK-LABEL: define void @"{{.*}}/cl/_testrt/slice2array.init"(){{.*}} { +// CHECK-NEXT: _llgo_0: +// CHECK-NEXT: %0 = load i1, ptr @"{{.*}}/cl/_testrt/slice2array.init$guard", align 1 +// CHECK-NEXT: br i1 %0, label %_llgo_2, label %_llgo_1 +// CHECK-EMPTY: +// CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 +// CHECK-NEXT: store i1 true, ptr @"{{.*}}/cl/_testrt/slice2array.init$guard", align 1 +// CHECK-NEXT: br label %_llgo_2 +// CHECK-EMPTY: +// CHECK-NEXT: _llgo_2: ; preds = %_llgo_1, %_llgo_0 +// CHECK-NEXT: ret void +// CHECK-NEXT: } + // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/slice2array.main"(){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %0 = call ptr @"{{.*}}/runtime/internal/runtime.AllocZ"(i64 4) @@ -80,9 +100,3 @@ package main // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-NEXT: } -func main() { - array := [4]byte{1, 2, 3, 4} - ptr := (*[4]byte)(array[:]) - println(array == *ptr) - println(*(*[2]byte)(array[:]) == [2]byte{1, 2}) -} diff --git a/cl/_testrt/tpabi/in.go b/cl/_testrt/tpabi/in.go index ca04d5219b..718960b4d4 100644 --- a/cl/_testrt/tpabi/in.go +++ b/cl/_testrt/tpabi/in.go @@ -5,8 +5,8 @@ import "github.com/goplus/lib/c" // CHECK: @0 = private unnamed_addr constant [1 x i8] c"a", align 1 // CHECK: @5 = private unnamed_addr constant [4 x i8] c"Info", align 1 -// CHECK: @10 = private unnamed_addr constant [54 x i8] c"{{.*}}/cl/_testrt/tpabi.T[string, int]", align 1 // CHECK: @11 = private unnamed_addr constant [5 x i8] c"hello", align 1 +// CHECK: @13 = private unnamed_addr constant [54 x i8] c"{{.*}}/cl/_testrt/tpabi.T[string, int]", align 1 // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/tpabi.init"(){{.*}} { // CHECK-NEXT: _llgo_0: @@ -102,7 +102,8 @@ func (t *K[N]) Advance(n int) *K[N] { // CHECK-NEXT: ret void // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %6, %"{{.*}}/runtime/internal/runtime.String" { ptr @10, i64 54 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %32 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %6, ptr @"_llgo_{{.*}}/cl/_testrt/tpabi.T[string,int]", ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %32) // CHECK-NEXT: unreachable // CHECK-NEXT: } @@ -149,7 +150,7 @@ func main() { // CHECK-LABEL: define linkonce void @"{{.*}}/cl/_testrt/tpabi.(*T[string,int]).Info"(ptr %0){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %1 = icmp eq ptr %0, null -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @10, i64 54 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 4 }) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicWrapNilPointer"(i1 %1, %"{{.*}}/runtime/internal/runtime.String" { ptr @13, i64 54 }, %"{{.*}}/runtime/internal/runtime.String" { ptr @5, i64 4 }) // CHECK-NEXT: %2 = load %"{{.*}}/cl/_testrt/tpabi.T[string,int]", ptr %0, align 8 // CHECK-NEXT: call void @"{{.*}}/cl/_testrt/tpabi.T[string,int].Info"(%"{{.*}}/cl/_testrt/tpabi.T[string,int]" %2) // CHECK-NEXT: ret void @@ -179,6 +180,12 @@ func main() { // CHECK-NEXT: ret void // CHECK-NEXT: } +// CHECK-LABEL: define linkonce i1 @"__llgo_stub.{{.*}}/runtime/internal/runtime.nilinterequal"(ptr %0, ptr %1, ptr %2){{.*}} { +// CHECK-NEXT: _llgo_0: +// CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.nilinterequal"(ptr %1, ptr %2) +// CHECK-NEXT: ret i1 %3 +// CHECK-NEXT: } + // CHECK-LABEL: define linkonce i1 @"__llgo_stub.{{.*}}/runtime/internal/runtime.interequal"(ptr %0, ptr %1, ptr %2){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.interequal"(ptr %1, ptr %2) diff --git a/cl/_testrt/typed/in.go b/cl/_testrt/typed/in.go index 1444e36c60..3f66fb6b1b 100644 --- a/cl/_testrt/typed/in.go +++ b/cl/_testrt/typed/in.go @@ -2,7 +2,6 @@ package main // CHECK: @0 = private unnamed_addr constant [5 x i8] c"hello", align 1 -// CHECK: @3 = private unnamed_addr constant [41 x i8] c"{{.*}}/cl/_testrt/typed.T", align 1 // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/typed.init"(){{.*}} { // CHECK-NEXT: _llgo_0: @@ -39,67 +38,68 @@ type A [2]int // CHECK-NEXT: br i1 %7, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PanicTypeAssert"(ptr %2, %"{{.*}}/runtime/internal/runtime.String" { ptr @3, i64 41 }, %"{{.*}}/runtime/internal/runtime.String" zeroinitializer) +// CHECK-NEXT: %8 = call %"{{.*}}/runtime/internal/runtime.eface" @"{{.*}}/runtime/internal/runtime.TypeAssertError"(ptr %2, ptr @"_llgo_{{.*}}/cl/_testrt/typed.T", ptr @_llgo_any) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %8) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_1 -// CHECK-NEXT: %8 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %1, 1 -// CHECK-NEXT: %9 = load %"{{.*}}/runtime/internal/runtime.String", ptr %8, align 8 -// CHECK-NEXT: %10 = insertvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } undef, %"{{.*}}/runtime/internal/runtime.String" %9, 0 -// CHECK-NEXT: %11 = insertvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %10, i1 true, 1 +// CHECK-NEXT: %9 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %1, 1 +// CHECK-NEXT: %10 = load %"{{.*}}/runtime/internal/runtime.String", ptr %9, align 8 +// CHECK-NEXT: %11 = insertvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } undef, %"{{.*}}/runtime/internal/runtime.String" %10, 0 +// CHECK-NEXT: %12 = insertvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %11, i1 true, 1 // CHECK-NEXT: br label %_llgo_5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_1 // CHECK-NEXT: br label %_llgo_5 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_4, %_llgo_3 -// CHECK-NEXT: %12 = phi { %"{{.*}}/runtime/internal/runtime.String", i1 } [ %11, %_llgo_3 ], [ zeroinitializer, %_llgo_4 ] -// CHECK-NEXT: %13 = extractvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %12, 0 -// CHECK-NEXT: %14 = extractvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %12, 1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" %13) +// CHECK-NEXT: %13 = phi { %"{{.*}}/runtime/internal/runtime.String", i1 } [ %12, %_llgo_3 ], [ zeroinitializer, %_llgo_4 ] +// CHECK-NEXT: %14 = extractvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %13, 0 +// CHECK-NEXT: %15 = extractvalue { %"{{.*}}/runtime/internal/runtime.String", i1 } %13, 1 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" %14) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %14) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %15) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: %15 = alloca [2 x i64], align 8 -// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %15, i8 0, i64 16, i1 false) -// CHECK-NEXT: %16 = getelementptr inbounds i64, ptr %15, i64 0 -// CHECK-NEXT: %17 = getelementptr inbounds i64, ptr %15, i64 1 -// CHECK-NEXT: store i64 1, ptr %16, align 8 -// CHECK-NEXT: store i64 2, ptr %17, align 8 -// CHECK-NEXT: %18 = load [2 x i64], ptr %15, align 8 -// CHECK-NEXT: %19 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store [2 x i64] %18, ptr %19, align 8 -// CHECK-NEXT: %20 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @"_llgo_{{.*}}/cl/_testrt/typed.A", ptr undef }, ptr %19, 1 -// CHECK-NEXT: %21 = alloca [2 x i64], align 8 -// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %21, i8 0, i64 16, i1 false) -// CHECK-NEXT: %22 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %20, 0 -// CHECK-NEXT: %23 = icmp eq ptr %22, @"_llgo_{{.*}}/cl/_testrt/typed.A" -// CHECK-NEXT: br i1 %23, label %_llgo_6, label %_llgo_7 +// CHECK-NEXT: %16 = alloca [2 x i64], align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %16, i8 0, i64 16, i1 false) +// CHECK-NEXT: %17 = getelementptr inbounds i64, ptr %16, i64 0 +// CHECK-NEXT: %18 = getelementptr inbounds i64, ptr %16, i64 1 +// CHECK-NEXT: store i64 1, ptr %17, align 8 +// CHECK-NEXT: store i64 2, ptr %18, align 8 +// CHECK-NEXT: %19 = load [2 x i64], ptr %16, align 8 +// CHECK-NEXT: %20 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: store [2 x i64] %19, ptr %20, align 8 +// CHECK-NEXT: %21 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @"_llgo_{{.*}}/cl/_testrt/typed.A", ptr undef }, ptr %20, 1 +// CHECK-NEXT: %22 = alloca [2 x i64], align 8 +// CHECK-NEXT: call void @llvm.memset.p0.i64(ptr %22, i8 0, i64 16, i1 false) +// CHECK-NEXT: %23 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %21, 0 +// CHECK-NEXT: %24 = icmp eq ptr %23, @"_llgo_{{.*}}/cl/_testrt/typed.A" +// CHECK-NEXT: br i1 %24, label %_llgo_6, label %_llgo_7 // CHECK-EMPTY: // CHECK-NEXT: _llgo_6: ; preds = %_llgo_5 -// CHECK-NEXT: %24 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %20, 1 -// CHECK-NEXT: %25 = load [2 x i64], ptr %24, align 8 -// CHECK-NEXT: %26 = insertvalue { [2 x i64], i1 } undef, [2 x i64] %25, 0 -// CHECK-NEXT: %27 = insertvalue { [2 x i64], i1 } %26, i1 true, 1 +// CHECK-NEXT: %25 = extractvalue %"{{.*}}/runtime/internal/runtime.eface" %21, 1 +// CHECK-NEXT: %26 = load [2 x i64], ptr %25, align 8 +// CHECK-NEXT: %27 = insertvalue { [2 x i64], i1 } undef, [2 x i64] %26, 0 +// CHECK-NEXT: %28 = insertvalue { [2 x i64], i1 } %27, i1 true, 1 // CHECK-NEXT: br label %_llgo_8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_7: ; preds = %_llgo_5 // CHECK-NEXT: br label %_llgo_8 // CHECK-EMPTY: // CHECK-NEXT: _llgo_8: ; preds = %_llgo_7, %_llgo_6 -// CHECK-NEXT: %28 = phi { [2 x i64], i1 } [ %27, %_llgo_6 ], [ zeroinitializer, %_llgo_7 ] -// CHECK-NEXT: %29 = extractvalue { [2 x i64], i1 } %28, 0 -// CHECK-NEXT: store [2 x i64] %29, ptr %21, align 8 -// CHECK-NEXT: %30 = extractvalue { [2 x i64], i1 } %28, 1 -// CHECK-NEXT: %31 = getelementptr inbounds i64, ptr %21, i64 0 -// CHECK-NEXT: %32 = load i64, ptr %31, align 8 -// CHECK-NEXT: %33 = getelementptr inbounds i64, ptr %21, i64 1 -// CHECK-NEXT: %34 = load i64, ptr %33, align 8 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 %32) +// CHECK-NEXT: %29 = phi { [2 x i64], i1 } [ %28, %_llgo_6 ], [ zeroinitializer, %_llgo_7 ] +// CHECK-NEXT: %30 = extractvalue { [2 x i64], i1 } %29, 0 +// CHECK-NEXT: store [2 x i64] %30, ptr %22, align 8 +// CHECK-NEXT: %31 = extractvalue { [2 x i64], i1 } %29, 1 +// CHECK-NEXT: %32 = getelementptr inbounds i64, ptr %22, i64 0 +// CHECK-NEXT: %33 = load i64, ptr %32, align 8 +// CHECK-NEXT: %34 = getelementptr inbounds i64, ptr %22, i64 1 +// CHECK-NEXT: %35 = load i64, ptr %34, align 8 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 %33) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 %34) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 %35) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %30) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintBool"(i1 %31) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) // CHECK-NEXT: ret void // CHECK-NEXT: } @@ -115,6 +115,12 @@ func main() { println(ar[0], ar[1], ok) } +// CHECK-LABEL: define linkonce i1 @"__llgo_stub.{{.*}}/runtime/internal/runtime.nilinterequal"(ptr %0, ptr %1, ptr %2){{.*}} { +// CHECK-NEXT: _llgo_0: +// CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.nilinterequal"(ptr %1, ptr %2) +// CHECK-NEXT: ret i1 %3 +// CHECK-NEXT: } + // CHECK-LABEL: define linkonce i1 @"__llgo_stub.{{.*}}/runtime/internal/runtime.memequal64"(ptr %0, ptr %1, ptr %2){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %3 = tail call i1 @"{{.*}}/runtime/internal/runtime.memequal64"(ptr %1, ptr %2) diff --git a/cl/caller_frame_test.go b/cl/caller_frame_test.go new file mode 100644 index 0000000000..69d570d9ee --- /dev/null +++ b/cl/caller_frame_test.go @@ -0,0 +1,822 @@ +//go:build !llgo +// +build !llgo + +package cl + +import ( + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/gogen/packages" + llssa "github.com/goplus/llgo/ssa" + gossa "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +func parseCallerFrameFile(t *testing.T, src string) *ast.File { + t.Helper() + file, err := parser.ParseFile(token.NewFileSet(), "caller_frame.go", src, 0) + if err != nil { + t.Fatal(err) + } + return file +} + +func TestFilesUseRuntimeCaller(t *testing.T) { + tests := []struct { + name string + src string + want bool + }{ + { + name: "runtime selector", + src: `package foo +import "runtime" +func f() { runtime.Caller(0) } +`, + want: true, + }, + { + name: "runtime alias", + src: `package foo +import rt "runtime" +func f() { rt.Callers(0, nil) } +`, + want: true, + }, + { + name: "runtime debug stack", + src: `package foo +import dbg "runtime/debug" +func f() { _ = dbg.Stack() } +`, + want: true, + }, + { + name: "dot import", + src: `package foo +import . "runtime" +func f() { Caller(0) } +`, + want: true, + }, + { + name: "runtime FuncForPC only", + src: `package foo +import "runtime" +func f() { _ = runtime.FuncForPC(0) } +`, + want: false, + }, + { + name: "blank import", + src: `package foo +import _ "runtime" +func f() {} +`, + want: false, + }, + { + name: "non caller runtime selector", + src: `package foo +import "runtime" +func f() { _ = runtime.GOOS } +`, + want: false, + }, + { + name: "caller name without runtime import", + src: `package foo +func f() { Caller(0) } +`, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := filesUseRuntimeCaller([]*ast.File{parseCallerFrameFile(t, tt.src)}); got != tt.want { + t.Fatalf("filesUseRuntimeCaller() = %v, want %v", got, tt.want) + } + }) + } + + badImport := &ast.File{ + Imports: []*ast.ImportSpec{{ + Path: &ast.BasicLit{Kind: token.STRING, Value: "runtime"}, + }}, + } + if filesUseRuntimeCaller([]*ast.File{badImport}) { + t.Fatal("invalid import literal should not enable caller frame tracking") + } +} + +func buildCallerFrameSSAPackage(t *testing.T, pkgPath, src string) (*gossa.Package, []*ast.File) { + t.Helper() + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "caller_frame_compile.go", src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + imp := packages.NewImporter(fset) + mode := gossa.SanityCheckFunctions | gossa.InstantiateGenerics + ssapkg, _, err := ssautil.BuildPackage( + &types.Config{Importer: imp}, + fset, + types.NewPackage(pkgPath, file.Name.Name), + files, + mode, + ) + if err != nil { + t.Fatal(err) + } + return ssapkg, files +} + +func newLLSSAProgForTarget(t *testing.T, target *llssa.Target) llssa.Program { + t.Helper() + prog := llssa.NewProgram(target) + prog.SetRuntime(func() *types.Package { + rt, err := importer.For("source", nil).Import(llssa.PkgRuntime) + if err != nil { + t.Fatal("load runtime failed:", err) + } + return rt + }) + if target != nil && target.GOARCH != "" { + prog.TypeSizes(types.SizesFor("gc", target.GOARCH)) + } + return prog +} + +func newRuntimeCallerAnalysis(pkg *gossa.Package) *runtimeCallerAnalysis { + funcs, trackable := collectRuntimeCallerFunctions(pkg) + return &runtimeCallerAnalysis{ + pkg: pkg, + funcs: funcs, + trackable: trackable, + callsites: collectRuntimeCallerCallsites(funcs), + memo: make(map[*gossa.Function]bool), + visiting: make(map[*gossa.Function]bool), + } +} + +func TestRuntimeCallerPackageDetection(t *testing.T) { + ssapkg, _ := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" +import "runtime/debug" + +type callerIface interface { Call() } +type callerImpl struct{} +type workerIface interface { Work() } +type workerImpl struct{} + +func direct() { runtime.Caller(0) } +func indirect() { direct() } +func dynamic(f func()) { f() } +func dynamicCaller() { dynamic(direct) } +func (callerImpl) Call() { direct() } +func interfaceDispatch(c callerIface) { c.Call() } +func interfaceCaller(c callerIface) { interfaceDispatch(c) } +func closureLayer(next func()) func() { return func() { next() } } +func closureCaller() { closureLayer(closureLayer(direct))() } +func stack() { _ = debug.Stack() } +func anonOnly() { func() { runtime.Caller(0) }() } +func funcForPCOnly() { _ = runtime.FuncForPC(0) } +func leaf() {} +func callFunc(f func()) { f() } +func callFuncHot() { callFunc(leaf) } +func (workerImpl) Work() {} +func callWorker(w workerIface) { w.Work() } +func workerHot() { var w workerIface = workerImpl{}; callWorker(w) } +func plain() {} +`) + if !packageUsesRuntimeCaller(ssapkg) { + t.Fatal("package should report runtime caller usage") + } + if !fnUsesRuntimeCaller(ssapkg.Func("direct")) { + t.Fatal("direct runtime.Caller use should be detected") + } + if !fnUsesRuntimeCaller(ssapkg.Func("indirect")) { + t.Fatal("transitive runtime.Caller use should be detected") + } + if !fnUsesRuntimeCaller(ssapkg.Func("stack")) { + t.Fatal("runtime/debug.Stack use should be detected") + } + if !fnUsesRuntimeCaller(ssapkg.Func("anonOnly")) { + t.Fatal("runtime caller use in anonymous functions should be detected") + } + if fnUsesRuntimeCaller(ssapkg.Func("plain")) { + t.Fatal("plain function should not report runtime caller usage") + } + runtimeCallerFuncs := runtimeCallerFuncSet(ssapkg) + for _, name := range []string{"dynamic", "dynamicCaller", "interfaceDispatch", "interfaceCaller", "closureLayer", "closureCaller"} { + if !runtimeCallerFuncs[ssapkg.Func(name)] { + t.Fatalf("%s should be tracked because dynamic calls may reach runtime stack APIs", name) + } + } + for _, name := range []string{"leaf", "callFunc", "callFuncHot", "callWorker", "workerHot"} { + if runtimeCallerFuncs[ssapkg.Func(name)] { + t.Fatalf("%s should not be tracked when resolved dynamic targets do not reach runtime stack APIs", name) + } + } + if runtimeCallerFuncs[ssapkg.Func("funcForPCOnly")] { + t.Fatal("FuncForPC-only function should not need caller frame tracking") + } + if runtimeCallerFuncs[ssapkg.Func("plain")] { + t.Fatal("plain function should not be tracked") + } + + for _, name := range []string{"Caller", "Callers", "CallersFrames", "FuncForPC", "Stack"} { + if !isRuntimeCallerName(name) { + t.Fatalf("%s should be a runtime caller metadata function", name) + } + } + if isRuntimeCallerFrameName("FuncForPC") { + t.Fatal("FuncForPC should not require caller frame tracking") + } + if !isRuntimeCallerFrameName("Caller") { + t.Fatal("Caller should require caller frame tracking") + } + if isRuntimeCallerName("Version") { + t.Fatal("Version should not be a runtime caller metadata function") + } + + rtpkg, _ := buildCallerFrameSSAPackage(t, "github.com/goplus/llgo/runtime/internal/lib/runtime", `package runtime +func Caller(skip int) (uintptr, string, int, bool) { return 0, "", 0, false } +func FuncForPC(pc uintptr) uintptr { return 0 } +`) + if !isRuntimeCallerFunc(rtpkg.Func("Caller")) || !isRuntimeCallerLookupFunc(rtpkg.Func("Caller")) { + t.Fatal("LLGo runtime lib Caller should be treated as runtime.Caller") + } + if !isRuntimeCallerFunc(rtpkg.Func("FuncForPC")) { + t.Fatal("LLGo runtime lib FuncForPC should be treated as runtime metadata use") + } + if isRuntimeCallerFrameFunc(rtpkg.Func("FuncForPC")) { + t.Fatal("FuncForPC should not require caller frame tracking") + } + if isRuntimeCallerLookupFunc(rtpkg.Func("FuncForPC")) { + t.Fatal("FuncForPC should not consume caller lookup tokens") + } +} + +func TestRuntimeCallerAnalysisEdgeCases(t *testing.T) { + if fnUsesRuntimeCaller(nil) { + t.Fatal("nil function should not use runtime caller metadata") + } + if fnUsesRuntimeCaller(&gossa.Function{}) { + t.Fatal("function without a package should not use runtime caller metadata") + } + if runtimeCallerFuncSet(nil) != nil { + t.Fatal("nil package should have no runtime caller set") + } + if fnHasDirectRuntimeCaller(nil) { + t.Fatal("nil function should not have direct runtime caller use") + } + if functionBelongsToPackage(nil, nil) { + t.Fatal("nil function/package should not belong to a package") + } + if typeBelongsToPackage(types.Typ[types.Int], nil) { + t.Fatal("types should not belong to a nil package") + } + if isRuntimeCallerLookupFunc(nil) { + t.Fatal("nil function should not be a runtime caller lookup") + } + called := false + forEachCall(nil, func(*gossa.CallCommon) { + called = true + }) + if called { + t.Fatal("forEachCall should ignore nil functions") + } + + ssapkg, _ := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +type I interface { Call() } +type J interface { Call() } +type T struct{} + +func target() { runtime.Caller(0) } +func plain() {} +func call(fn func()) { fn() } +func callRuntime() { call(target) } +func (T) Call() { runtime.Caller(0) } +func viaStatic() { var i I = T{}; i.Call() } +func viaChange(j J) { var i I = j; i.Call() } +func viaParam(i I) { i.Call() } +func passInterface() { var i I = T{}; viaParam(i) } +`) + analysis := newRuntimeCallerAnalysis(ssapkg) + if analysis.fnMayReachRuntimeCaller(nil) { + t.Fatal("nil function should not reach runtime caller metadata") + } + if targets, ok := analysis.functionValueTargets(ssapkg.Func("callRuntime"), ssapkg.Func("target")); !ok || !targets[ssapkg.Func("target")] { + t.Fatal("static function value should resolve to its target") + } + if _, ok := analysis.functionValueTargets(ssapkg.Func("target"), nil); ok { + t.Fatal("nil function value should be unresolved") + } + if _, ok := analysis.functionParamTargets(ssapkg.Func("call"), 99); ok { + t.Fatal("out-of-range function argument should be unresolved") + } + callFn := ssapkg.Func("call") + callParam := callFn.Params[0] + callParams := callFn.Params + callFn.Params = nil + if _, ok := analysis.functionValueTargets(callFn, callParam); ok { + t.Fatal("function parameter missing from Params should be unresolved") + } + callFn.Params = callParams + + iface := ssapkg.Pkg.Scope().Lookup("I").Type().Underlying().(*types.Interface) + method := iface.Method(0) + if !analysis.fnMayReachRuntimeCaller(ssapkg.Func("viaStatic")) { + t.Fatal("static interface dispatch should reach runtime caller metadata") + } + if !analysis.fnMayReachRuntimeCaller(ssapkg.Func("viaChange")) { + t.Fatal("changed interface dispatch should conservatively reach runtime caller metadata") + } + if targets, ok := analysis.interfaceMethodTargets(ssapkg.Func("viaParam"), ssapkg.Func("viaParam").Params[0], method); !ok || len(targets) == 0 { + t.Fatal("interface parameter callsites should resolve concrete method targets") + } + analysis.callsites[ssapkg.Func("viaParam")] = []*gossa.CallCommon{{}} + if _, ok := analysis.interfaceMethodTargets(ssapkg.Func("viaParam"), ssapkg.Func("viaParam").Params[0], method); ok { + t.Fatal("out-of-range interface argument should be unresolved") + } + if _, ok := analysis.interfaceMethodTargets(ssapkg.Func("viaStatic"), nil, method); ok { + t.Fatal("nil interface receiver should be unresolved") + } + if _, ok := analysis.staticInterfaceMethodTargets(&gossa.ChangeInterface{}, method); ok { + t.Fatal("empty interface conversion should be unresolved") + } + viaParam := ssapkg.Func("viaParam") + interfaceParam := viaParam.Params[0] + interfaceParams := viaParam.Params + viaParam.Params = nil + if _, ok := analysis.interfaceMethodTargets(viaParam, interfaceParam, method); ok { + t.Fatal("interface parameter missing from Params should be unresolved") + } + viaParam.Params = interfaceParams + if _, ok := analysis.methodTargetsForType(nil, nil); ok { + t.Fatal("nil method lookup should be unresolved") + } + other := types.NewFunc(token.NoPos, ssapkg.Pkg, "Other", nil) + if _, ok := analysis.methodTargetsForType(ssapkg.Type("T").Type(), other); ok { + t.Fatal("missing interface method should be unresolved") + } + if idx, ok := parameterIndex(ssapkg.Func("target"), nil); ok || idx != 0 { + t.Fatal("nil parameter should not be found") + } + + methodOnlyPkg, _ := buildCallerFrameSSAPackage(t, "example.com/methodonly", `package methodonly +import "runtime" + +type T struct{} +func (T) Call() { runtime.Caller(0) } +var _ = T{} +`) + if runtimeCallerFuncSet(methodOnlyPkg) != nil { + t.Fatal("method-only runtime caller use should not mark top-level functions") + } +} + +func TestCallerFrameTrackingEligibility(t *testing.T) { + if (&context{}).shouldTrackCallerFrames() { + t.Fatal("missing compiler state should not track caller frames") + } + var nilContext *context + if nilContext.shouldTrackCallerFrames() { + t.Fatal("nil context should not track caller frames") + } + + tests := []struct { + name string + pkgPath string + track bool + targetName string + goarch string + want bool + }{ + {name: "enabled user package", pkgPath: "example.com/foo", track: true, want: true}, + {name: "disabled flag", pkgPath: "example.com/foo", want: false}, + {name: "named target", pkgPath: "example.com/foo", track: true, targetName: "esp32", want: false}, + {name: "wasm", pkgPath: "example.com/foo", track: true, goarch: "wasm", want: false}, + {name: "stdlib", pkgPath: "fmt", track: true, want: false}, + {name: "runtime", pkgPath: "runtime", track: true, want: false}, + {name: "llgo runtime", pkgPath: llssa.PkgRuntime, track: true, want: false}, + {name: "llgo runtime internal", pkgPath: "github.com/goplus/llgo/runtime/internal/foo", track: true, want: false}, + {name: "command line package", pkgPath: "command-line-arguments", track: true, want: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ssapkg, _ := buildCallerFrameSSAPackage(t, tt.pkgPath, `package foo +import "runtime" +func f() { runtime.Caller(0) } +`) + prog := llssa.NewProgram(nil) + if tt.targetName != "" { + prog.Target().Target = tt.targetName + } + if tt.goarch != "" { + prog.Target().GOARCH = tt.goarch + } + pkg := prog.NewPackage("foo", tt.pkgPath) + fn := pkg.NewFunc("f", llssa.NoArgsNoRet, llssa.InGo) + goFn := ssapkg.Func("f") + ctx := &context{ + prog: prog, + pkg: pkg, + fn: fn, + goFn: goFn, + trackCallerFrames: tt.track, + runtimeCallerFuncs: runtimeCallerFuncSet(ssapkg), + } + if got := ctx.shouldTrackCallerFrames(); got != tt.want { + t.Fatalf("shouldTrackCallerFrames() = %v, want %v", got, tt.want) + } + }) + } + + if canTrackCallerFramesForPackage("net/http") { + t.Fatal("stdlib paths without dots should not track caller frames") + } +} + +func TestRuntimeFrameNameNormalization(t *testing.T) { + tests := []struct { + in string + want string + }{ + {in: "command-line-arguments.main", want: "main.main"}, + {in: "example.com/foo.f$1", want: "example.com/foo.f.func1"}, + {in: "example.com/foo.f", want: "example.com/foo.f"}, + {in: "example.com/foo.f$", want: "example.com/foo.f$"}, + {in: "example.com/foo.f$inner", want: "example.com/foo.f$inner"}, + } + for _, tt := range tests { + if got := runtimeFrameName(tt.in); got != tt.want { + t.Fatalf("runtimeFrameName(%q) = %q, want %q", tt.in, got, tt.want) + } + } + + if got := (*context)(nil).runtimeCallerFrameName(); got != "" { + t.Fatalf("nil context runtimeCallerFrameName() = %q, want empty", got) + } + if got := (&context{}).runtimeCallerFrameName(); got != "" { + t.Fatalf("empty context runtimeCallerFrameName() = %q, want empty", got) + } + prog := newLLSSAProg(t) + pkg := prog.NewPackage("main", "command-line-arguments") + sig := types.NewSignatureType(nil, nil, nil, nil, nil, false) + ctx := &context{fn: pkg.NewFuncEx("command-line-arguments.f$1", sig, llssa.InGo, false, false)} + if got, want := ctx.runtimeCallerFrameName(), "main.f.func1"; got != want { + t.Fatalf("fallback runtimeCallerFrameName() = %q, want %q", got, want) + } +} + +func TestCompileRuntimeCallerFrameInstrumentation(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime/debug" + +func f() { + _ = debug.Stack() +} +`) + prog := newLLSSAProg(t) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + for _, want := range []string{ + "RecordCallerLocation", + `c"example.com/foo.f`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("compiled caller-frame IR missing %q:\n%s", want, ir) + } + } + for _, old := range []string{"PushCallerFrame", "SetCallerLine", "PopCallerFrame"} { + if strings.Contains(ir, old) { + t.Fatalf("compiled caller-frame IR still contains old %q instrumentation:\n%s", old, ir) + } + } +} + +func TestCompileRuntimeCallerPCLineMetadata(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func top() { + runtime.Caller(0) + leaf() +} + +func leaf() {} +`) + prog := newLLSSAProg(t) + prog.Target().GOOS = "linux" + prog.Target().GOARCH = "amd64" + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + for _, want := range []string{ + `!llgo.pcline = !{!`, + `!"example.com/foo.top"`, + `!"caller_frame_compile.go"`, + "__llgo_pcsite_", + "${:uid}", + `.pushsection llgo_pcline`, + `.quad __llgo_pcsite_`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("missing pcline metadata %s:\n%s", want, ir) + } + } + for _, line := range strings.Split(ir, "\n") { + if strings.Contains(line, "!llgo.pcline") || strings.Contains(line, `!"example.com/foo.top"`) { + if strings.Contains(line, `ptr @`) { + t.Fatalf("pcline metadata should use symbol strings, not function pointers:\n%s", line) + } + } + } +} + +func TestCompileRuntimeCallerPCLineMetadata32Bit(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func top() { + runtime.Caller(0) +} +`) + prog := newLLSSAProgForTarget(t, &llssa.Target{GOOS: "linux", GOARCH: "386"}) + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + for _, want := range []string{ + `.p2align 2`, + `.long __llgo_pcsite_`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("missing 32-bit pcline asm %q:\n%s", want, ir) + } + } +} + +func TestCompileRuntimeCallerPCLineEscapesDollarInInlineAsm(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func top() { + func() { + runtime.Caller(0) + }() +} +`) + prog := newLLSSAProg(t) + prog.Target().GOOS = "linux" + prog.Target().GOARCH = "amd64" + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + if !strings.Contains(ir, `!"example.com/foo.top$1"`) { + t.Fatalf("metadata should keep the original Go symbol name:\n%s", ir) + } + if !strings.Contains(ir, `example.com/foo.top$$1`) { + t.Fatalf("inline asm should escape $ in the associated symbol:\n%s", ir) + } + for _, line := range strings.Split(ir, "\n") { + if strings.Contains(line, `.pushsection llgo_pcline`) && strings.Contains(line, `example.com/foo.top$1`) && !strings.Contains(line, `example.com/foo.top$$1`) { + t.Fatalf("inline asm has an unescaped $ operand:\n%s", line) + } + } +} + +func TestRuntimeCallerInstrumentationEdgeCases(t *testing.T) { + ssapkg, _ := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func top() { + runtime.Caller(0) +} +`) + prog := newLLSSAProgForTarget(t, &llssa.Target{GOOS: "linux", GOARCH: "amd64"}) + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg := prog.NewPackage("foo", "example.com/foo") + fn := pkg.NewFunc("example.com/foo.top", llssa.NoArgsNoRet, llssa.InGo) + ctx := &context{ + prog: prog, + pkg: pkg, + fn: fn, + goFn: ssapkg.Func("top"), + fset: token.NewFileSet(), + trackCallerFrames: true, + runtimeCallerFuncs: runtimeCallerFuncSet(ssapkg), + } + var b llssa.Builder + ctx.pushCallerLocationFrame(b, nil) + ctx.recordRuntimeLocation(b, token.NoPos, "RecordCallerLocation") + ctx.emitPCLineLabel(b, token.NoPos) + ctx.popCallerLocationFrame(b) + + if pos := (&context{}).funcInfoPosition(nil); pos.IsValid() { + t.Fatal("nil function should have no funcinfo position") + } + if canEmitPCLineLabelsForTarget(nil) { + t.Fatal("nil target should not emit pc-line labels") + } + if canEmitPCLineLabelsForTarget(&llssa.Target{GOOS: "linux", GOARCH: "wasm"}) { + t.Fatal("wasm target should not emit pc-line labels") + } + if canEmitPCLineLabelsForTarget(&llssa.Target{GOOS: "linux", GOARCH: "amd64", Target: "esp32"}) { + t.Fatal("named target should not emit pc-line labels") + } + if got, want := asmQuoteSymbol(`a\b"c$d`), `"a\\b\"c$$d"`; got != want { + t.Fatalf("asmQuoteSymbol() = %q, want %q", got, want) + } +} + +func TestCompileRuntimeCallerPCLineMetadataSitesDisabled(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func top() { + runtime.Caller(0) +} +`) + prog := newLLSSAProg(t) + prog.Target().GOOS = "linux" + prog.Target().GOARCH = "amd64" + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(false) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + // Funcinfo metadata still flows... + if !strings.Contains(ir, llssa.FuncInfoMetadataName) { + t.Fatalf("sites disabled should keep funcinfo metadata:\n%s", ir) + } + // ...but no pc-line site labels are emitted. + for _, bad := range []string{"__llgo_pcsite_", ".pushsection llgo_pcline", "!llgo.pcline"} { + if strings.Contains(ir, bad) { + t.Fatalf("sites disabled should not emit pc-line sites, found %q:\n%s", bad, ir) + } + } +} + +func TestCompileRuntimeCallerPCLineMetadataOnDarwin(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func top() { + runtime.Caller(0) +} +`) + prog := newLLSSAProg(t) + prog.Target().GOOS = "darwin" + prog.Target().GOARCH = "arm64" + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + for _, want := range []string{ + `!llgo.pcline`, + "__llgo_pcsite_", + `.pushsection __DATA,__llgo_pcl`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("darwin should emit Mach-O pc-site labels, missing %q:\n%s", want, ir) + } + } + if strings.Contains(ir, `.pushsection llgo_pcline`) { + t.Fatalf("darwin must not use the ELF pcline section syntax:\n%s", ir) + } +} + +func TestCompileRuntimeCallerFrameUsesGoNameForLinkname(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "command-line-arguments", `package main +import "runtime" + +func renamedPC() uintptr { + pc, _, _, _ := runtime.Caller(0) + return pc +} +`) + prog := newLLSSAProg(t) + prog.SetLinkname("command-line-arguments.renamedPC", "main.renamedPCSymbol") + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + if !strings.Contains(ir, `c"main.renamedPC"`) { + t.Fatalf("compiled caller-frame IR missing source function name:\n%s", ir) + } + if strings.Contains(ir, `c"main.renamedPCSymbol"`) { + t.Fatalf("compiled caller-frame IR used linkname target as runtime function name:\n%s", ir) + } +} + +func TestCompileRuntimeCallerFrameInstrumentationSkipped(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func f() { + runtime.Caller(0) +} +`) + prog := newLLSSAProg(t) + prog.Target().Target = "esp32" + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + if ir := pkg.Module().String(); strings.Contains(ir, "RecordCallerLocation") || strings.Contains(ir, "RecordPanicLocation") { + t.Fatalf("target builds should not emit caller location tracking:\n%s", ir) + } + + ssapkg, files = buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +func f() {} +`) + prog = newLLSSAProg(t) + pkg, err = NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + if ir := pkg.Module().String(); strings.Contains(ir, "RecordCallerLocation") || strings.Contains(ir, "RecordPanicLocation") { + t.Fatalf("packages without runtime stack APIs should not emit caller location tracking:\n%s", ir) + } + + ssapkg, files = buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" +func f() { _ = runtime.FuncForPC(0) } +`) + prog = newLLSSAProg(t) + prog.Target().GOOS = "linux" + prog.Target().GOARCH = "amd64" + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg, err = NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + for _, bad := range []string{"RecordCallerLocation", "RecordPanicLocation", "PushCallerLocationFrame", `!llgo.pcline`} { + if strings.Contains(ir, bad) { + t.Fatalf("FuncForPC-only packages should not emit caller frame tracking %q:\n%s", bad, ir) + } + } + if !strings.Contains(ir, `!llgo.funcinfo = !{!`) { + t.Fatalf("FuncForPC-only packages should still emit funcinfo metadata:\n%s", ir) + } +} + +func TestCompileRuntimeCallerLocationOnlyForRuntimePaths(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func helper() {} + +func f() { + helper() + runtime.Caller(0) +} +`) + prog := newLLSSAProg(t) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + if !strings.Contains(ir, "RecordCallerLocation") { + t.Fatalf("runtime.Caller should record caller location:\n%s", ir) + } + if strings.Contains(ir, "SetCallerLine") || strings.Contains(ir, "PushCallerFrame") { + t.Fatalf("caller location tracking should not emit old TLS instrumentation:\n%s", ir) + } +} 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..7e1d6fc8be 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -157,25 +157,28 @@ type pkgInfo struct { type none = struct{} type context struct { - prog llssa.Program - pkg llssa.Package - fn llssa.Function - goFn *ssa.Function - fset *token.FileSet - goProg *ssa.Program - goTyps *types.Package - goPkg *ssa.Package - pyMod string - skips map[string]none - loaded map[*types.Package]*pkgInfo // loaded packages - bvals map[ssa.Value]llssa.Expr // block values - methodNilDerefChecks map[*ssa.UnOp]none - vargs map[*ssa.Alloc][]llssa.Expr // varargs - funcs map[*ssa.Function]llssa.Function - linkOnceFns map[*ssa.Function]none - stackDefers map[*ssa.Function]bool - anonDefers map[*ssa.Function]bool - paramDIVars map[*types.Var]llssa.DIVar + prog llssa.Program + pkg llssa.Package + fn llssa.Function + goFn *ssa.Function + fset *token.FileSet + goProg *ssa.Program + goTyps *types.Package + goPkg *ssa.Package + pyMod string + skips map[string]none + loaded map[*types.Package]*pkgInfo // loaded packages + bvals map[ssa.Value]llssa.Expr // block values + methodNilDerefChecks map[*ssa.UnOp]none + vargs map[*ssa.Alloc][]llssa.Expr // varargs + funcs map[*ssa.Function]llssa.Function + linkOnceFns map[*ssa.Function]none + stackDefers map[*ssa.Function]bool + anonDefers map[*ssa.Function]bool + paramDIVars map[*types.Var]llssa.DIVar + runtimeCallerFuncs map[*ssa.Function]bool + pcLineSeq uint64 + recoverVolatileAllocs map[*ssa.Alloc]none patches Patches blkInfos []blocks.Info @@ -199,6 +202,9 @@ type context struct { rewrites map[string]string embedMap goembed.VarMap embedInits []embedInit + + trackCallerFrames bool + callerFrameMark llssa.Expr } func (p *context) rewriteValue(name string) (string, bool) { @@ -214,6 +220,79 @@ func (p *context) rewriteValue(name string) (string, bool) { return val, ok } +func filesUseRuntimeCaller(files []*ast.File) bool { + for _, file := range files { + imports := make(map[string]string) + dotImports := make(map[string]bool) + for _, imp := range file.Imports { + path, err := strconv.Unquote(imp.Path.Value) + if err != nil { + continue + } + switch path { + case "runtime", "runtime/debug": + default: + continue + } + name := path[strings.LastIndex(path, "/")+1:] + if imp.Name != nil { + switch imp.Name.Name { + case ".": + dotImports[path] = true + continue + case "_": + continue + default: + name = imp.Name.Name + } + } + imports[name] = path + } + if len(imports) == 0 && len(dotImports) == 0 { + continue + } + found := false + ast.Inspect(file, func(n ast.Node) bool { + if found { + return false + } + switch n := n.(type) { + case *ast.SelectorExpr: + ident, ok := n.X.(*ast.Ident) + if !ok { + return true + } + if runtimeCallerSelector(imports[ident.Name], n.Sel.Name) { + found = true + return false + } + case *ast.Ident: + if (dotImports["runtime"] && isRuntimeCallerFrameName(n.Name)) || + (dotImports["runtime/debug"] && n.Name == "Stack") { + found = true + return false + } + } + return true + }) + if found { + return true + } + } + return false +} + +func runtimeCallerSelector(path, name string) bool { + switch path { + case "runtime": + return isRuntimeCallerFrameName(name) + case "runtime/debug": + return name == "Stack" + default: + 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". @@ -469,13 +548,27 @@ 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) + pcLineNoInline := p.needsPCLineNoInline(f) + if disableInline || noInlineDirective || runtimeStackNoInline || pcLineNoInline || functionUsesRecover(f) { + fn.Inline(llssa.NoInline) + } + if noInlineDirective || runtimeStackNoInline || pcLineNoInline { + 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.funcInfoPosition(f) + pkg.EmitFuncInfo(fn.Name(), goName, pos.Filename, pos.Line, pos.Column) + } var childInits []func() if len(f.AnonFuncs) > 0 { parentInits := p.inits @@ -500,12 +593,20 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun dbgEnabled := enableDbg && (f == nil || f.Origin() == nil) dbgSymsEnabled := enableDbgSyms && (f == nil || f.Origin() == nil) p.inits = append(p.inits, func() { - oldFn, oldGoFn, oldMethodNilDerefChecks := p.fn, p.goFn, p.methodNilDerefChecks + oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark := p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark + oldRecoverVolatileAllocs := p.recoverVolatileAllocs p.fn = fn p.goFn = f + p.callerFrameMark = llssa.Nil p.state = state // restore pkgState when compiling funcBody + if f.Recover != nil { + p.recoverVolatileAllocs = make(map[*ssa.Alloc]none) + } else { + p.recoverVolatileAllocs = nil + } defer func() { - p.fn, p.goFn, p.methodNilDerefChecks = oldFn, oldGoFn, oldMethodNilDerefChecks + p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark = oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark + p.recoverVolatileAllocs = oldRecoverVolatileAllocs }() p.phis = nil if dbgSymsEnabled { @@ -560,6 +661,45 @@ 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) needsPCLineNoInline(f *ssa.Function) bool { + if p == nil || f == nil || !p.prog.FuncInfoSitesEnabled() || !p.trackCallerFrames || !p.runtimeCallerFuncs[f] { + return false + } + if !canEmitPCLineLabelsForTarget(p.prog.Target()) { + return false + } + return p.pkg != nil && canTrackCallerFramesForPackage(p.pkg.Path()) +} + func (p *context) getFuncBodyPos(f *ssa.Function) token.Position { if f.Object() != nil { if fn, ok := f.Object().(*types.Func); ok && fn.Scope() != nil { @@ -569,6 +709,23 @@ func (p *context) getFuncBodyPos(f *ssa.Function) token.Position { return p.goProg.Fset.Position(f.Pos()) } +func (p *context) funcInfoPosition(f *ssa.Function) token.Position { + if f != nil { + switch syntax := f.Syntax().(type) { + case *ast.FuncDecl: + if syntax.Body != nil && len(syntax.Body.List) != 0 { + return p.goProg.Fset.Position(syntax.Body.List[0].Pos()) + } + case *ast.FuncLit: + if syntax.Body != nil && len(syntax.Body.List) != 0 { + return p.goProg.Fset.Position(syntax.Body.List[0].Pos()) + } + } + return p.goProg.Fset.Position(f.Pos()) + } + return token.Position{} +} + func isGlobal(v *types.Var) bool { // TODO(lijie): better implementation return strings.HasPrefix(v.Parent().String(), "package ") @@ -627,6 +784,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.pushCallerLocationFrame(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") } @@ -902,6 +1062,29 @@ func (p *context) syntheticMakeSliceCap(v *ssa.Slice) (llssa.Expr, bool) { return p.prog.IntVal(uint64(arr.Len()), p.prog.Int()), true } +func (p *context) markRecoverVolatileAlloc(v *ssa.Alloc) { + if p.recoverVolatileAllocs == nil || v.Heap { + return + } + p.recoverVolatileAllocs[v] = none{} +} + +func (p *context) isRecoverVolatileAddr(v ssa.Value) bool { + if p.recoverVolatileAllocs == nil { + return false + } + switch v := v.(type) { + case *ssa.Alloc: + _, ok := p.recoverVolatileAllocs[v] + return ok + case *ssa.FieldAddr: + return p.isRecoverVolatileAddr(v.X) + case *ssa.IndexAddr: + return p.isRecoverVolatileAddr(v.X) + } + return false +} + func isAllocVargs(ctx *context, v *ssa.Alloc) bool { refs := *v.Referrers() n := len(refs) @@ -1015,6 +1198,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue if t := p.type_(v.Type(), llssa.InGo); t.RawType() != nil { if p.isLargeNonPointerValue(t) { x := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) p.assertNilDerefBase(b, v.X) b.AssertNilDeref(x) return @@ -1028,6 +1212,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue // Zero-length slice-to-array conversions can leave only // an unused slice deref; preserve its required nil check. x := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) p.assertNilDerefBase(b, v.X) b.AssertNilDeref(x) return @@ -1058,11 +1243,33 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue } } x := p.compileValue(b, v.X) + if v.Op != token.ARROW { + p.recordPanicLocation(b, v.Pos()) + } if shouldAssertDirectNilDeref(v) { b.AssertNilDeref(x) } if v.Op == token.ARROW { ret = b.Recv(x, v.CommaOk) + } else if v.Op == token.MUL { + if t := p.type_(v.Type(), llssa.InGo); t.RawType() != nil && p.prog.SizeOf(t) == 0 { + p.assertNilDerefBase(b, v.X) + } + interfaceCompareDeref := isInterfaceCompareDeref(v) + if interfaceCompareDeref { + p.assertNilDerefBase(b, v.X) + } + // A recovered panic resumes through the recover block, which reads + // result slots. Keep nil derefs in recover-capable functions ordered + // so the panic cannot be removed or moved past partial result writes. + recoverVolatile := p.isRecoverVolatileAddr(v.X) + if interfaceCompareDeref || (p.recoverVolatileAllocs != nil && !recoverVolatile) { + b.AssertNilDeref(x) + } + ret = b.Load(x) + if recoverVolatile { + ret = ret.SetVolatile(true) + } } else { if v.Op == token.MUL { if t := p.type_(v.Type(), llssa.InGo); t.RawType() != nil && p.prog.SizeOf(t) == 0 { @@ -1093,6 +1300,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue ret = b.Convert(p.type_(t, llssa.InGo), x) case *ssa.FieldAddr: x := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) if p.isAddressOfFieldAddr(v) { b.AssertNilDeref(x) } @@ -1107,6 +1315,10 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue } elem := p.type_(t.Elem(), llssa.InGo) ret = b.Alloc(elem, v.Heap) + p.markRecoverVolatileAlloc(v) + if p.isRecoverVolatileAddr(v) { + b.Store(ret, p.prog.Zero(elem)).SetVolatile(true) + } case *ssa.IndexAddr: vx := v.X if _, ok := p.isVArgs(vx); ok { // varargs: this is a varargs index @@ -1114,10 +1326,12 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue } x := p.compileValue(b, vx) idx := p.compileValue(b, v.Index) + p.recordPanicLocation(b, v.Pos()) ret = b.IndexAddr(x, idx) case *ssa.Index: x := p.compileValue(b, v.X) idx := p.compileValue(b, v.Index) + p.recordPanicLocation(b, v.Pos()) ret = b.Index(x, idx, func() (addr llssa.Expr, zero bool) { switch n := v.X.(type) { case *ssa.Const: @@ -1151,6 +1365,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue if v.Max != nil { max = p.compileValue(b, v.Max) } + p.recordPanicLocation(b, v.Pos()) ret = b.Slice(x, low, high, max) ret.Type = p.type_(v.Type(), llssa.InGo) case *ssa.MakeInterface: @@ -1207,6 +1422,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue case *ssa.TypeAssert: x := p.compileValue(b, v.X) t := p.type_(v.AssertedType, llssa.InGo) + p.recordPanicLocation(b, v.Pos()) ret = b.TypeAssert(x, t, v.CommaOk) case *ssa.Extract: x := p.compileValue(b, v.Tuple) @@ -1247,6 +1463,7 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue case *ssa.SliceToArrayPointer: t := p.type_(v.Type(), llssa.InGo) x := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) ret = b.SliceToArrayPointer(x, t) default: panic(fmt.Sprintf("compileInstrAndValue: unknown instr - %T\n", iv)) @@ -1368,7 +1585,10 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { } ptr := p.compileValue(b, va) val := p.compileValue(b, v.Val) - b.Store(ptr, val) + store := b.Store(ptr, val) + if p.isRecoverVolatileAddr(va) { + store.SetVolatile(true) + } case *ssa.Jump: jmpb := p.jumpTo(v) b.Jump(jmpb) @@ -1381,8 +1601,12 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { } } if p.returnNeedsImplicitRunDefers(v) { + p.recordPanicLocation(b, v.Pos()) b.RunDefers() } + if p.shouldTrackCallerFrames() { + p.popCallerLocationFrame(b) + } b.Return(results...) case *ssa.If: fn := p.fn @@ -1395,6 +1619,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { m := p.compileValue(b, v.Map) key := p.compileValue(b, v.Key) val := p.compileValue(b, v.Value) + p.recordPanicLocation(b, v.Pos()) b.MapUpdate(m, key, val) case *ssa.Defer: if v.DeferStack != nil { @@ -1405,13 +1630,16 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { case *ssa.Go: p.call(b, llssa.Go, &v.Call) case *ssa.RunDefers: + p.recordPanicLocation(b, v.Pos()) b.RunDefers() case *ssa.Panic: arg := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) b.Panic(arg) case *ssa.Send: ch := p.compileValue(b, v.Chan) x := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) b.Send(ch, x) case *ssa.DebugRef: if enableDbgSyms && v.Parent().Origin() == nil { @@ -1434,6 +1662,25 @@ func (p *context) getLocalVariable(b llssa.Builder, fn *ssa.Function, v *types.V return b.DIVarAuto(scope, pos, v.Name(), t) } +func functionUsesRecover(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 + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if ok && builtin.Name() == "recover" { + return true + } + } + } + return false +} + func (p *context) compileFunction(v *ssa.Function) (goFn llssa.Function, pyFn llssa.PyObjRef, kind int) { // TODO(xsw) v.Pkg == nil: means auto generated function? if v.Pkg == p.goPkg || v.Pkg == nil { @@ -1727,6 +1974,9 @@ func newPackageEx(prog llssa.Program, patches Patches, rewrites map[string]strin }, cgoSymbols: make([]string, 0, 128), rewrites: rewrites, + + trackCallerFrames: filesUseRuntimeCaller(files) || packageUsesRuntimeCaller(pkg), + runtimeCallerFuncs: runtimeCallerFuncSet(pkg), } if embedMap != nil { ctx.embedMap = *embedMap diff --git a/cl/funcinfo_metadata_test.go b/cl/funcinfo_metadata_test.go new file mode 100644 index 0000000000..5af5800613 --- /dev/null +++ b/cl/funcinfo_metadata_test.go @@ -0,0 +1,169 @@ +//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) + prog.EnableFuncInfoSites(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) + } + if got := records["foo.top"].line; got != 6 { + t.Fatalf("top funcinfo line = %d, want first body statement line 6", got) + } + if got := records["foo.leaf"].line; got != 9 { + t.Fatalf("leaf funcinfo line = %d, want line 9", got) + } + if got := records["foo.T.method"].line; got != 11 { + t.Fatalf("empty method funcinfo line = %d, want declaration line 11", 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/cl/instr.go b/cl/instr.go index b7fc52abd3..8ea22ecb4d 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -853,6 +853,745 @@ 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.goFn == nil || !p.trackCallerFrames { + return false + } + if !p.runtimeCallerFuncs[p.goFn] { + return false + } + if target := p.prog.Target(); target != nil && (target.Target != "" || target.GOARCH == "wasm") { + return false + } + return canTrackCallerFramesForPackage(p.pkg.Path()) +} + +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 { + return len(runtimeCallerFuncSet(pkg)) != 0 +} + +func fnUsesRuntimeCaller(fn *ssa.Function) bool { + if fn == nil { + return false + } + if fn.Pkg == nil { + return fnHasDirectRuntimeCaller(fn) + } + return runtimeCallerFuncSet(fn.Pkg)[fn] +} + +func runtimeCallerFuncSet(pkg *ssa.Package) map[*ssa.Function]bool { + if pkg == nil { + return nil + } + funcs, trackable := collectRuntimeCallerFunctions(pkg) + analysis := &runtimeCallerAnalysis{ + pkg: pkg, + funcs: funcs, + trackable: trackable, + callsites: collectRuntimeCallerCallsites(funcs), + memo: make(map[*ssa.Function]bool), + visiting: make(map[*ssa.Function]bool), + } + if !analysis.packageHasRuntimeCaller() { + return nil + } + out := make(map[*ssa.Function]bool) + for { + ntrack := len(trackable) + for fn := range trackable { + if analysis.fnMayReachRuntimeCaller(fn) { + out[fn] = true + } + } + if len(trackable) == ntrack { + break + } + } + if len(out) == 0 { + return nil + } + return out +} + +type runtimeCallerAnalysis struct { + pkg *ssa.Package + funcs map[*ssa.Function]bool + trackable map[*ssa.Function]bool + callsites map[*ssa.Function][]*ssa.CallCommon + memo map[*ssa.Function]bool + visiting map[*ssa.Function]bool +} + +func collectRuntimeCallerFunctions(pkg *ssa.Package) (funcs, trackable map[*ssa.Function]bool) { + funcs = make(map[*ssa.Function]bool) + trackable = make(map[*ssa.Function]bool) + var add func(*ssa.Function, bool) bool + add = func(fn *ssa.Function, track bool) bool { + if fn == nil || !functionBelongsToPackage(pkg, fn) { + return false + } + if track { + trackable[fn] = true + } + if funcs[fn] { + return false + } + funcs[fn] = true + for _, anon := range fn.AnonFuncs { + add(anon, false) + } + return true + } + for _, member := range pkg.Members { + if fn, ok := member.(*ssa.Function); ok { + add(fn, true) + } + } + if pkg.Prog != nil && pkg.Pkg != nil { + for _, typ := range pkg.Prog.RuntimeTypes() { + if !typeBelongsToPackage(typ, pkg.Pkg) { + continue + } + methods := pkg.Prog.MethodSets.MethodSet(typ) + for i := 0; i < methods.Len(); i++ { + add(pkg.Prog.MethodValue(methods.At(i)), false) + } + } + } + for changed := true; changed; { + changed = false + for fn := range funcs { + forEachCall(fn, func(call *ssa.CallCommon) { + if add(call.StaticCallee(), trackable[fn]) { + changed = true + } + }) + } + } + return funcs, trackable +} + +func collectRuntimeCallerCallsites(funcs map[*ssa.Function]bool) map[*ssa.Function][]*ssa.CallCommon { + callsites := make(map[*ssa.Function][]*ssa.CallCommon) + for fn := range funcs { + forEachCall(fn, func(call *ssa.CallCommon) { + callee := call.StaticCallee() + if funcs[callee] { + callsites[callee] = append(callsites[callee], call) + } + }) + } + return callsites +} + +func functionBelongsToPackage(pkg *ssa.Package, fn *ssa.Function) bool { + if pkg == nil || fn == nil { + return false + } + if fn.Pkg == pkg { + return true + } + return fn.Pkg == nil && fn.Parent() != nil && functionBelongsToPackage(pkg, fn.Parent()) +} + +func typeBelongsToPackage(typ types.Type, pkg *types.Package) bool { + if pkg == nil { + return false + } + for { + if ptr, ok := types.Unalias(typ).(*types.Pointer); ok { + typ = ptr.Elem() + continue + } + break + } + named, ok := types.Unalias(typ).(*types.Named) + return ok && named.Obj() != nil && named.Obj().Pkg() == pkg +} + +func (a *runtimeCallerAnalysis) packageHasRuntimeCaller() bool { + for fn := range a.funcs { + if fnHasDirectRuntimeCaller(fn) { + return true + } + } + return false +} + +func fnHasDirectRuntimeCaller(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 isRuntimeCallerFrameFunc(call.Common().StaticCallee()) { + return true + } + } + } + for _, anon := range fn.AnonFuncs { + if fnHasDirectRuntimeCaller(anon) { + return true + } + } + return false +} + +func (a *runtimeCallerAnalysis) fnMayReachRuntimeCaller(fn *ssa.Function) bool { + if fn == nil { + return false + } + if isRuntimeCallerFrameFunc(fn) { + return true + } + if !a.funcs[fn] { + return false + } + if ok, done := a.memo[fn]; done { + return ok + } + if a.visiting[fn] { + return false + } + a.visiting[fn] = true + defer delete(a.visiting, fn) + reaches := false + forEachCall(fn, func(call *ssa.CallCommon) { + if reaches { + return + } + callee := call.StaticCallee() + switch { + case isRuntimeCallerFrameFunc(callee): + reaches = true + case callee != nil: + reaches = a.fnMayReachRuntimeCaller(callee) + case call.Method != nil: + reaches = a.interfaceInvokeMayReachRuntimeCaller(fn, call) + default: + reaches = a.functionValueCallMayReachRuntimeCaller(fn, call.Value) + } + }) + if !reaches { + for _, anon := range fn.AnonFuncs { + if a.fnMayReachRuntimeCaller(anon) { + if a.trackable[fn] { + a.trackable[anon] = true + } + reaches = true + break + } + } + } + a.memo[fn] = reaches + return reaches +} + +func (a *runtimeCallerAnalysis) functionValueCallMayReachRuntimeCaller(fn *ssa.Function, value ssa.Value) bool { + targets, ok := a.functionValueTargets(fn, value) + if !ok { + return true + } + for target := range targets { + if a.fnMayReachRuntimeCaller(target) { + return true + } + } + return false +} + +func (a *runtimeCallerAnalysis) functionValueTargets(fn *ssa.Function, value ssa.Value) (map[*ssa.Function]bool, bool) { + if targets, ok := staticFunctionTargets(value); ok { + return targets, true + } + param, ok := value.(*ssa.Parameter) + if !ok || param.Parent() != fn { + return nil, false + } + idx, ok := parameterIndex(fn, param) + if !ok { + return nil, false + } + return a.functionParamTargets(fn, idx) +} + +func (a *runtimeCallerAnalysis) functionParamTargets(fn *ssa.Function, idx int) (map[*ssa.Function]bool, bool) { + callsites := a.callsites[fn] + if len(callsites) == 0 { + return nil, false + } + targets := make(map[*ssa.Function]bool) + for _, call := range callsites { + args := call.Args + if idx >= len(args) { + return nil, false + } + argTargets, ok := staticFunctionTargets(args[idx]) + if !ok { + return nil, false + } + for target := range argTargets { + targets[target] = true + } + } + return targets, true +} + +func staticFunctionTargets(value ssa.Value) (map[*ssa.Function]bool, bool) { + switch v := value.(type) { + case *ssa.Function: + return map[*ssa.Function]bool{v: true}, true + case *ssa.MakeClosure: + if fn, ok := v.Fn.(*ssa.Function); ok { + return map[*ssa.Function]bool{fn: true}, true + } + } + return nil, false +} + +func (a *runtimeCallerAnalysis) interfaceInvokeMayReachRuntimeCaller(fn *ssa.Function, call *ssa.CallCommon) bool { + targets, ok := a.interfaceMethodTargets(fn, call.Value, call.Method) + if !ok { + return true + } + for target := range targets { + if a.fnMayReachRuntimeCaller(target) { + return true + } + } + return false +} + +func (a *runtimeCallerAnalysis) interfaceMethodTargets(fn *ssa.Function, value ssa.Value, method *types.Func) (map[*ssa.Function]bool, bool) { + if targets, ok := a.staticInterfaceMethodTargets(value, method); ok { + return targets, true + } + param, ok := value.(*ssa.Parameter) + if !ok || param.Parent() != fn { + return nil, false + } + idx, ok := parameterIndex(fn, param) + if !ok { + return nil, false + } + callsites := a.callsites[fn] + if len(callsites) == 0 { + return nil, false + } + targets := make(map[*ssa.Function]bool) + for _, call := range callsites { + args := call.Args + if idx >= len(args) { + return nil, false + } + argTargets, ok := a.staticInterfaceMethodTargets(args[idx], method) + if !ok { + return nil, false + } + for target := range argTargets { + targets[target] = true + } + } + return targets, true +} + +func (a *runtimeCallerAnalysis) staticInterfaceMethodTargets(value ssa.Value, method *types.Func) (map[*ssa.Function]bool, bool) { + switch v := value.(type) { + case *ssa.MakeInterface: + return a.methodTargetsForType(v.X.Type(), method) + case *ssa.ChangeInterface: + return a.staticInterfaceMethodTargets(v.X, method) + } + return nil, false +} + +func (a *runtimeCallerAnalysis) methodTargetsForType(typ types.Type, method *types.Func) (map[*ssa.Function]bool, bool) { + if a.pkg == nil || a.pkg.Prog == nil || method == nil { + return nil, false + } + methods := a.pkg.Prog.MethodSets.MethodSet(typ) + for i := 0; i < methods.Len(); i++ { + sel := methods.At(i) + if sel.Obj().Name() != method.Name() { + continue + } + fn := a.pkg.Prog.MethodValue(sel) + if fn == nil { + return nil, false + } + return map[*ssa.Function]bool{fn: true}, true + } + return nil, false +} + +func parameterIndex(fn *ssa.Function, param *ssa.Parameter) (int, bool) { + for i, candidate := range fn.Params { + if candidate == param { + return i, true + } + } + return 0, false +} + +func forEachCall(fn *ssa.Function, do func(*ssa.CallCommon)) { + if fn == nil { + return + } + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + if call, ok := instr.(ssa.CallInstruction); ok { + do(call.Common()) + } + } + } +} + +func isRuntimeCallerFunc(fn *ssa.Function) bool { + if fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + return false + } + switch fn.Pkg.Pkg.Path() { + case "runtime", "github.com/goplus/llgo/runtime/internal/lib/runtime": + return isRuntimeCallerName(fn.Name()) + case "runtime/debug": + return fn.Name() == "Stack" + default: + return false + } +} + +func isRuntimeCallerFrameFunc(fn *ssa.Function) bool { + if fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + return false + } + switch fn.Pkg.Pkg.Path() { + case "runtime", "github.com/goplus/llgo/runtime/internal/lib/runtime": + return isRuntimeCallerFrameName(fn.Name()) + case "runtime/debug": + return fn.Name() == "Stack" + default: + return false + } +} + +func isRuntimeCallerLookupFunc(fn *ssa.Function) bool { + if fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + return false + } + switch fn.Pkg.Pkg.Path() { + case "runtime", "github.com/goplus/llgo/runtime/internal/lib/runtime": + switch fn.Name() { + case "Caller", "Callers", "Stack": + return true + } + case "runtime/debug": + return fn.Name() == "Stack" + } + return false +} + +func isRuntimeCallerName(name string) bool { + switch name { + case "Caller", "Callers", "CallersFrames", "FuncForPC", "Stack": + return true + default: + return false + } +} + +func isRuntimeCallerFrameName(name string) bool { + switch name { + case "Caller", "Callers", "CallersFrames", "Stack": + return true + default: + return false + } +} + +func (p *context) runtimeCallerFrameName() string { + if p == nil { + return "" + } + if p.goFn != nil && p.goFn.Pkg != nil && p.goFn.Pkg.Pkg != nil { + return runtimeFrameName(funcName(p.goFn.Pkg.Pkg, p.goFn, false)) + } + if p.fn != nil { + return runtimeFrameName(p.fn.Name()) + } + return "" +} + +func (p *context) pushCallerLocationFrame(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) + p.callerFrameMark = b.Call( + p.runtimeFunc("PushCallerLocationFrame", pushCallerLocationFrameSig()), + entry, + b.Str(p.runtimeCallerFrameName()), + b.Str(pos.Filename), + p.prog.IntVal(uint64(pos.Line), p.prog.Int()), + ) +} + +func (p *context) recordCallerLocation(b llssa.Builder, pos token.Pos) { + p.recordRuntimeLocation(b, pos, "RecordCallerLocation") +} + +func (p *context) recordPanicLocation(b llssa.Builder, pos token.Pos) { + p.recordRuntimeLocation(b, pos, "RecordPanicLocation") +} + +func (p *context) recordRuntimeLocation(b llssa.Builder, pos token.Pos, fn string) { + if !p.shouldTrackCallerFrames() { + return + } + position := p.fset.Position(pos) + if position.Line <= 0 || position.Filename == "" { + return + } + b.Call( + p.runtimeFunc(fn, recordRuntimeLocationSig()), + b.Convert(p.prog.Uintptr(), p.fn.Expr), + b.Str(p.runtimeCallerFrameName()), + b.Str(position.Filename), + p.prog.IntVal(uint64(position.Line), p.prog.Int()), + ) +} + +func (p *context) recordCallerLocationForCall(b llssa.Builder, call *ssa.CallCommon) { + if !p.shouldTrackCallerFrames() { + return + } + callee := call.StaticCallee() + if isRuntimeCallerLookupFunc(callee) { + p.recordCallerLocation(b, call.Pos()) + return + } + p.recordPanicLocation(b, call.Pos()) +} + +func (p *context) emitPCLineLabel(b llssa.Builder, pos token.Pos) { + if p == nil || p.pkg == nil || p.fn == nil || !p.prog.FuncInfoSitesEnabled() || !p.shouldTrackCallerFrames() { + return + } + target := p.prog.Target() + if !canEmitPCLineLabelsForTarget(target) { + return + } + position := p.fset.Position(pos) + if position.Line <= 0 || position.Filename == "" { + return + } + p.pcLineSeq++ + id := pcLineID(p.fn.Name(), p.pcLineSeq) + label := pcLineLabelName(id) + if target.GOOS == "darwin" { + // Mach-O subsections-via-symbols treats every non-local symbol as an + // atom boundary; a visible label in the middle of a function body + // lets the linker split and reorder the function. The "L" prefix + // keeps the label assembler-local so the function stays one atom. + label = "L" + label + } + asmLabel := label + "_${:uid}" + ptrDirective := ".quad" + align := "3" + if p.prog.PointerSize() == 4 { + ptrDirective = ".long" + align = "2" + } + // Keep section names in sync with internal/build/funcinfo_table.go + // (pcLineSiteSectionInfo). ELF ties the record to the function via + // SHF_LINK_ORDER (honored by --gc-sections); Mach-O uses a live_support + // section plus one linker-private atom symbol per record so -dead_strip + // keeps a record exactly when the function containing its label is live. + pushSection := ".pushsection llgo_pcline,\"ao\",@progbits," + asmQuoteSymbol(p.fn.Name()) + recordSymbol := "" + if target.GOOS == "darwin" { + pushSection = ".pushsection __DATA,__llgo_pcl,regular,live_support" + recordSymbol = "l_llgo_pcline_rec_${:uid}:\n" + } + b.InlineAsm( + asmLabel + ":\n" + + pushSection + "\n" + + ".p2align " + align + "\n" + + recordSymbol + + ptrDirective + " " + asmLabel + "\n" + + ".quad " + uint64Hex(id) + "\n" + + ".popsection", + ) + p.pkg.EmitPCLineInfo(id, p.fn.Name(), position.Filename, position.Line, position.Column) +} + +func canEmitPCLineLabelsForTarget(target *llssa.Target) bool { + if target == nil { + return false + } + if target.Target != "" || target.GOARCH == "wasm" { + return false + } + // ELF uses SHF_LINK_ORDER associated sections; Mach-O uses plain + // __DATA,__llgo_pcl sections (safe because LLGo's global DCE runs at the + // IR level). Other object formats need separate support. + switch target.GOOS { + case "linux", "darwin": + return true + } + return false +} + +func pcLineID(symbol string, seq uint64) uint64 { + const ( + offset = uint64(14695981039346656037) + prime = uint64(1099511628211) + ) + h := offset + for i := 0; i < len(symbol); i++ { + h ^= uint64(symbol[i]) + h *= prime + } + for i := 0; i < 8; i++ { + h ^= byteOfUint64(seq, uint(i*8)) + h *= prime + } + if h == 0 { + return 1 + } + return h +} + +func byteOfUint64(v uint64, shift uint) uint64 { + return (v >> shift) & 0xff +} + +func pcLineLabelName(id uint64) string { + const hexdigits = "0123456789abcdef" + var buf [16]byte + for i := len(buf) - 1; i >= 0; i-- { + buf[i] = hexdigits[id&0xf] + id >>= 4 + } + return "__llgo_pcsite_" + string(buf[:]) +} + +func uint64Hex(v uint64) string { + const hexdigits = "0123456789abcdef" + var buf [18]byte + buf[0] = '0' + buf[1] = 'x' + for i := len(buf) - 1; i >= 2; i-- { + buf[i] = hexdigits[v&0xf] + v >>= 4 + } + return string(buf[:]) +} + +func asmQuoteSymbol(symbol string) string { + var b strings.Builder + b.Grow(len(symbol) + 2) + b.WriteByte('"') + for i := 0; i < len(symbol); i++ { + switch symbol[i] { + case '\\', '"': + b.WriteByte('\\') + case '$': + b.WriteByte('$') + } + b.WriteByte(symbol[i]) + } + b.WriteByte('"') + return b.String() +} + +func (p *context) popCallerLocationFrame(b llssa.Builder) { + if p.callerFrameMark.IsNil() { + return + } + b.Call(p.runtimeFunc("PopCallerLocationFrame", popCallerLocationFrameSig()), p.callerFrameMark) +} + +func (p *context) runtimeFunc(name string, sig *types.Signature) llssa.Expr { + p.pkg.NeedRuntime = true + fullName := llssa.PkgRuntime + "." + name + if fn := p.pkg.FuncOf(fullName); fn != nil { + return fn.Expr + } + return p.pkg.NewFuncEx(fullName, sig, llssa.InGo, false, false).Expr +} + +func pushCallerLocationFrameSig() *types.Signature { + return types.NewSignatureType(nil, nil, nil, + types.NewTuple( + types.NewVar(token.NoPos, nil, "entry", types.Typ[types.Uintptr]), + types.NewVar(token.NoPos, nil, "name", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "file", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "startLine", types.Typ[types.Int]), + ), + types.NewTuple(types.NewVar(token.NoPos, nil, "", types.Typ[types.Int])), + false, + ) +} + +func recordRuntimeLocationSig() *types.Signature { + return types.NewSignatureType(nil, nil, nil, + types.NewTuple( + types.NewVar(token.NoPos, nil, "entry", types.Typ[types.Uintptr]), + types.NewVar(token.NoPos, nil, "name", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "file", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "line", types.Typ[types.Int]), + ), + nil, + false, + ) +} + +func popCallerLocationFrameSig() *types.Signature { + return types.NewSignatureType(nil, nil, nil, + types.NewTuple(types.NewVar(token.NoPos, nil, "mark", types.Typ[types.Int])), + nil, + false, + ) +} + +func runtimeFrameName(name string) string { + const commandLineArguments = "command-line-arguments." + if strings.HasPrefix(name, commandLineArguments) { + name = "main." + name[len(commandLineArguments):] + } + return normalizeRuntimeAnonFuncName(name) +} + +func normalizeRuntimeAnonFuncName(name string) string { + dollar := strings.LastIndexByte(name, '$') + if dollar < 0 || dollar == len(name)-1 { + return name + } + for i := dollar + 1; i < len(name); i++ { + if name[i] < '0' || name[i] > '9' { + return name + } + } + return name[:dollar] + ".func" + name[dollar+1:] +} + // ----------------------------------------------------------------------------- type explicitDeferStack struct { @@ -893,10 +1632,20 @@ func (p *context) deferStackOwner(fn *ssa.Function) llssa.Function { } 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 { + return p.emitDoEx(b, act, ds, false, false, fn, buildCall, args...) +} + +func (p *context) emitDoEx(b llssa.Builder, act llssa.DoAction, ds *explicitDeferStack, maskRecover, forwardRecover bool, fn llssa.Expr, buildCall func(llssa.Builder, llssa.Expr, ...llssa.Expr) llssa.Expr, args ...llssa.Expr) llssa.Expr { if ds != nil { b.DeferTo(ds.owner, ds.stack, fn, buildCall, args...) return llssa.Nil } + if maskRecover && act == llssa.Call { + return b.MaskRecoverCall(fn, buildCall, args...) + } + if forwardRecover && act == llssa.Call { + return b.ForwardRecoverFrameCall(fn, buildCall, args...) + } return b.Do(act, fn, buildCall, args...) } @@ -1048,8 +1797,25 @@ func collectMethodNilDerefChecks(fn *ssa.Function) map[*ssa.UnOp]none { return checks } +func (p *context) recoverTransparentWrapperCall(call *ssa.CallCommon) bool { + if p.goFn == nil || call.StaticCallee() == nil { + return false + } + name := p.goFn.Name() + if strings.HasSuffix(name, "$thunk") || strings.HasSuffix(name, "$bound") { + return true + } + synthetic := p.goFn.Synthetic + return strings.Contains(synthetic, "thunk") || strings.Contains(synthetic, "bound") || strings.Contains(synthetic, "wrapper") +} + func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallCommon, ds *explicitDeferStack) (ret llssa.Expr) { + p.recordCallerLocationForCall(b, call) + p.emitPCLineLabel(b, call.Pos()) cv := call.Value + calleeUsesRecover := functionUsesRecover(call.StaticCallee()) + forwardRecover := act == llssa.Call && calleeUsesRecover && p.recoverTransparentWrapperCall(call) + maskRecover := act == llssa.Call && calleeUsesRecover && !forwardRecover if mthd := call.Method; mthd != nil { reflectCheck := p.reflectTypeMethodCheck(call, mthd) o := p.compileValue(b, cv) @@ -1059,7 +1825,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.emitDoEx(b, act, ds, maskRecover, forwardRecover, fn, llssa.Builder.Call, args...) b.EmitReflectTypeMethodCheckedLoad(ret, reflectCheck) return } @@ -1090,7 +1856,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.emitDoEx(b, act, ds, maskRecover, forwardRecover, llssa.Builtin(fn), llssa.Builder.Call, args...) case *ssa.Function: aFn, pyFn, ftype := p.compileFunction(cv) // TODO(xsw): check ca != llssa.Call @@ -1099,13 +1865,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.emitDoEx(b, act, ds, maskRecover, forwardRecover, 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.emitDoEx(b, act, ds, maskRecover, forwardRecover, 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.emitDoEx(b, act, ds, maskRecover, forwardRecover, pyFn.Expr, llssa.Builder.Call, args...) case llgoPyList: args := p.compileValues(b, args, fnHasVArg) ret = b.PyList(args...) @@ -1215,7 +1981,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.emitDoEx(b, act, ds, maskRecover, forwardRecover, fn, llssa.Builder.Call, args...) } return } diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index 35112239f6..ee6ae42d8f 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -474,6 +474,76 @@ func TestEmitDoWithoutExplicitDeferStack(t *testing.T) { } } +func TestEmitDoRecoverFrameModes(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + pkg := prog.NewPackage("foo", "foo") + + callee := pkg.NewFunc("callee", llssa.NoArgsNoRet, llssa.InGo) + cb := callee.MakeBody(1) + cb.Return() + cb.EndBuild() + + fn := pkg.NewFunc("main", llssa.NoArgsNoRet, llssa.InGo) + b := fn.MakeBody(1) + + ctx := &context{} + ctx.emitDoEx(b, llssa.Call, nil, true, false, callee.Expr, llssa.Builder.Call) + ctx.emitDoEx(b, llssa.Call, nil, false, true, callee.Expr, llssa.Builder.Call) + b.Return() + b.EndBuild() + + ir := pkg.String() + for _, want := range []string{ + "runtime/internal/runtime.StartRecoverFrame", + "runtime/internal/runtime.StartRecoverWrapperFrame", + "runtime/internal/runtime.EndRecoverFrame", + "llvm.frameaddress.p0", + } { + if !strings.Contains(ir, want) { + t.Fatalf("missing %s in recover-frame emitDoEx IR:\n%s", want, ir) + } + } +} + +func TestRecoverTransparentWrapperCall(t *testing.T) { + const src = `package foo + +func usesRecover() { recover() } + +func caller() { usesRecover() } +` + ssapkg := buildSSAPackage(t, src) + callee := ssapkg.Func("usesRecover") + caller := ssapkg.Func("caller") + call := findStaticCallByName(t, caller, "usesRecover") + + if !functionUsesRecover(callee) { + t.Fatal("usesRecover should be detected as using recover") + } + if functionUsesRecover(caller) { + t.Fatal("caller should not be detected as using recover directly") + } + + ctx := &context{} + if ctx.recoverTransparentWrapperCall(call.Common()) { + t.Fatal("nil current function should not be recover transparent") + } + ctx.goFn = caller + if ctx.recoverTransparentWrapperCall(call.Common()) { + t.Fatal("plain caller should not be recover transparent") + } + + caller.Synthetic = "bound method wrapper" + if !ctx.recoverTransparentWrapperCall(call.Common()) { + t.Fatal("synthetic wrapper caller should be recover transparent") + } + + ctx.goFn = ssapkg.Prog.NewFunction("caller$thunk", caller.Signature, "test") + if !ctx.recoverTransparentWrapperCall(call.Common()) { + t.Fatal("thunk-suffixed caller should be recover transparent") + } +} + func TestNestedRangeFuncDeferAnalysisCombinations(t *testing.T) { const src = `package foo diff --git a/doc/design/pclntab-linkphase.md b/doc/design/pclntab-linkphase.md new file mode 100644 index 0000000000..cbbbaf776e --- /dev/null +++ b/doc/design/pclntab-linkphase.md @@ -0,0 +1,132 @@ +# Link-phase ftab/findfunctab generation + +Status: design + staged plan. Depends on #2012 (runtime funcinfo find index) +and benefits from #2015 (nanosecond monotonic clock, for honest benchmarks). + +## Problem + +#2012 builds the sorted function-entry table and the Go-style findfunctab at +**first use in the running process**, because LLVM IR generation does not know +final linked text order. This leaves four measured gaps against Go 1.26: + +1. `cold.FirstFuncForPC`: 36µs on macOS / 12µs on Linux vs Go's 2.4µs / 375ns. + The cold fast path (bounded linear scan of raw entry sections, then dladdr) + is a transitional mechanism; Go needs none of it because the linker ships a + sorted table. +2. LTO inlining duplicates the body-embedded entry-site inline asm into every + inline site: `llgo_funcinfo_entry` grew ~4x on the multipkg benchmark and + host-function PCs get registered under the inlinee's symbol ID. IR-level + fixes were tried and ruled out (see Facts below); dedup must happen after + final code generation. +3. The runtime keeps ~300 lines of transitional complexity: cold lookup + budget, section scans, first-use sort, entry-PC slack matching. +4. pcvalue-style instruction-level line tables (the next alignment step with + Go) need a per-function table keyed by final text order. + +## Approach: post-link table generation + +Insert a post-link step into `internal/build` after the final clang/lld link: + +``` +link -> post-link tool: parse binary -> sort/dedup -> build buckets -> write back +``` + +A separate linker plugin was considered and rejected: llgo drives stock +clang/lld and a plugin would need to be maintained per linker flavor +(ld64.lld, ld.lld) and per LTO mode. Editing the linked artifact is +linker-agnostic. + +### Data flow + +1. **Parse** the linked binary's metadata sections (`debug/elf`, + `debug/macho` from the Go stdlib — the tool runs on the host): + - `llgo_funcinfo_entry` / `__DATA,__llgo_fie`: `{pc, symbolID}` records. + - `llgo_funcinfo_stubsite` / `__DATA,__llgo_stub`: same layout. + - Zero records are skipped, as in the runtime today. +2. **Dedup by symbolID**: LTO inline copies register the same symbolID at + several PCs. The true entry is the record whose PC lies inside the text + range of the symbol that owns the symbolID; resolve via the binary's + symbol table (`.symtab` / `nlist`). Records that fall inside a different + function's range are inline copies — drop them. This is the fix for gap 2 + that IR-level metadata could not express. +3. **Sort** by PC; append a sentinel entry (end of text) so the runtime can + use Go's forward-scan lookup shape (`internal/pclntab.LookupFuncIndex`). +4. **Build buckets** with `internal/pclntab.BuildFindFuncBuckets` — the + faithful port of `cmd/link`'s algorithm that has been sitting unwired + since #2012. Delta overflow is a hard error here, mirroring Go's linker; + if it ever fires, fall back to leaving the prebuilt table absent. +5. **Write back** into a reserved section: + - The main module already emits `__llgo_funcinfo_*` globals; add a + `__llgo_pclntab_prebuilt` global sized from the collected package data + (entry-record count is known at main-module emission time; LTO can only + shrink it after dedup) plus a header {magic, version, count, anchorOff}. + - The tool rewrites the section contents in place (same size or smaller; + unused tail is zeroed) and flips the header magic to "valid". + +### ASLR + +Stored PCs must survive load-time slide. Store **offsets relative to an +anchor symbol** (`__llgo_pclntab_anchor`, placed in the same section). At +startup the runtime computes `slide = &anchor_runtime - anchorOff_stored` +and adds it during lookup (one add on the hot path, same as Go's +`datap.text` bias). Note the entry-site records themselves are already +rebased by the loader (they hold absolute pointers with relocations); the +prebuilt table deliberately holds offsets so the tool does not need to +emit relocations. + +### Runtime integration + +`initRuntimeFuncPCFramesOnce` gains a fast path: if the prebuilt header is +valid, adopt the table directly (no section scan, no sort, no bucket build) +— `FirstFuncForPC` becomes bucket-lookup cost, matching Go's shape. The +existing first-use construction remains as the fallback whenever the header +is invalid (older compilers, exotic formats, overflow bail-out), so the +change is strictly additive and safe to land incrementally. + +## Staging + +- **P1** `chore/pclnpost`: standalone tool, parse + dedup + sort + bucket + build + stats printing; golden tests against binaries produced by the + existing test programs. No behavior change. +- **P2** Reserve the section in `internal/build`, run the tool as a post-link + step, wire the runtime fast path. Benchmarks: cold.FirstFuncForPC on both + platforms; assert `llgo funcinfo: ... entries= prebuilt` via + LLGO_FUNCINFO_DEBUG. +- **P3** (done) Mach-O bind-record resolution: pointer slots naming exported + functions — every `__llgo_stub.*` and any exported Go function — are + chained-fixup BIND nodes, not rebases; without decoding them through the + imports table, all stub records miss the prebuilt ftab and function-value + `FuncForPC` silently pays a dladdr per fresh pc (~6µs). Also: the prebuilt + header's base slot is spliced back into the fixup chain as a live rebase + node, so the runtime reads a dyld-slid runtime PC directly (no slide + arithmetic). Transitional cold budget/scan stays as the fallback for + non-rewritten binaries. +- **P4** pcvalue-style line tables keyed by the prebuilt function order + (replaces the call-site pcline records; gives instruction-level FileLine). + +## Established facts (verified in #2012 work; do not re-derive) + +- Mach-O metadata sections need `live_support` + one lowercase-`l` + linker-private symbol per record; ld64/lld `-dead_strip` then drops records + exactly with their function. Verified with lld 19.1.7, including LTO. +- Boundary symbols: ELF `__start_/__stop_`; Mach-O `section$start$SEG$SECT` + referenced from IR needs the `\x01` verbatim-name prefix or LLVM prepends + an underscore and the linker stops recognizing it. +- Visible (non-`L`) labels inside Mach-O function bodies split the function + into atoms that the linker may reorder — assembler-local labels only. +- `!associated` affects only linker GC; IR-level GlobalDCE deletes such + globals regardless, and `llvm.compiler.used` pins dead functions through + the records' initializers. This is why records stay body-embedded inline + asm and dedup happens post-link. +- Mach-O chained fixups encode anchors to exported symbols as BIND nodes + (import ordinal + addend), even when the target is defined in the same + image; only local-symbol anchors are rebases. Decode both. +- Adding fixup nodes does not pre-touch pages at load on modern macOS: + dyld uses page-in linking (the kernel applies fixups lazily at first + touch), so "sacrificial fixups to warm the table's pages" is not a + viable optimization — measured no effect on first-lookup latency. +- `internal/pclntab` is a faithful port of Go 1.26's findfunctab generation + and lookup (uint8 deltas, overflow error, forward scan, sentinel); the + runtime's in-process variant deliberately uses uint16 deltas because LLGo + lacks Go's MINFUNC guarantee. The post-link table can use the faithful + uint8 layout since dedup restores the one-record-per-function invariant. diff --git a/internal/build/build.go b/internal/build/build.go index 149411815f..db02fd67f3 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -52,6 +52,7 @@ import ( "github.com/goplus/llgo/internal/monitor" "github.com/goplus/llgo/internal/optlevel" "github.com/goplus/llgo/internal/packages" + "github.com/goplus/llgo/internal/pclnpost" "github.com/goplus/llgo/internal/typepatch" "github.com/goplus/llgo/ssa/abi" xenv "github.com/goplus/llgo/xtool/env" @@ -318,6 +319,16 @@ func Do(args []string, conf *Config) ([]Package, error) { prog := llssa.NewProgram(target) prog.EnableGoGlobalDCE(conf.goGlobalDCEEnabled()) + funcInfo := conf.Mode != ModeGen && IsFuncInfoEnabled() + prog.EnableFuncInfoMetadata(funcInfo) + // Site records are inline-asm fragments inside function bodies; their + // anchors shift instruction/scope layout enough to confuse debuggers + // (LLDB reported variables from an inner lexical block as in scope before + // the block began). Debug builds keep the metadata tables — FuncForPC + // name/FileLine fidelity survives via the dlsym path — but drop the + // sites. Caller-frame instrumentation is independent of both switches, + // so runtime.Caller keeps working in debug builds. + prog.EnableFuncInfoSites(funcInfo && !IsDbgEnabled() && IsFuncInfoSitesEnabled()) sizes := func(sizes types.Sizes, compiler, arch string) types.Sizes { if arch == "wasm" { sizes = &types.StdSizes{WordSize: 4, MaxAlign: 4} @@ -467,6 +478,7 @@ func Do(args []string, conf *Config) ([]Package, error) { if err != nil { return nil, err } + rewritePrebuiltFuncTab(ctx, outFmts.Out, verbose) if conf.Mode == ModeBuild && conf.SizeReport { if err := reportBinarySize(outFmts.Out, conf.SizeFormat, conf.SizeLevel, allPkgs); err != nil { fmt.Fprintf(os.Stderr, "Warning: size report failed: %v\n", err) @@ -960,6 +972,35 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { return objFiles, nil } +// rewritePrebuiltFuncTab runs the link-phase prebuilt-table rewrite on the +// linked executable: it deduplicates LTO inline copies of the funcinfo entry +// records against the symbol table and replaces the entry section with a +// sorted ftab plus findfunctab that the runtime adopts zero-copy (see +// internal/pclnpost and doc/design/pclntab-linkphase.md). Any failure leaves +// the binary fully functional on the first-use construction fallback. +func rewritePrebuiltFuncTab(ctx *context, out string, verbose bool) { + if ctx == nil || ctx.prog == nil || !ctx.prog.FuncInfoSitesEnabled() || !shouldEmitRuntimeSites(ctx) { + return + } + if ctx.buildConf.BuildMode != BuildModeExe { + return + } + if os.Getenv("LLGO_PCLNPOST") == "0" { // escape hatch: keep first-use construction + return + } + st, err := pclnpost.Rewrite(out) + if err != nil { + if verbose { + fmt.Fprintf(os.Stderr, "llgo: prebuilt functab rewrite skipped: %v\n", err) + } + return + } + if verbose { + fmt.Fprintf(os.Stderr, "llgo: prebuilt functab: %d entries (%d LTO inline copies removed), %d buckets\n", + st.FtabEntries, st.InlineCopies, st.Buckets) + } +} + func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPath string, verbose bool) error { needRuntime := false needPyInit := false @@ -1043,6 +1084,9 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa // Generate main module file (needed for global variables even in library modes) // This is compiled directly to .o and added to linkInputs (not cached) // Use a stable synthetic name to avoid confusing it with the real main package in traces/logs. + funcInfo := prepareFuncInfoTableRecords(collectFuncInfo(linkedOrder), nil) + pcLineInfo := collectPCLineInfo(linkedOrder) + funcInfoStubs := collectFuncInfoStubRecords(linkedOrder, funcInfo) entryPkg := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{ rtInit: needRuntime, pyInit: needPyInit, @@ -1050,6 +1094,9 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa methodByIndex: methodByIndex, methodByName: methodByName, abiSymbols: linkedModuleGlobals(linkedOrder), + funcInfo: funcInfo, + pcLineInfo: pcLineInfo, + funcInfoStubs: funcInfoStubs, }) entryObjFile, err := exportObject(ctx, "entry_main", entryPkg.ExportFile, entryPkg.LPkg) if err != nil { @@ -1100,6 +1147,9 @@ func linkedModuleGlobals(pkgs []Package) map[string]none { continue } for g := pkg.LPkg.Module().FirstGlobal(); !g.IsNil(); g = gllvm.NextGlobal(g) { + if g.IsDeclaration() { + continue + } seen[g.Name()] = none{} } } @@ -1130,9 +1180,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 +1228,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. @@ -1324,6 +1388,8 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error { return fmt.Errorf("run LLVM passes failed for %v: %v", pkgPath, err) } } + emitFuncInfoEntrySites(ctx, ret) + emitFuncInfoStubSites(ctx, ret) printCmds := ctx.shouldPrintCommands(verbose) cgoLLFiles, cgoLdflags, err := buildCgo(ctx, aPkg, aPkg.Package.Syntax, externs, printCmds) @@ -1796,6 +1862,8 @@ var ( const llgoDebug = "LLGO_DEBUG" const llgoDbgSyms = "LLGO_DEBUG_SYMBOLS" +const llgoFuncInfo = "LLGO_FUNCINFO" +const llgoFuncInfoSites = "LLGO_FUNCINFO_SITES" const llgoTrace = "LLGO_TRACE" const llgoOptimize = "LLGO_OPTIMIZE" const llgoWasmRuntime = "LLGO_WASM_RUNTIME" @@ -1843,6 +1911,18 @@ func IsDbgEnabled() bool { return isEnvOn(llgoDebug, false) || isEnvOn(llgoDbgSyms, false) } +func IsFuncInfoEnabled() bool { + return isEnvOn(llgoFuncInfo, true) +} + +// IsFuncInfoSitesEnabled controls the body-embedded site records +// independently of the funcinfo tables (LLGO_FUNCINFO_SITES=0 keeps the +// metadata but drops entry/stub/pc-line inline-asm sites). Useful for +// isolating codegen perturbation caused by the in-body asm anchors. +func IsFuncInfoSitesEnabled() bool { + return isEnvOn(llgoFuncInfoSites, true) +} + func IsDbgSymsEnabled() bool { return isEnvOn(llgoDbgSyms, false) } diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 51401c2131..c8ce08e9e6 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -22,6 +22,7 @@ import ( "github.com/goplus/llgo/internal/mockable" "github.com/goplus/llgo/internal/packages" llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" ) func TestMain(m *testing.M) { @@ -55,6 +56,68 @@ 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 TestLinkedModuleGlobalsSkipsDeclarations(t *testing.T) { + prog := llssa.NewProgram(nil) + lpkg := prog.NewPackage("example.com/p", "example.com/p") + mod := lpkg.Module() + i32 := mod.Context().Int32Type() + + defined := llvm.AddGlobal(mod, i32, "example.com/p.defined") + defined.SetInitializer(llvm.ConstInt(i32, 1, false)) + llvm.AddGlobal(mod, i32, "example.com/p.declared") + + got := linkedModuleGlobals([]Package{{LPkg: lpkg}}) + if _, ok := got["example.com/p.defined"]; !ok { + t.Fatalf("linkedModuleGlobals missing defined global: %#v", got) + } + if _, ok := got["example.com/p.declared"]; ok { + t.Fatalf("linkedModuleGlobals should skip external declarations: %#v", got) + } +} + 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..76c7cc123c --- /dev/null +++ b/internal/build/funcinfo/funcinfo.go @@ -0,0 +1,416 @@ +/* + * 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 PCLineRecord struct { + ID uint64 + Symbol string + File string + Line uint32 + Column uint32 +} + +type EncodedRecord struct { + SymbolPkg uint16 + SymbolName uint16 + NamePkg uint16 + NameName uint16 + FileRoot uint16 + FileName uint16 + Line uint32 +} + +type EncodedPCLineRecord struct { + ID uint64 + Func uint32 + File uint32 + Line uint32 +} + +type Table struct { + Records []EncodedRecord + PCLines []EncodedPCLineRecord + StringOffsets []uint32 + Strings []byte + Hash []uint16 +} + +func Encode(records []Record) (Table, error) { + return EncodeWithPCLines(records, nil) +} + +func EncodeWithPCLines(records []Record, pcLines []PCLineRecord) (Table, error) { + funcIndex := make(map[string]uint32, len(records)) + for i, rec := range records { + if rec.Symbol != "" { + funcIndex[rec.Symbol] = uint32(i + 1) + } + } + filteredPCLines := make([]PCLineRecord, 0, len(pcLines)) + for _, rec := range pcLines { + if rec.ID == 0 || funcIndex[rec.Symbol] == 0 { + continue + } + filteredPCLines = append(filteredPCLines, rec) + } + if len(records) == 0 && len(filteredPCLines) == 0 { + return Table{}, nil + } + ids, offsets, strings, err := buildStringTable(collectStrings(records, filteredPCLines)) + if err != nil { + return Table{}, err + } + out := Table{ + Records: make([]EncodedRecord, 0, len(records)), + StringOffsets: offsets, + Strings: strings, + } + for _, rec := range records { + symPkg, symName := splitQualifiedName(rec.Symbol) + namePkg, nameName := splitQualifiedName(rec.Name) + fileRoot, fileName := splitFileName(rec.File) + out.Records = append(out.Records, EncodedRecord{ + SymbolPkg: ids[symPkg], + SymbolName: ids[symName], + NamePkg: ids[namePkg], + NameName: ids[nameName], + FileRoot: ids[fileRoot], + FileName: ids[fileName], + Line: rec.Line, + }) + } + out.PCLines = make([]EncodedPCLineRecord, 0, len(filteredPCLines)) + for _, rec := range filteredPCLines { + idx := funcIndex[rec.Symbol] + fileRoot, fileName := splitFileName(rec.File) + out.PCLines = append(out.PCLines, EncodedPCLineRecord{ + ID: rec.ID, + Func: idx, + File: packStringIDs(ids[fileRoot], ids[fileName]), + Line: rec.Line, + }) + } + sort.Slice(out.PCLines, func(i, j int) bool { + return out.PCLines[i].ID < out.PCLines[j].ID + }) + out.Hash, err = buildHash(records) + if err != nil { + return Table{}, err + } + return out, nil +} + +func collectStrings(records []Record, pcLines []PCLineRecord) []string { + seen := make(map[string]bool) + for _, rec := range records { + for _, s := range splitRecordStrings(rec) { + seen[s] = true + } + } + for _, rec := range pcLines { + fileRoot, fileName := splitFileName(rec.File) + seen[fileRoot] = true + seen[fileName] = 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 +} + +func packStringIDs(hi, lo uint16) uint32 { + return uint32(hi)<<16 | uint32(lo) +} + +func splitRecordStrings(rec Record) []string { + symPkg, symName := splitQualifiedName(rec.Symbol) + namePkg, nameName := splitQualifiedName(rec.Name) + fileRoot, fileName := splitFileName(rec.File) + return []string{symPkg, symName, namePkg, nameName, fileRoot, fileName} +} + +func buildStringTable(strings []string) (map[string]uint16, []uint32, []byte, error) { + ids := map[string]uint16{"": 0} + values := []string{""} + for _, s := range strings { + if _, ok := ids[s]; ok { + continue + } + if len(values) > math.MaxUint16 { + return nil, nil, nil, fmt.Errorf("funcinfo string id table exceeds 65535 entries") + } + ids[s] = uint16(len(values)) + values = append(values, s) + } + pool := stringPool{ + offsets: map[string]uint32{"": 0}, + data: []byte{0}, + text: "\x00", + } + offsets := make([]uint32, len(values)) + for id, s := range values { + off, err := pool.offset(s) + if err != nil { + return nil, nil, nil, err + } + offsets[id] = off + } + return ids, offsets, pool.data, nil +} + +func splitQualifiedName(name string) (pkg, local string) { + if name == "" { + return "", "" + } + start := strings.LastIndexByte(name, '/') + if start < 0 { + start = 0 + } else { + start++ + } + if dot := strings.IndexByte(name[start:], '.'); dot >= 0 { + idx := start + dot + return name[:idx], name[idx+1:] + } + return "", name +} + +func splitFileName(file string) (root, name string) { + if file == "" { + return "", "" + } + if slash := strings.LastIndexByte(file, '/'); slash >= 0 { + return file[:slash+1], file[slash+1:] + } + return "", file +} + +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) ([]uint16, error) { + if len(records) == 0 { + return nil, nil + } + if len(records) > math.MaxUint16 { + // Runtime hash slots store 1-based uint16 record indexes. Larger + // tables remain correct by omitting the hash and using linear lookup. + return nil, nil + } + buckets := 1 + for buckets*3 < len(records)*4 { + buckets <<= 1 + } + hash := make([]uint16, 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] = uint16(i + 1) + } + return hash, nil +} + +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 +} + +func (t Table) String(id uint16) string { + if int(id) >= len(t.StringOffsets) { + return "" + } + return cstring(t.Strings, t.StringOffsets[id]) +} + +func (t Table) Symbol(rec EncodedRecord) string { + return joinQualified(t.String(rec.SymbolPkg), t.String(rec.SymbolName)) +} + +func (t Table) Name(rec EncodedRecord) string { + return joinQualified(t.String(rec.NamePkg), t.String(rec.NameName)) +} + +func (t Table) File(rec EncodedRecord) string { + return t.String(rec.FileRoot) + t.String(rec.FileName) +} + +func (t Table) PCLineFile(rec EncodedPCLineRecord) string { + return t.String(uint16(rec.File>>16)) + t.String(uint16(rec.File)) +} + +func (t Table) LookupSymbol(symbol string) (int, bool) { + if len(t.Hash) == 0 { + return 0, false + } + mask := uint32(len(t.Hash) - 1) + slot := HashString(symbol) & mask + for probes := 0; probes < len(t.Hash); probes++ { + idx := t.Hash[slot] + if idx == 0 { + return 0, false + } + rec := t.Records[idx-1] + if t.Symbol(rec) == symbol { + return int(idx - 1), true + } + slot = (slot + 1) & mask + } + return 0, false +} + +func (t Table) SizeBytes() int { + return len(t.Records)*16 + len(t.PCLines)*24 + len(t.StringOffsets)*4 + len(t.Strings) + len(t.Hash)*2 +} + +func joinQualified(pkg, local string) string { + if pkg == "" { + return local + } + if local == "" { + return pkg + } + return pkg + "." + local +} + +func cstring(data []byte, off uint32) string { + end := int(off) + for end < len(data) && data[end] != 0 { + end++ + } + return string(data[off:end]) +} + +type PCIndex struct { + PageShift uint + Base uint64 + Pages []uint32 +} + +const DefaultPCPageShift = 12 + +func BuildPCIndex(entries []uint64) PCIndex { + return BuildPCIndexWithShift(entries, DefaultPCPageShift) +} + +func BuildPCIndexWithShift(entries []uint64, shift uint) PCIndex { + if len(entries) == 0 { + return PCIndex{PageShift: shift} + } + base := entries[0] >> shift + last := entries[len(entries)-1] >> shift + pages := make([]uint32, last-base+2) + next := 0 + for page := range pages { + limit := (base + uint64(page)) << shift + for next < len(entries) && entries[next] < limit { + next++ + } + pages[page] = uint32(next) + } + return PCIndex{ + PageShift: shift, + Base: base, + Pages: pages, + } +} + +func LookupPC(entries []uint64, index PCIndex, pc uint64) int { + if len(entries) == 0 { + return -1 + } + lo, hi := 0, len(entries) + page := pc >> index.PageShift + if len(index.Pages) != 0 && page >= index.Base { + off := page - index.Base + if off < uint64(len(index.Pages)) { + lo = int(index.Pages[off]) + if off+1 < uint64(len(index.Pages)) { + hi = int(index.Pages[off+1]) + } + if lo > 0 { + lo-- + } + if hi < len(entries) { + hi++ + } + } + } + i := sort.Search(hi-lo, func(i int) bool { + return entries[lo+i] > pc + }) + idx := lo + i - 1 + if idx < 0 { + return -1 + } + return idx +} diff --git a/internal/build/funcinfo/funcinfo_test.go b/internal/build/funcinfo/funcinfo_test.go new file mode 100644 index 0000000000..78a59fad2a --- /dev/null +++ b/internal/build/funcinfo/funcinfo_test.go @@ -0,0 +1,242 @@ +/* + * 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].FileRoot == table.Records[1].FileRoot { + t.Fatalf("distinct file roots should use distinct ids") + } + if got := table.File(table.Records[1]); 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 := table.LookupSymbol("example.com/p.a"); !ok || idx != 0 { + t.Fatalf("lookup a = %d, %v; want 0, true", idx, ok) + } + if idx, ok := table.LookupSymbol("example.com/p.b"); !ok || idx != 1 { + t.Fatalf("lookup b = %d, %v; want 1, true", idx, ok) + } + if _, ok := table.LookupSymbol("missing"); ok { + t.Fatalf("lookup missing succeeded") + } +} + +func TestEncodeWithPCLines(t *testing.T) { + table, err := EncodeWithPCLines( + []Record{ + {Symbol: "example.com/p.f", Name: "example.com/p.F", File: "/src/p/f.go", Line: 10, Column: 1}, + {Symbol: "example.com/p.g", Name: "example.com/p.G", File: "/src/p/g.go", Line: 20, Column: 1}, + }, + []PCLineRecord{ + {ID: 3, Symbol: "missing", File: "missing.go", Line: 30}, + {ID: 2, Symbol: "example.com/p.g", File: "/src/p/call_g.go", Line: 22}, + {ID: 1, Symbol: "example.com/p.f", File: "/src/p/call_f.go", Line: 12}, + {ID: 0, Symbol: "example.com/p.f", File: "zero.go", Line: 1}, + }, + ) + if err != nil { + t.Fatal(err) + } + if len(table.PCLines) != 2 { + t.Fatalf("encoded pclines = %d, want 2", len(table.PCLines)) + } + if got := table.PCLines[0]; got.ID != 1 || got.Func != 1 || got.Line != 12 { + t.Fatalf("first pcline = %+v, want id 1 func 1 line 12", got) + } + if got := table.PCLines[1]; got.ID != 2 || got.Func != 2 || got.Line != 22 { + t.Fatalf("second pcline = %+v, want id 2 func 2 line 22", got) + } + if got := table.PCLineFile(table.PCLines[0]); got != "/src/p/call_f.go" { + t.Fatalf("pcline file = %q, want /src/p/call_f.go", got) + } +} + +func TestEncodeRoundTripsSingleRecord(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 := table.Symbol(rec), "s"; got != want { + t.Fatalf("symbol = %q, want %q", got, want) + } + if got, want := table.Name(rec), "n"; got != want { + t.Fatalf("name = %q, want %q", got, want) + } + if got, want := table.File(rec), "f"; got != want { + t.Fatalf("file = %q, want %q", got, want) + } + if rec.Line != 1 { + t.Fatalf("source line = %d, want 1", rec.Line) + } +} + +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 := table.LookupSymbol(a); !ok || idx != 0 { + t.Fatalf("lookup collision a = %d, %v; want 0, true", idx, ok) + } + if idx, ok := table.LookupSymbol(b); !ok || idx != 1 { + t.Fatalf("lookup collision b = %d, %v; want 1, true", idx, ok) + } +} + +func TestEncodeOmitsHashWhenRecordIndexesDoNotFitUint16(t *testing.T) { + records := make([]Record, 1<<16) + for i := range records { + records[i] = Record{Symbol: "example.com/p.f", Name: "example.com/p.F"} + } + table, err := Encode(records) + if err != nil { + t.Fatal(err) + } + if table.Hash != nil { + t.Fatalf("hash buckets = %d, want nil fallback for oversized table", len(table.Hash)) + } + if len(table.Records) != len(records) { + t.Fatalf("records = %d, want %d", len(table.Records), len(records)) + } +} + +func TestEncodeSplitsPackageAndFilePrefixes(t *testing.T) { + records := []Record{ + {Symbol: "example.com/p.alpha", Name: "example.com/p.Alpha", File: "/home/me/mod/p/alpha.go", Line: 10}, + {Symbol: "example.com/p.beta", Name: "example.com/p.Beta", File: "/home/me/mod/p/beta.go", Line: 20}, + {Symbol: "example.com/q.gamma", Name: "example.com/q.Gamma", File: "/home/me/mod/q/gamma.go", Line: 30}, + } + table, err := Encode(records) + if err != nil { + t.Fatal(err) + } + for i, rec := range table.Records { + if got := table.Symbol(rec); got != records[i].Symbol { + t.Fatalf("record %d symbol = %q, want %q", i, got, records[i].Symbol) + } + if got := table.Name(rec); got != records[i].Name { + t.Fatalf("record %d name = %q, want %q", i, got, records[i].Name) + } + if got := table.File(rec); got != records[i].File { + t.Fatalf("record %d file = %q, want %q", i, got, records[i].File) + } + } + if table.Records[0].SymbolPkg != table.Records[1].SymbolPkg { + t.Fatalf("same package prefix got different ids: %d vs %d", table.Records[0].SymbolPkg, table.Records[1].SymbolPkg) + } + if table.Records[0].FileRoot != table.Records[1].FileRoot { + t.Fatalf("same file root got different ids: %d vs %d", table.Records[0].FileRoot, table.Records[1].FileRoot) + } + if got := table.SizeBytes(); got >= legacySizeBytes(records) { + t.Fatalf("compressed table size = %d, want below legacy %d", got, legacySizeBytes(records)) + } +} + +func TestLookupPCUsesPageIndex(t *testing.T) { + entries := []uint64{0x1000, 0x1010, 0x2800, 0x4000, 0x4010} + index := BuildPCIndex(entries) + tests := []struct { + pc uint64 + want int + }{ + {0xfff, -1}, + {0x1000, 0}, + {0x100f, 0}, + {0x1010, 1}, + {0x27ff, 1}, + {0x2800, 2}, + {0x4018, 4}, + } + for _, tt := range tests { + if got := LookupPC(entries, index, tt.pc); got != tt.want { + t.Fatalf("LookupPC(%#x) = %d, want %d", tt.pc, got, tt.want) + } + } +} + +func BenchmarkLookupPCRandom(b *testing.B) { + entries := make([]uint64, 8192) + for i := range entries { + entries[i] = 0x100000 + uint64(i)*37 + } + index := BuildPCIndex(entries) + var sum int + for i := 0; i < b.N; i++ { + pc := entries[(i*1103515245+12345)&(len(entries)-1)] + uint64(i&31) + sum += LookupPC(entries, index, pc) + } + if sum == 0 { + b.Fatal(sum) + } +} + +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 legacySizeBytes(records []Record) int { + seen := make(map[string]bool) + stringsBytes := 1 + for _, rec := range records { + for _, s := range []string{rec.Symbol, rec.Name, rec.File} { + if s == "" || seen[s] { + continue + } + seen[s] = true + stringsBytes += len(s) + 1 + } + } + buckets := 1 + for buckets*3 < len(records)*4 { + buckets <<= 1 + } + return len(records)*20 + stringsBytes + buckets*4 +} diff --git a/internal/build/funcinfo_table.go b/internal/build/funcinfo_table.go new file mode 100644 index 0000000000..ea57e7a865 --- /dev/null +++ b/internal/build/funcinfo_table.go @@ -0,0 +1,961 @@ +/* + * 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" + "strings" + + "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" + funcInfoStringOffsetsSymbol = "__llgo_funcinfo_string_offsets" + funcInfoStringCountSymbol = "__llgo_funcinfo_string_count" + funcInfoHashSymbol = "__llgo_funcinfo_hash" + funcInfoHashMaskSymbol = "__llgo_funcinfo_hash_mask" + funcInfoSymbolIndexSymbol = "__llgo_funcinfo_symbol_index" + funcInfoSymbolIndexCountSymbol = "__llgo_funcinfo_symbol_index_count" + funcInfoStubIndexesSymbol = "__llgo_funcinfo_stub_indexes" + funcInfoStubCountSymbol = "__llgo_funcinfo_stub_count" + funcInfoEntryStartPtrSymbol = "__llgo_funcinfo_entry_start" + funcInfoEntryEndPtrSymbol = "__llgo_funcinfo_entry_end" + funcInfoStubSiteStartPtrSymbol = "__llgo_funcinfo_stubsite_start" + funcInfoStubSiteEndPtrSymbol = "__llgo_funcinfo_stubsite_end" + pcLineTableSymbol = "__llgo_pcline_table" + pcLineCountSymbol = "__llgo_pcline_count" + pcSiteStartPtrSymbol = "__llgo_pcsite_start" + pcSiteEndPtrSymbol = "__llgo_pcsite_end" + funcInfoDataSymbol = "__llgo_funcinfo_table$data" + pcLineDataSymbol = "__llgo_pcline_table$data" + funcInfoStringsDataSymbol = "__llgo_funcinfo_strings$data" + funcInfoStringOffsetsDataSymbol = "__llgo_funcinfo_string_offsets$data" + funcInfoHashDataSymbol = "__llgo_funcinfo_hash$data" + funcInfoSymbolIndexDataSymbol = "__llgo_funcinfo_symbol_index$data" + funcInfoStubIndexesDataSymbol = "__llgo_funcinfo_stub_indexes$data" + closureStubPrefix = "__llgo_stub." +) + +type funcInfoRecord struct { + symbol string + name string + file string + line uint32 + column uint32 +} + +type pcLineRecord struct { + id uint64 + symbol string + file string + line uint32 + column uint32 +} + +type funcInfoStubRecord struct { + symbol string + funcIndex uint32 +} + +type funcInfoSymbolIndexRecord struct { + symbolID uint64 + funcIndex 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 collectPCLineInfo(pkgs []Package) []pcLineRecord { + var out []pcLineRecord + seen := make(map[uint64]none) + for _, pkg := range pkgs { + if pkg == nil || pkg.LPkg == nil { + continue + } + for _, rec := range readPCLineInfo(pkg.LPkg.Module()) { + if rec.id == 0 || rec.symbol == "" { + continue + } + if _, ok := seen[rec.id]; ok { + continue + } + seen[rec.id] = none{} + out = append(out, rec) + } + } + sort.Slice(out, func(i, j int) bool { + if out[i].symbol != out[j].symbol { + return out[i].symbol < out[j].symbol + } + if out[i].line != out[j].line { + return out[i].line < out[j].line + } + return out[i].id < out[j].id + }) + return out +} + +func collectFuncInfoStubRecords(pkgs []Package, records []funcInfoRecord) []funcInfoStubRecord { + if len(records) == 0 { + return nil + } + recordBySymbol := make(map[string]uint32, len(records)) + for i, rec := range records { + if rec.symbol != "" { + recordBySymbol[rec.symbol] = uint32(i + 1) + } + } + seen := make(map[string]funcInfoStubRecord) + for _, pkg := range pkgs { + if pkg == nil || pkg.LPkg == nil { + continue + } + fn := pkg.LPkg.Module().FirstFunction() + for !fn.IsNil() { + if fn.IsDeclaration() || fn.BasicBlocksCount() == 0 { + fn = llvm.NextFunction(fn) + continue + } + name := fn.Name() + if target, ok := strings.CutPrefix(name, closureStubPrefix); ok { + if idx := recordBySymbol[target]; idx != 0 { + seen[name] = funcInfoStubRecord{symbol: name, funcIndex: idx} + } + } + fn = llvm.NextFunction(fn) + } + } + if len(seen) == 0 { + return nil + } + out := make([]funcInfoStubRecord, 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 collectFuncInfoSymbolIndexRecords(records []funcInfoRecord) []funcInfoSymbolIndexRecord { + if len(records) == 0 { + return nil + } + seen := make(map[uint64]uint32, len(records)) + for i, rec := range records { + if rec.symbol == "" { + continue + } + id := funcInfoSymbolID(rec.symbol) + idx := uint32(i + 1) + if prev, ok := seen[id]; ok && prev != idx { + seen[id] = 0 + continue + } + seen[id] = idx + } + if len(seen) == 0 { + return nil + } + out := make([]funcInfoSymbolIndexRecord, 0, len(seen)) + for id, idx := range seen { + if id == 0 || idx == 0 { + continue + } + out = append(out, funcInfoSymbolIndexRecord{symbolID: id, funcIndex: idx}) + } + sort.Slice(out, func(i, j int) bool { + return out[i].symbolID < out[j].symbolID + }) + 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 readPCLineInfo(mod llvm.Module) []pcLineRecord { + rows := mod.NamedMetadataOperands(llssa.PCLineMetadataName) + if len(rows) == 0 { + return nil + } + out := make([]pcLineRecord, 0, len(rows)) + for _, row := range rows { + fields := row.MDNodeOperands() + if len(fields) != 6 || fields[0].ZExtValue() != 1 { + continue + } + if !fields[2].IsAMDString() || !fields[3].IsAMDString() { + continue + } + out = append(out, pcLineRecord{ + id: fields[1].ZExtValue(), + symbol: 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, pcLines []pcLineRecord, stubRecords []funcInfoStubRecord) { + mod := pkg.Module() + llvmCtx := mod.Context() + i8Type := llvmCtx.Int8Type() + i16Type := llvmCtx.Int16Type() + i32Type := llvmCtx.Int32Type() + i64Type := llvmCtx.Int64Type() + countType := llvmCtx.IntType(ctx.prog.PointerSize() * 8) + recordType := llvmCtx.StructType([]llvm.Type{ + i16Type, + i16Type, + i16Type, + i16Type, + i16Type, + i16Type, + i32Type, + }, false) + pcLineRecordType := llvmCtx.StructType([]llvm.Type{ + i64Type, + i32Type, + i32Type, + i32Type, + }, false) + symbolIndexRecordType := llvmCtx.StructType([]llvm.Type{ + i64Type, + i32Type, + }, false) + funcEntryRecordType := llvmCtx.StructType([]llvm.Type{ + llvm.PointerType(i8Type, 0), + i64Type, + }, false) + stubSiteRecordType := llvmCtx.StructType([]llvm.Type{ + llvm.PointerType(i8Type, 0), + i64Type, + }, false) + pcSiteRecordType := llvmCtx.StructType([]llvm.Type{ + llvm.PointerType(i8Type, 0), + i64Type, + }, false) + + tablePtr := llvm.AddGlobal(mod, llvm.PointerType(recordType, 0), funcInfoTableSymbol) + pcLinePtr := llvm.AddGlobal(mod, llvm.PointerType(pcLineRecordType, 0), pcLineTableSymbol) + pcSiteStartPtr := llvm.AddGlobal(mod, llvm.PointerType(pcSiteRecordType, 0), pcSiteStartPtrSymbol) + pcSiteEndPtr := llvm.AddGlobal(mod, llvm.PointerType(pcSiteRecordType, 0), pcSiteEndPtrSymbol) + entryStartPtr := llvm.AddGlobal(mod, llvm.PointerType(funcEntryRecordType, 0), funcInfoEntryStartPtrSymbol) + entryEndPtr := llvm.AddGlobal(mod, llvm.PointerType(funcEntryRecordType, 0), funcInfoEntryEndPtrSymbol) + stubSiteStartPtr := llvm.AddGlobal(mod, llvm.PointerType(stubSiteRecordType, 0), funcInfoStubSiteStartPtrSymbol) + stubSiteEndPtr := llvm.AddGlobal(mod, llvm.PointerType(stubSiteRecordType, 0), funcInfoStubSiteEndPtrSymbol) + stringsPtr := llvm.AddGlobal(mod, llvm.PointerType(i8Type, 0), funcInfoStringsSymbol) + stringOffsetsPtr := llvm.AddGlobal(mod, llvm.PointerType(i32Type, 0), funcInfoStringOffsetsSymbol) + stringCount := llvm.AddGlobal(mod, countType, funcInfoStringCountSymbol) + hashPtr := llvm.AddGlobal(mod, llvm.PointerType(i16Type, 0), funcInfoHashSymbol) + symbolIndexPtr := llvm.AddGlobal(mod, llvm.PointerType(symbolIndexRecordType, 0), funcInfoSymbolIndexSymbol) + count := llvm.AddGlobal(mod, countType, funcInfoCountSymbol) + symbolIndexCount := llvm.AddGlobal(mod, countType, funcInfoSymbolIndexCountSymbol) + stubIndexesPtr := llvm.AddGlobal(mod, llvm.PointerType(i32Type, 0), funcInfoStubIndexesSymbol) + stubCount := llvm.AddGlobal(mod, countType, funcInfoStubCountSymbol) + pcLineCount := llvm.AddGlobal(mod, countType, pcLineCountSymbol) + hashMask := llvm.AddGlobal(mod, countType, funcInfoHashMaskSymbol) + if len(records) == 0 && len(pcLines) == 0 { + tablePtr.SetInitializer(llvm.ConstPointerNull(tablePtr.GlobalValueType())) + pcLinePtr.SetInitializer(llvm.ConstPointerNull(pcLinePtr.GlobalValueType())) + pcSiteStartPtr.SetInitializer(llvm.ConstPointerNull(pcSiteStartPtr.GlobalValueType())) + pcSiteEndPtr.SetInitializer(llvm.ConstPointerNull(pcSiteEndPtr.GlobalValueType())) + entryStartPtr.SetInitializer(llvm.ConstPointerNull(entryStartPtr.GlobalValueType())) + entryEndPtr.SetInitializer(llvm.ConstPointerNull(entryEndPtr.GlobalValueType())) + stubSiteStartPtr.SetInitializer(llvm.ConstPointerNull(stubSiteStartPtr.GlobalValueType())) + stubSiteEndPtr.SetInitializer(llvm.ConstPointerNull(stubSiteEndPtr.GlobalValueType())) + stringsPtr.SetInitializer(llvm.ConstPointerNull(stringsPtr.GlobalValueType())) + stringOffsetsPtr.SetInitializer(llvm.ConstPointerNull(stringOffsetsPtr.GlobalValueType())) + stringCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + hashPtr.SetInitializer(llvm.ConstPointerNull(hashPtr.GlobalValueType())) + symbolIndexPtr.SetInitializer(llvm.ConstPointerNull(symbolIndexPtr.GlobalValueType())) + count.SetInitializer(llvm.ConstInt(countType, 0, false)) + symbolIndexCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + stubIndexesPtr.SetInitializer(llvm.ConstPointerNull(stubIndexesPtr.GlobalValueType())) + stubCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + pcLineCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + hashMask.SetInitializer(llvm.ConstInt(countType, 0, false)) + return + } + + encoded, err := buildfuncinfo.EncodeWithPCLines(toFuncInfoRecords(records), toPCLineRecords(pcLines)) + if err != nil { + panic(err) + } + if len(encoded.Records) == 0 && len(encoded.PCLines) == 0 { + tablePtr.SetInitializer(llvm.ConstPointerNull(tablePtr.GlobalValueType())) + pcLinePtr.SetInitializer(llvm.ConstPointerNull(pcLinePtr.GlobalValueType())) + pcSiteStartPtr.SetInitializer(llvm.ConstPointerNull(pcSiteStartPtr.GlobalValueType())) + pcSiteEndPtr.SetInitializer(llvm.ConstPointerNull(pcSiteEndPtr.GlobalValueType())) + entryStartPtr.SetInitializer(llvm.ConstPointerNull(entryStartPtr.GlobalValueType())) + entryEndPtr.SetInitializer(llvm.ConstPointerNull(entryEndPtr.GlobalValueType())) + stubSiteStartPtr.SetInitializer(llvm.ConstPointerNull(stubSiteStartPtr.GlobalValueType())) + stubSiteEndPtr.SetInitializer(llvm.ConstPointerNull(stubSiteEndPtr.GlobalValueType())) + stringsPtr.SetInitializer(llvm.ConstPointerNull(stringsPtr.GlobalValueType())) + stringOffsetsPtr.SetInitializer(llvm.ConstPointerNull(stringOffsetsPtr.GlobalValueType())) + stringCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + hashPtr.SetInitializer(llvm.ConstPointerNull(hashPtr.GlobalValueType())) + symbolIndexPtr.SetInitializer(llvm.ConstPointerNull(symbolIndexPtr.GlobalValueType())) + count.SetInitializer(llvm.ConstInt(countType, 0, false)) + symbolIndexCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + stubIndexesPtr.SetInitializer(llvm.ConstPointerNull(stubIndexesPtr.GlobalValueType())) + stubCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + pcLineCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + hashMask.SetInitializer(llvm.ConstInt(countType, 0, false)) + return + } + + values := make([]llvm.Value, 0, len(encoded.Records)) + for _, rec := range encoded.Records { + values = append(values, llvm.ConstNamedStruct(recordType, []llvm.Value{ + llvm.ConstInt(i16Type, uint64(rec.SymbolPkg), false), + llvm.ConstInt(i16Type, uint64(rec.SymbolName), false), + llvm.ConstInt(i16Type, uint64(rec.NamePkg), false), + llvm.ConstInt(i16Type, uint64(rec.NameName), false), + llvm.ConstInt(i16Type, uint64(rec.FileRoot), false), + llvm.ConstInt(i16Type, uint64(rec.FileName), false), + llvm.ConstInt(i32Type, uint64(rec.Line), 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) + + pcLineValues := make([]llvm.Value, 0, len(encoded.PCLines)) + for _, rec := range encoded.PCLines { + pcLineValues = append(pcLineValues, llvm.ConstNamedStruct(pcLineRecordType, []llvm.Value{ + llvm.ConstInt(i64Type, rec.ID, false), + llvm.ConstInt(i32Type, uint64(rec.Func), false), + llvm.ConstInt(i32Type, uint64(rec.File), false), + llvm.ConstInt(i32Type, uint64(rec.Line), false), + })) + } + if len(pcLineValues) == 0 { + pcLinePtr.SetInitializer(llvm.ConstPointerNull(pcLinePtr.GlobalValueType())) + pcLineCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + pcSiteStartPtr.SetInitializer(llvm.ConstPointerNull(pcSiteStartPtr.GlobalValueType())) + pcSiteEndPtr.SetInitializer(llvm.ConstPointerNull(pcSiteEndPtr.GlobalValueType())) + } else { + pcLineArrayType := llvm.ArrayType(pcLineRecordType, len(pcLineValues)) + pcLineData := llvm.AddGlobal(mod, pcLineArrayType, pcLineDataSymbol) + pcLineData.SetInitializer(llvm.ConstArray(pcLineRecordType, pcLineValues)) + pcLineData.SetLinkage(llvm.PrivateLinkage) + pcLineData.SetGlobalConstant(true) + pcLineData.SetUnnamedAddr(true) + pcLineData.SetAlignment(8) + pcLinePtr.SetInitializer(llvm.ConstInBoundsGEP(pcLineArrayType, pcLineData, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + pcLineCount.SetInitializer(llvm.ConstInt(countType, uint64(len(encoded.PCLines)), false)) + if shouldEmitRuntimeSites(ctx) { + startName, endName := pcLineSiteSectionInfo.boundary(shouldEmitRuntimeMachOSites(ctx)) + pcSiteStart := llvm.AddGlobal(mod, pcSiteRecordType, startName) + pcSiteEnd := llvm.AddGlobal(mod, pcSiteRecordType, endName) + pcSiteStartPtr.SetInitializer(pcSiteStart) + pcSiteEndPtr.SetInitializer(pcSiteEnd) + } else { + pcSiteStartPtr.SetInitializer(llvm.ConstPointerNull(pcSiteStartPtr.GlobalValueType())) + pcSiteEndPtr.SetInitializer(llvm.ConstPointerNull(pcSiteEndPtr.GlobalValueType())) + } + } + machOSites := shouldEmitRuntimeMachOSites(ctx) + emitSites := shouldEmitRuntimeSites(ctx) + emitEntrySites := shouldEmitRuntimeEntryELFSites(ctx) && len(encoded.Records) != 0 + emitStubSites := shouldEmitRuntimeStubELFSites(ctx) + emitRuntimeFuncInfoSites(mod, ctx.prog.PointerSize(), machOSites, emitSites && len(pcLineValues) != 0, emitEntrySites, emitStubSites && len(stubRecords) != 0) + if emitEntrySites { + startName, endName := entrySiteSectionInfo.boundary(machOSites) + entryStart := llvm.AddGlobal(mod, funcEntryRecordType, startName) + entryEnd := llvm.AddGlobal(mod, funcEntryRecordType, endName) + entryStartPtr.SetInitializer(entryStart) + entryEndPtr.SetInitializer(entryEnd) + } else { + entryStartPtr.SetInitializer(llvm.ConstPointerNull(entryStartPtr.GlobalValueType())) + entryEndPtr.SetInitializer(llvm.ConstPointerNull(entryEndPtr.GlobalValueType())) + } + if emitStubSites && len(stubRecords) != 0 { + startName, endName := stubSiteSectionInfo.boundary(machOSites) + stubSiteStart := llvm.AddGlobal(mod, stubSiteRecordType, startName) + stubSiteEnd := llvm.AddGlobal(mod, stubSiteRecordType, endName) + stubSiteStartPtr.SetInitializer(stubSiteStart) + stubSiteEndPtr.SetInitializer(stubSiteEnd) + } else { + stubSiteStartPtr.SetInitializer(llvm.ConstPointerNull(stubSiteStartPtr.GlobalValueType())) + stubSiteEndPtr.SetInitializer(llvm.ConstPointerNull(stubSiteEndPtr.GlobalValueType())) + } + + 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) + + stringOffsetValues := make([]llvm.Value, 0, len(encoded.StringOffsets)) + for _, off := range encoded.StringOffsets { + stringOffsetValues = append(stringOffsetValues, llvm.ConstInt(i32Type, uint64(off), false)) + } + stringOffsetsArrayType := llvm.ArrayType(i32Type, len(stringOffsetValues)) + stringOffsetsData := llvm.AddGlobal(mod, stringOffsetsArrayType, funcInfoStringOffsetsDataSymbol) + stringOffsetsData.SetInitializer(llvm.ConstArray(i32Type, stringOffsetValues)) + stringOffsetsData.SetLinkage(llvm.PrivateLinkage) + stringOffsetsData.SetGlobalConstant(true) + stringOffsetsData.SetUnnamedAddr(true) + stringOffsetsData.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), + })) + stringOffsetsPtr.SetInitializer(llvm.ConstInBoundsGEP(stringOffsetsArrayType, stringOffsetsData, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + stringCount.SetInitializer(llvm.ConstInt(countType, uint64(len(encoded.StringOffsets)), false)) + if len(encoded.Hash) == 0 { + hashPtr.SetInitializer(llvm.ConstPointerNull(hashPtr.GlobalValueType())) + hashMask.SetInitializer(llvm.ConstInt(countType, 0, false)) + } else { + hashValues := make([]llvm.Value, 0, len(encoded.Hash)) + for _, idx := range encoded.Hash { + hashValues = append(hashValues, llvm.ConstInt(i16Type, uint64(idx), false)) + } + hashArrayType := llvm.ArrayType(i16Type, len(hashValues)) + hashData := llvm.AddGlobal(mod, hashArrayType, funcInfoHashDataSymbol) + hashData.SetInitializer(llvm.ConstArray(i16Type, hashValues)) + hashData.SetLinkage(llvm.PrivateLinkage) + hashData.SetGlobalConstant(true) + hashData.SetUnnamedAddr(true) + hashData.SetAlignment(2) + hashPtr.SetInitializer(llvm.ConstInBoundsGEP(hashArrayType, hashData, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + hashMask.SetInitializer(llvm.ConstInt(countType, uint64(len(encoded.Hash)-1), false)) + } + count.SetInitializer(llvm.ConstInt(countType, uint64(len(encoded.Records)), false)) + symbolIndexRecords := collectFuncInfoSymbolIndexRecords(records) + symbolIndexValues := make([]llvm.Value, 0, len(symbolIndexRecords)) + for _, rec := range symbolIndexRecords { + if rec.funcIndex == 0 || int(rec.funcIndex) > len(encoded.Records) { + continue + } + symbolIndexValues = append(symbolIndexValues, llvm.ConstNamedStruct(symbolIndexRecordType, []llvm.Value{ + llvm.ConstInt(i64Type, rec.symbolID, false), + llvm.ConstInt(i32Type, uint64(rec.funcIndex), false), + })) + } + if len(symbolIndexValues) == 0 { + symbolIndexPtr.SetInitializer(llvm.ConstPointerNull(symbolIndexPtr.GlobalValueType())) + symbolIndexCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + } else { + symbolIndexArrayType := llvm.ArrayType(symbolIndexRecordType, len(symbolIndexValues)) + symbolIndexData := llvm.AddGlobal(mod, symbolIndexArrayType, funcInfoSymbolIndexDataSymbol) + symbolIndexData.SetInitializer(llvm.ConstArray(symbolIndexRecordType, symbolIndexValues)) + symbolIndexData.SetLinkage(llvm.PrivateLinkage) + symbolIndexData.SetGlobalConstant(true) + symbolIndexData.SetUnnamedAddr(true) + symbolIndexData.SetAlignment(8) + symbolIndexPtr.SetInitializer(llvm.ConstInBoundsGEP(symbolIndexArrayType, symbolIndexData, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + symbolIndexCount.SetInitializer(llvm.ConstInt(countType, uint64(len(symbolIndexValues)), false)) + } + stubIndexSeen := make(map[uint32]none, len(stubRecords)) + stubIndexValues := make([]llvm.Value, 0, len(stubRecords)) + for _, stub := range stubRecords { + idx := stub.funcIndex + if idx == 0 || int(idx) > len(encoded.Records) { + continue + } + if _, ok := stubIndexSeen[idx]; ok { + continue + } + stubIndexSeen[idx] = none{} + stubIndexValues = append(stubIndexValues, llvm.ConstInt(i32Type, uint64(idx), false)) + } + if len(stubIndexValues) == 0 { + stubIndexesPtr.SetInitializer(llvm.ConstPointerNull(stubIndexesPtr.GlobalValueType())) + stubCount.SetInitializer(llvm.ConstInt(countType, 0, false)) + } else { + stubIndexArrayType := llvm.ArrayType(i32Type, len(stubIndexValues)) + stubIndexData := llvm.AddGlobal(mod, stubIndexArrayType, funcInfoStubIndexesDataSymbol) + stubIndexData.SetInitializer(llvm.ConstArray(i32Type, stubIndexValues)) + stubIndexData.SetLinkage(llvm.PrivateLinkage) + stubIndexData.SetGlobalConstant(true) + stubIndexData.SetUnnamedAddr(true) + stubIndexData.SetAlignment(4) + stubIndexesPtr.SetInitializer(llvm.ConstInBoundsGEP(stubIndexArrayType, stubIndexData, []llvm.Value{ + llvm.ConstInt(countType, 0, false), + llvm.ConstInt(countType, 0, false), + })) + stubCount.SetInitializer(llvm.ConstInt(countType, uint64(len(stubIndexValues)), false)) + } +} + +func shouldEmitRuntimeELFSites(ctx *context) bool { + return ctx != nil && + ctx.buildConf != nil && + ctx.buildConf.Goos == "linux" && + ctx.buildConf.Target == "" +} + +func shouldEmitRuntimeMachOSites(ctx *context) bool { + return ctx != nil && + ctx.buildConf != nil && + ctx.buildConf.Goos == "darwin" && + ctx.buildConf.Target == "" +} + +// shouldEmitRuntimeSites reports whether the target object format has a +// DCE-safe section story for metadata site records. ELF uses SHF_LINK_ORDER +// associated sections (honored by --gc-sections). Mach-O uses live_support +// sections: under ld64/lld -dead_strip a live_support atom survives only if +// the atom it references (the anchor inside the function body) is live, which +// is the same records-follow-function semantics. Sites are additionally +// gated per Program: debug builds keep the funcinfo tables but drop the +// body-embedded site records (see Program.EnableFuncInfoSites). +func shouldEmitRuntimeSites(ctx *context) bool { + if ctx == nil || ctx.prog == nil || !ctx.prog.FuncInfoSitesEnabled() { + return false + } + return shouldEmitRuntimeELFSites(ctx) || shouldEmitRuntimeMachOSites(ctx) +} + +func shouldEmitRuntimeStubELFSites(ctx *context) bool { + return shouldEmitRuntimeSites(ctx) +} + +func shouldEmitRuntimeEntryELFSites(ctx *context) bool { + return shouldEmitRuntimeSites(ctx) +} + +// siteSectionInfo names one metadata site section in both object formats. +// Mach-O section names are capped at 16 characters, hence the short forms. +type siteSectionInfo struct { + elf string + machO string +} + +var ( + entrySiteSectionInfo = siteSectionInfo{elf: "llgo_funcinfo_entry", machO: "__DATA,__llgo_fie"} + stubSiteSectionInfo = siteSectionInfo{elf: "llgo_funcinfo_stubsite", machO: "__DATA,__llgo_stub"} + pcLineSiteSectionInfo = siteSectionInfo{elf: "llgo_pcline", machO: "__DATA,__llgo_pcl"} +) + +func (s siteSectionInfo) push(machO bool, anchor string) string { + if machO { + return ".pushsection " + s.machO + ",regular,live_support" + } + return ".pushsection " + s.elf + ",\"ao\",@progbits," + anchor +} + +// recordSymbol returns the extra label line each Mach-O record needs: the +// lowercase-l linker-private symbol splits the section into one atom per +// record, so -dead_strip can drop records individually, and the symbol itself +// is discarded at link time. ELF needs nothing here. +func (s siteSectionInfo) recordSymbol(machO bool, kind string) string { + if !machO { + return "" + } + return "l_llgo_" + kind + "_rec_${:uid}:\n" +} + +func (s siteSectionInfo) retain(machO bool) string { + if machO { + return ".section " + s.machO + ",regular,live_support" + } + return ".section " + s.elf + ",\"aR\",@progbits" +} + +// retainSymbol returns the label lines that pin the zero record under +// -dead_strip on Mach-O; nothing references the zero record, so it must be a +// no_dead_strip atom for the section (and its boundary symbols) to survive. +func (s siteSectionInfo) retainSymbol(machO bool, kind string) string { + if !machO { + return "" + } + sym := "l_llgo_" + kind + "_zero" + return sym + ":\n.no_dead_strip " + sym + "\n" +} + +// boundary returns the linker-synthesized section boundary symbols: ELF +// __start_/__stop_ for C-identifier section names, ld64 section$start$/ +// section$end$ for Mach-O. +func (s siteSectionInfo) boundary(machO bool) (start, end string) { + if machO { + base := strings.Replace(s.machO, ",", "$", 1) + // The \x01 prefix makes LLVM emit the name verbatim. Without it the + // Mach-O mangler prepends an underscore and the linker no longer + // recognizes the exact section$start$SEG$SECT boundary spelling. + return "\x01section$start$" + base, "\x01section$end$" + base + } + return "__start_" + s.elf, "__stop_" + s.elf +} + +func siteAnchorLabel(machO bool, kind string) string { + if machO { + // Mach-O assembler-local labels use the plain "L" prefix. + return "Lllgo_" + kind + "_anchor_${:uid}" + } + return ".Lllgo_" + kind + "_anchor_${:uid}" +} + +func emitFuncInfoEntrySites(ctx *context, pkg llssa.Package) { + if !shouldEmitRuntimeEntryELFSites(ctx) || pkg == nil || !ctx.prog.FuncInfoMetadataEnabled() { + return + } + mod := pkg.Module() + records := readFuncInfo(mod) + if len(records) == 0 { + return + } + symbolIDs := make(map[string]uint64, len(records)) + for _, rec := range records { + if rec.symbol != "" { + symbolIDs[rec.symbol] = funcInfoSymbolID(rec.symbol) + } + } + if len(symbolIDs) == 0 { + return + } + // This is LLGo's DCE-safe substitute for the function PC list that Go's + // linker has while building pclntab. The inline-asm fragment lives in a + // section tied to the function body (SHF_LINK_ORDER on ELF; live_support + // on Mach-O), so dead functions do not leave stale entry records behind. + // Runtime still sorts these final PCs before building the Go-style + // findfunc bucket index, because LLVM IR generation does not know final + // linked text order. + // + // Known limitation: because the record is emitted inside the function + // body, LTO inlining duplicates it into every inline site, bloating the + // section (~4x on multipkg) and registering host-function PCs under the + // inlinee's symbol ID; the runtime only consults this table when native + // symbolization fails, which bounds the impact. Data-global alternatives + // were tried and do not work with the current LLVM semantics: !associated + // affects only linker GC, so IR-level GlobalDCE deletes every record; + // keeping records via llvm.compiler.used makes their function-address + // initializers pin dead functions alive; and noduplicate on the asm call + // blocks inlining outright. Deduplicating the section is therefore + // link-phase work and lands together with the final ftab generation. + machO := shouldEmitRuntimeMachOSites(ctx) + llvmCtx := mod.Context() + builder := llvmCtx.NewBuilder() + defer builder.Dispose() + asmType := llvm.FunctionType(llvmCtx.VoidType(), nil, false) + ptrDirective := ".quad" + align := "3" + if ctx.prog.PointerSize() == 4 { + ptrDirective = ".long" + align = "2" + } + for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { + if fn.IsDeclaration() || fn.BasicBlocksCount() == 0 { + continue + } + symbol := fn.Name() + symbolID := symbolIDs[symbol] + if symbolID == 0 { + continue + } + entry := fn.EntryBasicBlock() + if entry.IsNil() { + continue + } + first := entry.FirstInstruction() + if first.IsNil() { + builder.SetInsertPointAtEnd(entry) + } else { + builder.SetInsertPointBefore(first) + } + anchor := siteAnchorLabel(machO, "funcinfo_entry") + instruction := anchor + ":\n" + + entrySiteSectionInfo.push(machO, anchor) + "\n" + + ".p2align " + align + "\n" + + entrySiteSectionInfo.recordSymbol(machO, "funcinfo_entry") + + ptrDirective + " " + anchor + "\n" + + ".quad " + uint64Hex(symbolID) + "\n" + + ".popsection" + asm := llvm.InlineAsm(asmType, instruction, "", true, false, llvm.InlineAsmDialectATT, false) + builder.CreateCall(asmType, asm, nil, "") + } +} + +func emitFuncInfoStubSites(ctx *context, pkg llssa.Package) { + if !shouldEmitRuntimeStubELFSites(ctx) || pkg == nil || !ctx.prog.FuncInfoMetadataEnabled() { + return + } + machO := shouldEmitRuntimeMachOSites(ctx) + mod := pkg.Module() + llvmCtx := mod.Context() + builder := llvmCtx.NewBuilder() + defer builder.Dispose() + asmType := llvm.FunctionType(llvmCtx.VoidType(), nil, false) + ptrDirective := ".quad" + align := "3" + if ctx.prog.PointerSize() == 4 { + ptrDirective = ".long" + align = "2" + } + for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { + if fn.IsDeclaration() || fn.BasicBlocksCount() == 0 { + continue + } + symbol := fn.Name() + target, ok := strings.CutPrefix(symbol, closureStubPrefix) + if !ok || target == "" { + continue + } + entry := fn.EntryBasicBlock() + if entry.IsNil() { + continue + } + first := entry.FirstInstruction() + if first.IsNil() { + builder.SetInsertPointAtEnd(entry) + } else { + builder.SetInsertPointBefore(first) + } + anchor := siteAnchorLabel(machO, "funcinfo_stubsite") + instruction := anchor + ":\n" + + stubSiteSectionInfo.push(machO, anchor) + "\n" + + ".p2align " + align + "\n" + + stubSiteSectionInfo.recordSymbol(machO, "funcinfo_stubsite") + + ptrDirective + " " + anchor + "\n" + + ".quad " + uint64Hex(funcInfoSymbolID(target)) + "\n" + + ".popsection" + asm := llvm.InlineAsm(asmType, instruction, "", true, false, llvm.InlineAsmDialectATT, false) + builder.CreateCall(asmType, asm, nil, "") + } +} + +func funcInfoSymbolID(symbol string) uint64 { + const ( + offset = uint64(14695981039346656037) + prime = uint64(1099511628211) + ) + h := offset + for i := 0; i < len(symbol); i++ { + h ^= uint64(symbol[i]) + h *= prime + } + if h == 0 { + return 1 + } + return h +} + +func uint64Hex(v uint64) string { + const hexdigits = "0123456789abcdef" + var buf [18]byte + buf[0] = '0' + buf[1] = 'x' + for i := len(buf) - 1; i >= 2; i-- { + buf[i] = hexdigits[v&0xf] + v >>= 4 + } + return string(buf[:]) +} + +// emitRuntimeFuncInfoSites emits one zero record per used site section so the +// section always exists and the linker-synthesized boundary symbols resolve +// even when no package contributed records. Runtime skips zero records. +// funcInfoMetaRecordMagic marks the entry-section meta record consumed by +// internal/pclnpost ("LLGOMET1" little-endian). +const funcInfoMetaRecordMagic = uint64(0x3154454D4F474C4C) + +func emitRuntimeFuncInfoSites(mod llvm.Module, pointerSize int, machO bool, pcSite bool, entrySite bool, stubSite bool) { + if !pcSite && !entrySite && !stubSite { + return + } + ptrDirective := ".quad" + align := "3" + if pointerSize == 4 { + ptrDirective = ".long" + align = "2" + } + var asm strings.Builder + writeZeroRecord := func(info siteSectionInfo, kind string) { + asm.WriteString(info.retain(machO) + "\n") + asm.WriteString(".p2align " + align + "\n") + asm.WriteString(info.retainSymbol(machO, kind)) + asm.WriteString(ptrDirective + " 0\n") + asm.WriteString(".quad 0\n") + } + if pcSite { + writeZeroRecord(pcLineSiteSectionInfo, "pcline") + } + if entrySite { + writeZeroRecord(entrySiteSectionInfo, "funcinfo_entry") + // Meta records for the link-phase tool: relocations carrying the + // addresses of the symbol-index pointer global and its count global. + // Relocations are resolved by the linker regardless of what LTO + // internalization does to the symbol table, which is what keeps this + // reachable in +LTO binaries. The runtime skips all three rows: the + // first has pc==0 and the other two have symbolID==0. + idxSym, cntSym := funcInfoSymbolIndexSymbol, funcInfoSymbolIndexCountSymbol + if machO { + idxSym, cntSym = "_"+idxSym, "_"+cntSym + } + asm.WriteString(ptrDirective + " 0\n") + asm.WriteString(".quad " + uint64Hex(funcInfoMetaRecordMagic) + "\n") + asm.WriteString(ptrDirective + " " + idxSym + "\n") + asm.WriteString(".quad 0\n") + asm.WriteString(ptrDirective + " " + cntSym + "\n") + asm.WriteString(".quad 0\n") + } + if stubSite { + writeZeroRecord(stubSiteSectionInfo, "funcinfo_stubsite") + } + mod.SetInlineAsm(asm.String()) +} + +func asmQuoteELFSymbol(symbol string) string { + var b strings.Builder + b.Grow(len(symbol) + 2) + b.WriteByte('"') + for i := 0; i < len(symbol); i++ { + switch symbol[i] { + case '\\', '"': + b.WriteByte('\\') + case '$': + b.WriteByte('$') + } + b.WriteByte(symbol[i]) + } + b.WriteByte('"') + return b.String() +} + +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 +} + +func toPCLineRecords(records []pcLineRecord) []buildfuncinfo.PCLineRecord { + out := make([]buildfuncinfo.PCLineRecord, len(records)) + for i, rec := range records { + out[i] = buildfuncinfo.PCLineRecord{ + ID: rec.id, + Symbol: rec.symbol, + 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..1c330d3e3f --- /dev/null +++ b/internal/build/funcinfo_table_test.go @@ -0,0 +1,495 @@ +/* + * 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/lto" + "github.com/goplus/llgo/internal/packages" + llssa "github.com/goplus/llgo/ssa" +) + +func TestFuncInfoTableMaterializesMetadataWithoutFunctionPointers(t *testing.T) { + prog := llssa.NewProgram(nil) + prog.EnableFuncInfoSites(true) + 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_pcline_table = global ptr null", + "@__llgo_pcsite_start = global ptr null", + "@__llgo_pcsite_end = global ptr null", + "@__llgo_funcinfo_strings = global ptr", + "@__llgo_funcinfo_string_offsets = global ptr", + "@__llgo_funcinfo_string_count = global i64 5", + "@__llgo_funcinfo_hash = global ptr", + "@__llgo_funcinfo_symbol_index = global ptr", + "@__llgo_funcinfo_count = global i64 1", + "@__llgo_funcinfo_symbol_index_count = global i64 1", + "@__llgo_funcinfo_entry_start = global ptr @__start_llgo_funcinfo_entry", + "@__llgo_funcinfo_entry_end = global ptr @__stop_llgo_funcinfo_entry", + "@__llgo_funcinfo_stub_indexes = global ptr null", + "@__llgo_funcinfo_stub_count = global i64 0", + "@__llgo_pcline_count = global i64 0", + "@__llgo_funcinfo_hash_mask = global i64 1", + "module asm \".section llgo_funcinfo_entry", + `@"__llgo_funcinfo_table$data" = private unnamed_addr constant [1 x { i16, i16, i16, i16, i16, i16, i32 }]`, + `@"__llgo_funcinfo_string_offsets$data" = private unnamed_addr constant`, + `@"__llgo_funcinfo_hash$data" = private unnamed_addr constant [2 x i16]`, + `@"__llgo_funcinfo_symbol_index$data" = private unnamed_addr constant [1 x { i64, i32 }]`, + `example.com/p\00`, + `live\00`, + `Live\00`, + `live.go\00`, + "i32 17", + } { + 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 TestFuncInfoTableMaterializesEntrySites(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.missing", "example.com/p.Missing", "missing.go", 19, 1) + liveFn := src.NewFunc("example.com/p.live", llssa.NoArgsNoRet, llssa.InC) + liveFn.MakeBody(1).Return() + otherFn := src.NewFunc("example.com/p.other", llssa.NoArgsNoRet, llssa.InC) + otherFn.MakeBody(1).Return() + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + emitFuncInfoEntrySites(ctx, src) + srcIR := src.String() + for _, want := range []string{ + "call void asm sideeffect", + ".pushsection llgo_funcinfo_entry", + ".Lllgo_funcinfo_entry_anchor_", + ".quad .Lllgo_funcinfo_entry_anchor_", + ".quad 0x", + } { + if !strings.Contains(srcIR, want) { + t.Fatalf("package entry site IR missing %q:\n%s", want, srcIR) + } + } + for _, bad := range []string{ + `.quad \22example.com/p.live\22`, + `.quad \22example.com/p.other\22`, + `.quad \22example.com/p.missing\22`, + } { + if strings.Contains(srcIR, bad) { + t.Fatalf("package entry site IR should not contain %q:\n%s", bad, srcIR) + } + } + + records := collectFuncInfo([]Package{{LPkg: src}}) + 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_entry_start = global ptr @__start_llgo_funcinfo_entry", + "@__llgo_funcinfo_entry_end = global ptr @__stop_llgo_funcinfo_entry", + "module asm \".section llgo_funcinfo_entry", + } { + if !strings.Contains(ir, want) { + t.Fatalf("funcinfo entry table IR missing %q:\n%s", want, ir) + } + } + + ltoCtx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + LTO: lto.Full, + }, + } + ltoEntry := genMainModule(ltoCtx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records}) + ltoIR := ltoEntry.LPkg.String() + for _, want := range []string{ + "@__llgo_funcinfo_entry_start = global ptr @__start_llgo_funcinfo_entry", + "@__llgo_funcinfo_entry_end = global ptr @__stop_llgo_funcinfo_entry", + "module asm \".section llgo_funcinfo_entry", + } { + if !strings.Contains(ltoIR, want) { + t.Fatalf("full LTO funcinfo table IR missing %q:\n%s", want, ltoIR) + } + } +} + +func TestFuncInfoTableSitesDisabledKeepsTables(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.EmitPCLineInfo(0x1234, "example.com/p.live", "call.go", 23, 5) + liveFn := src.NewFunc("example.com/p.live", llssa.NoArgsNoRet, llssa.InC) + liveFn.MakeBody(1).Return() + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(false) + + emitFuncInfoEntrySites(ctx, src) + emitFuncInfoStubSites(ctx, src) + srcIR := src.String() + for _, bad := range []string{"llgo_funcinfo_entry", "llgo_funcinfo_stubsite", "call void asm sideeffect"} { + if strings.Contains(srcIR, bad) { + t.Fatalf("sites disabled: package IR should not contain %q:\n%s", bad, srcIR) + } + } + + records := collectFuncInfo([]Package{{LPkg: src}}) + pcLines := collectPCLineInfo([]Package{{LPkg: src}}) + entry := genMainModule(ctx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records, pcLineInfo: pcLines}) + ir := entry.LPkg.String() + // The metadata tables must still materialize... + for _, want := range []string{ + "@__llgo_funcinfo_table = global ptr", + "@__llgo_funcinfo_count = global", + "@__llgo_pcline_table = global ptr", + } { + if !strings.Contains(ir, want) { + t.Fatalf("sites disabled: funcinfo table IR missing %q:\n%s", want, ir) + } + } + // ...while the site sections and their boundary symbols must not. + for _, bad := range []string{ + "@__start_llgo_funcinfo_entry", + "@__start_llgo_funcinfo_stubsite", + "@__start_llgo_pcline", + "module asm \".section llgo_", + } { + if strings.Contains(ir, bad) { + t.Fatalf("sites disabled: funcinfo table IR should not contain %q:\n%s", bad, ir) + } + } +} + +func TestFuncInfoTableMaterializesClosureStubIndexes(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.other", "example.com/p.Other", "other.go", 23, 1) + stubFn := src.NewFunc(closureStubPrefix+"example.com/p.live", llssa.NoArgsNoRet, llssa.InC) + stubFn.MakeBody(1).Return() + ctx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + emitFuncInfoStubSites(ctx, src) + srcIR := src.String() + for _, want := range []string{ + "call void asm sideeffect", + ".pushsection llgo_funcinfo_stubsite", + ".Lllgo_funcinfo_stubsite_anchor_", + ".quad .Lllgo_funcinfo_stubsite_anchor_", + ".quad 0x", + } { + if !strings.Contains(srcIR, want) { + t.Fatalf("package stub site IR missing %q:\n%s", want, srcIR) + } + } + if strings.Contains(srcIR, `.quad \22__llgo_stub.example.com/p.live\22`) { + t.Fatalf("package stub site must not reference stub function symbols:\n%s", srcIR) + } + + records := collectFuncInfo([]Package{{LPkg: src}}) + stubs := collectFuncInfoStubRecords([]Package{{LPkg: src}}, records) + if len(stubs) != 1 || records[stubs[0].funcIndex-1].symbol != "example.com/p.live" || + stubs[0].symbol != closureStubPrefix+"example.com/p.live" { + t.Fatalf("stub indexes = %+v for records %+v, want live", stubs, records) + } + + entry := genMainModule(ctx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records, funcInfoStubs: stubs}) + ir := entry.LPkg.String() + for _, want := range []string{ + "@__llgo_funcinfo_stub_indexes = global ptr", + "@__llgo_funcinfo_stub_count = global i64 1", + "@__llgo_funcinfo_symbol_index = global ptr", + "@__llgo_funcinfo_symbol_index_count = global i64 2", + "@__llgo_funcinfo_stubsite_start = global ptr @__start_llgo_funcinfo_stubsite", + "@__llgo_funcinfo_stubsite_end = global ptr @__stop_llgo_funcinfo_stubsite", + `@"__llgo_funcinfo_stub_indexes$data" = private unnamed_addr constant [1 x i32]`, + "@__llgo_funcinfo_count = global i64 2", + "module asm \".section llgo_funcinfo_stubsite", + ".quad 0", + } { + if !strings.Contains(ir, want) { + t.Fatalf("funcinfo stub index table IR missing %q:\n%s", want, ir) + } + } + if strings.Contains(ir, closureStubPrefix+"example.com/p.live\\00") { + t.Fatalf("stub index table should not add stub symbol strings:\n%s", ir) + } + + ltoCtx := &context{ + prog: prog, + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + LTO: lto.Full, + }, + } + ltoEntry := genMainModule(ltoCtx, llssa.PkgRuntime, &packages.Package{ + PkgPath: "example.com/main", + ExportFile: "main.a", + }, &genConfig{funcInfo: records, funcInfoStubs: stubs}) + ltoIR := ltoEntry.LPkg.String() + for _, want := range []string{ + "@__llgo_funcinfo_stubsite_start = global ptr @__start_llgo_funcinfo_stubsite", + "@__llgo_funcinfo_stubsite_end = global ptr @__stop_llgo_funcinfo_stubsite", + "module asm \".section llgo_funcinfo_stubsite", + } { + if !strings.Contains(ltoIR, want) { + t.Fatalf("full LTO funcinfo stub site table IR missing %q:\n%s", want, ltoIR) + } + } +} + +func TestFuncInfoTableMaterializesPCLineMetadata(t *testing.T) { + prog := llssa.NewProgram(nil) + prog.EnableFuncInfoSites(true) + src := prog.NewPackage("example.com/p", "example.com/p") + src.EmitFuncInfo("example.com/p.live", "example.com/p.Live", "live.go", 17, 3) + src.EmitPCLineInfo(0x1234, "example.com/p.live", "call.go", 23, 5) + src.EmitPCLineInfo(0x5678, "example.com/p.missing", "missing.go", 99, 1) + + records := collectFuncInfo([]Package{{LPkg: src}}) + pcLines := collectPCLineInfo([]Package{{LPkg: src}}) + if len(records) != 1 { + t.Fatalf("collectFuncInfo returned %d records, want 1", len(records)) + } + if len(pcLines) != 2 { + t.Fatalf("collectPCLineInfo returned %d records, want 2", len(pcLines)) + } + + 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, pcLineInfo: pcLines}) + ir := entry.LPkg.String() + for _, want := range []string{ + "@__llgo_pcline_table = global ptr", + "@__llgo_pcsite_start = global ptr @__start_llgo_pcline", + "@__llgo_pcsite_end = global ptr @__stop_llgo_pcline", + "@__llgo_pcline_count = global i64 1", + "@__llgo_funcinfo_string_count = global i64 6", + "module asm \".section llgo_pcline", + `@"__llgo_pcline_table$data" = private unnamed_addr constant [1 x { i64, i32, i32, i32 }]`, + "i64 4660", + "i32 23", + `call.go\00`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("pcline table IR missing %q:\n%s", want, ir) + } + } + if strings.Contains(ir, "missing.go") || strings.Contains(ir, "i64 22136") { + t.Fatalf("pcline table should drop records without matching function metadata:\n%s", ir) + } + if strings.Contains(ir, `ptr @"example.com/p.live"`) { + t.Fatalf("pcline 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_pcline_table = global ptr null", + "@__llgo_pcsite_start = global ptr null", + "@__llgo_pcsite_end = global ptr null", + "@__llgo_funcinfo_strings = global ptr null", + "@__llgo_funcinfo_string_offsets = global ptr null", + "@__llgo_funcinfo_string_count = global i64 0", + "@__llgo_funcinfo_hash = global ptr null", + "@__llgo_funcinfo_symbol_index = global ptr null", + "@__llgo_funcinfo_count = global i64 0", + "@__llgo_funcinfo_symbol_index_count = global i64 0", + "@__llgo_funcinfo_entry_start = global ptr null", + "@__llgo_funcinfo_entry_end = global ptr null", + "@__llgo_funcinfo_stub_indexes = global ptr null", + "@__llgo_funcinfo_stub_count = global i64 0", + "@__llgo_pcline_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..67378f6e2e 100644 --- a/internal/build/main_module.go +++ b/internal/build/main_module.go @@ -43,6 +43,9 @@ type genConfig struct { methodByIndex map[int]none methodByName map[string]none abiSymbols map[string]none + funcInfo []funcInfoRecord + pcLineInfo []pcLineRecord + funcInfoStubs []funcInfoStubRecord } // genMainModule generates the main entry module for an llgo program. @@ -60,6 +63,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, cfg.pcLineInfo, cfg.funcInfoStubs) exportFile := pkg.ExportFile if exportFile == "" { diff --git a/internal/build/pclntab_llvm.go b/internal/build/pclntab_llvm.go new file mode 100644 index 0000000000..e7cc079ac3 --- /dev/null +++ b/internal/build/pclntab_llvm.go @@ -0,0 +1,38 @@ +//go:build !llgo +// +build !llgo + +package build + +import ( + "github.com/goplus/llgo/internal/pclntab" + llvm "github.com/xgo-dev/llvm" +) + +// emitPCLnFindFuncBuckets is the LLVM materialization layer for the Go-style +// findfunctab data produced by internal/pclntab. Keep the algorithm in that +// package; this function should only translate buckets into IR constants. +func emitPCLnFindFuncBuckets(mod llvm.Module, symbol string, buckets []pclntab.FindFuncBucket) llvm.Value { + ctx := mod.Context() + i8Type := ctx.Int8Type() + i32Type := ctx.Int32Type() + subType := llvm.ArrayType(i8Type, pclntab.FindFuncSubbucket) + bucketType := ctx.StructType([]llvm.Type{i32Type, subType}, false) + arrayType := llvm.ArrayType(bucketType, len(buckets)) + values := make([]llvm.Value, 0, len(buckets)) + for _, bucket := range buckets { + subs := make([]llvm.Value, 0, len(bucket.Subbuckets)) + for _, sub := range bucket.Subbuckets { + subs = append(subs, llvm.ConstInt(i8Type, uint64(sub), false)) + } + values = append(values, llvm.ConstNamedStruct(bucketType, []llvm.Value{ + llvm.ConstInt(i32Type, uint64(bucket.Idx), false), + llvm.ConstArray(i8Type, subs), + })) + } + global := llvm.AddGlobal(mod, arrayType, symbol) + global.SetInitializer(llvm.ConstArray(bucketType, values)) + global.SetGlobalConstant(true) + global.SetUnnamedAddr(true) + global.SetAlignment(4) + return global +} diff --git a/internal/build/pclntab_llvm_test.go b/internal/build/pclntab_llvm_test.go new file mode 100644 index 0000000000..1e74667238 --- /dev/null +++ b/internal/build/pclntab_llvm_test.go @@ -0,0 +1,36 @@ +//go:build !llgo +// +build !llgo + +package build + +import ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/pclntab" + llvm "github.com/xgo-dev/llvm" +) + +func TestEmitPCLnFindFuncBuckets(t *testing.T) { + llvm.InitializeAllTargets() + ctx := llvm.NewContext() + defer ctx.Dispose() + mod := ctx.NewModule("pclntab-test") + defer mod.Dispose() + + buckets := []pclntab.FindFuncBucket{ + {Idx: 0, Subbuckets: [16]uint8{0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}}, + {Idx: 3, Subbuckets: [16]uint8{0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}}, + } + emitPCLnFindFuncBuckets(mod, "__llgo_findfunctab", buckets) + ir := mod.String() + for _, want := range []string{ + `@__llgo_findfunctab = unnamed_addr constant [2 x { i32, [16 x i8] }]`, + `{ i32 0, [16 x i8] c"\00\01\02`, + `{ i32 3, [16 x i8] c"\00\00\01`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("IR missing %q:\n%s", want, ir) + } + } +} diff --git a/internal/pclnpost/binary.go b/internal/pclnpost/binary.go new file mode 100644 index 0000000000..0a9ce4a6e1 --- /dev/null +++ b/internal/pclnpost/binary.go @@ -0,0 +1,408 @@ +/* + * 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 pclnpost implements the P1/P2 prototype of link-phase ftab/findfunctab +// generation (doc/design/pclntab-linkphase.md). It parses a linked LLGo +// binary's funcinfo site sections, deduplicates LTO inline copies against the +// symbol table, sorts the entries, builds the Go-layout findfunctab via +// internal/pclntab, and prints what the P2 build integration would write +// back. It performs no writes; its purpose is to prove the risky steps on +// real binaries. +package pclnpost + +import ( + "debug/elf" + "debug/macho" + "encoding/binary" + "fmt" + "os" + "sort" + + "github.com/goplus/llgo/internal/pclntab" +) + +type siteRecord struct { + pc uint64 + symbolID uint64 +} + +type textSym struct { + addr uint64 + size uint64 + name string +} + +type secInfo struct { + vmaddr uint64 + size uint64 + fileOff uint64 +} + +type binaryInfo struct { + format string + raw []byte + entrySec []byte + stubSec []byte + textStart uint64 + textEnd uint64 + imageBase uint64 + syms []textSym // sorted by addr, text symbols only + secs []secInfo + // Mach-O chained-fixup import targets, ordinal -> resolved vmaddr (0 if + // the import name has no local definition). Exported symbols' pointer + // slots are emitted as BIND nodes even when they bind to this image, so + // record decoding needs the imports table, not just rebase decoding. + bindTargets []uint64 + + entryVMAddr, entryVMSize, entryFileOff uint64 + stubVMSize, stubFileOff uint64 +} + +// readVM returns n bytes at a link-time virtual address. +func readVM(info *binaryInfo, addr uint64, n int) []byte { + for _, s := range info.secs { + if addr >= s.vmaddr && addr+uint64(n) <= s.vmaddr+s.size { + off := s.fileOff + (addr - s.vmaddr) + return info.raw[off : off+uint64(n)] + } + } + return make([]byte, n) +} + +func load(path string) (*binaryInfo, error) { + if mf, err := macho.Open(path); err == nil { + defer mf.Close() + info := &binaryInfo{format: "macho"} + info.raw, _ = os.ReadFile(path) + for _, s := range mf.Sections { + info.secs = append(info.secs, secInfo{vmaddr: s.Addr, size: s.Size, fileOff: uint64(s.Offset)}) + } + if s := mf.Section("__llgo_fie"); s != nil { + info.entrySec, _ = s.Data() + info.entryVMAddr, info.entryVMSize, info.entryFileOff = s.Addr, s.Size, uint64(s.Offset) + } + if s := mf.Section("__llgo_stub"); s != nil { + info.stubSec, _ = s.Data() + info.stubVMSize, info.stubFileOff = s.Size, uint64(s.Offset) + } + if s := mf.Section("__text"); s != nil { + info.textStart, info.textEnd = s.Addr, s.Addr+s.Size + info.imageBase = s.Addr &^ 0xFFFFFFF + } + if mf.Symtab != nil { + for _, sym := range mf.Symtab.Syms { + if sym.Value >= info.textStart && sym.Value < info.textEnd && sym.Name != "" { + info.syms = append(info.syms, textSym{addr: sym.Value, name: sym.Name}) + } + } + } + loadBindTargets(info, mf) + finish(info) + return info, nil + } + ef, err := elf.Open(path) + if err != nil { + return nil, fmt.Errorf("not Mach-O and not ELF: %w", err) + } + defer ef.Close() + info := &binaryInfo{format: "elf"} + info.raw, _ = os.ReadFile(path) + for _, s := range ef.Sections { + if s.Type != elf.SHT_NOBITS && s.Addr != 0 { + info.secs = append(info.secs, secInfo{vmaddr: s.Addr, size: s.Size, fileOff: s.Offset}) + } + } + if s := ef.Section("llgo_funcinfo_entry"); s != nil { + info.entrySec, _ = s.Data() + info.entryVMAddr, info.entryVMSize, info.entryFileOff = s.Addr, s.Size, s.Offset + } + if s := ef.Section("llgo_funcinfo_stubsite"); s != nil { + info.stubSec, _ = s.Data() + info.stubVMSize, info.stubFileOff = s.Size, s.Offset + } + if s := ef.Section(".text"); s != nil { + info.textStart, info.textEnd = s.Addr, s.Addr+s.Size + info.imageBase = s.Addr &^ 0xFFFFFFF + } + syms, _ := ef.Symbols() + for _, sym := range syms { + if elf.ST_TYPE(sym.Info) == elf.STT_FUNC && sym.Value >= info.textStart && sym.Value < info.textEnd { + info.syms = append(info.syms, textSym{addr: sym.Value, size: sym.Size, name: sym.Name}) + } + } + finish(info) + return info, nil +} + +func finish(info *binaryInfo) { + sort.Slice(info.syms, func(i, j int) bool { return info.syms[i].addr < info.syms[j].addr }) + // Collapse same-address aliases, then derive missing extents from the + // next distinct symbol start (Mach-O nlist carries no sizes; Go's linker + // uses the same next-start rule for its final ftab). + dedup := info.syms[:0] + for _, s := range info.syms { + if len(dedup) > 0 && dedup[len(dedup)-1].addr == s.addr { + continue + } + dedup = append(dedup, s) + } + info.syms = dedup + for i := range info.syms { + if info.syms[i].size == 0 { + if i+1 < len(info.syms) { + info.syms[i].size = info.syms[i+1].addr - info.syms[i].addr + } else { + info.syms[i].size = info.textEnd - info.syms[i].addr + } + } + } +} + +func parseRecords(info *binaryInfo, sec []byte) []siteRecord { + var out []siteRecord + for off := 0; off+16 <= len(sec); off += 16 { + pc := binary.LittleEndian.Uint64(sec[off:]) + id := binary.LittleEndian.Uint64(sec[off+8:]) + if pc == 0 || id == 0 { // zero keep-alive record + continue + } + // Mach-O pointer slots in the on-disk file hold dyld chained-fixup + // encodings; dyld rewrites them at load. Rebase nodes + // (DYLD_CHAINED_PTR_64) carry the target in the low 36 bits. Anchors + // naming *exported* functions — every `__llgo_stub.*` and any + // exported Go function — are emitted as BIND nodes instead (bit 63 + // set, import ordinal in the low 24 bits, addend above), even though + // they bind back into this same image, so those resolve through the + // imports table. The P2 write-back avoids the problem entirely by + // storing anchor-relative offsets instead of pointers. + if info.format == "macho" && (pc < info.textStart || pc >= info.textEnd) { + if pc>>63 != 0 { // DYLD_CHAINED_PTR_64_BIND + ordinal := pc & (1<<24 - 1) + addend := (pc >> 24) & 0xFF + if ordinal >= uint64(len(info.bindTargets)) || info.bindTargets[ordinal] == 0 { + continue + } + pc = info.bindTargets[ordinal] + addend + } else if t := pc & (1<<36 - 1); t >= info.textStart && t < info.textEnd { + pc = t + } + } + out = append(out, siteRecord{pc: pc, symbolID: id}) + } + return out +} + +// owner returns the text symbol containing addr. +func owner(info *binaryInfo, addr uint64) (textSym, bool) { + i := sort.Search(len(info.syms), func(i int) bool { return info.syms[i].addr > addr }) + if i == 0 { + return textSym{}, false + } + s := info.syms[i-1] + if addr >= s.addr+s.size { + return textSym{}, false + } + return s, true +} + +// fnv64 mirrors funcInfoSymbolID in internal/build/funcinfo_table.go. +func fnv64(name string) uint64 { + const offset = uint64(14695981039346656037) + const prime = uint64(1099511628211) + h := offset + for i := 0; i < len(name); i++ { + h ^= uint64(name[i]) + h *= prime + } + if h == 0 { + return 1 + } + return h +} + +const stubPrefix = "__llgo_stub." + +// canonicalOwner reports whether owner symbol `name` is the function the +// record's symbolID names, or that function's `__llgo_stub.` wrapper. +// Mach-O symbol names carry a C-mangling underscore, and debug/macho's +// suffix-shared string table can surface one underscore more or less than +// the source-level name, so try each plausible normalization — matching a +// specific 64-bit FNV makes a false positive practically impossible. +func canonicalOwner(info *binaryInfo, name string, symbolID uint64) bool { + for { + cand := name + if len(cand) > len(stubPrefix) { + if i := stringIndex(cand, stubPrefix); i >= 0 { + cand = cand[i+len(stubPrefix):] + } + } + if fnv64(cand) == symbolID { + return true + } + if info.format == "macho" && len(name) > 1 && name[0] == '_' { + name = name[1:] + continue + } + return false + } +} + +func stringIndex(s, prefix string) int { + // prefix at the start, allowing for leading mangling underscores only + for i := 0; i+len(prefix) <= len(s) && i <= 2; i++ { + if s[i:i+len(prefix)] == prefix { + return i + } + if s[i] != '_' { + break + } + } + return -1 +} + +// dedupe keeps exactly the canonical record per emitting function: a record +// is canonical when the symbol that owns its anchor PC is the function the +// symbolID names (id == fnv64(owner)) or that function's closure stub +// (owner "__llgo_stub.X" with id == fnv64(X) — stubs share the target's +// symbolID by design). Everything else with a known owner is an LTO inline +// copy: inlining duplicated the body-embedded record into a host function. +// Kept records are normalized to their owner's true entry address. Records +// whose owner cannot be determined are dropped conservatively. +func dedupe(info *binaryInfo, recs []siteRecord, verbose bool) (kept []siteRecord, droppedInline, droppedUnknown int) { + seenOwner := make(map[uint64]bool, len(recs)) + for _, r := range recs { + sym, ok := owner(info, r.pc) + if !ok { + droppedUnknown++ + continue + } + if !canonicalOwner(info, sym.name, r.symbolID) { + droppedInline++ + if verbose { + fmt.Printf(" inline copy: id=%#x pc=%#x inside %s\n", r.symbolID, r.pc, sym.name) + } + continue + } + if seenOwner[sym.addr] { + continue + } + seenOwner[sym.addr] = true + kept = append(kept, siteRecord{pc: sym.addr, symbolID: r.symbolID}) + } + return kept, droppedInline, droppedUnknown +} + +// buildFtab returns the sorted table plus the base PC (Go's minpc): offsets +// are relative to the first recorded function so ftab[0].EntryOff == 0, as +// internal/pclntab requires. +func buildFtab(info *binaryInfo, kept []siteRecord) ([]pclntab.FuncTabEntry, uint64) { + sort.Slice(kept, func(i, j int) bool { return kept[i].pc < kept[j].pc }) + if len(kept) == 0 { + return nil, info.textStart + } + base := kept[0].pc + ftab := make([]pclntab.FuncTabEntry, 0, len(kept)+1) + prev := uint64(0) + for i, r := range kept { + if r.pc == prev { + continue // two symbolIDs at one entry (aliases); keep first + } + prev = r.pc + ftab = append(ftab, pclntab.FuncTabEntry{EntryOff: uint32(r.pc - base), FuncOff: uint32(i)}) + } + // Go-style sentinel at end of text. + ftab = append(ftab, pclntab.FuncTabEntry{EntryOff: uint32(info.textEnd - base), FuncOff: ^uint32(0)}) + return ftab, base +} + +// loadBindTargets parses the LC_DYLD_CHAINED_FIXUPS imports table and +// resolves each import ordinal to the address of its local definition (this +// is a main executable: every funcinfo bind target is defined in-image). +func loadBindTargets(info *binaryInfo, mf *macho.File) { + raw := info.raw + if len(raw) < 32 { + return + } + ncmds := binary.LittleEndian.Uint32(raw[16:]) + var fixOff, fixSize uint64 + off := uint64(32) + for i := uint32(0); i < ncmds && off+8 <= uint64(len(raw)); i++ { + cmd := binary.LittleEndian.Uint32(raw[off:]) + size := binary.LittleEndian.Uint32(raw[off+4:]) + if cmd == lcDyldChainedFixups { + fixOff = uint64(binary.LittleEndian.Uint32(raw[off+8:])) + fixSize = uint64(binary.LittleEndian.Uint32(raw[off+12:])) + } + off += uint64(size) + } + if fixOff == 0 || fixOff+28 > uint64(len(raw)) { + return + } + hdr := raw[fixOff : fixOff+fixSize] + importsOff := binary.LittleEndian.Uint32(hdr[8:]) + symbolsOff := binary.LittleEndian.Uint32(hdr[12:]) + importsCount := binary.LittleEndian.Uint32(hdr[16:]) + importsFormat := binary.LittleEndian.Uint32(hdr[20:]) + if importsCount == 0 || importsCount > 1<<24 { + return + } + var stride, nameShift uint32 + switch importsFormat { + case 1: // DYLD_CHAINED_IMPORT: u32 {lib:8, weak:1, name_offset:23} + stride, nameShift = 4, 9 + case 2: // DYLD_CHAINED_IMPORT_ADDEND: {u32, i32 addend} + stride, nameShift = 8, 9 + default: // ADDEND64 or unknown: leave unresolved + return + } + byName := make(map[string]uint64, len(info.syms)) + if mf.Symtab != nil { + for _, sym := range mf.Symtab.Syms { + if sym.Value != 0 && sym.Name != "" { + byName[sym.Name] = sym.Value + } + } + } + cstr := func(b []byte) string { + for i, c := range b { + if c == 0 { + return string(b[:i]) + } + } + return string(b) + } + targets := make([]uint64, importsCount) + for i := uint32(0); i < importsCount; i++ { + rec := uint64(importsOff) + uint64(i*stride) + if rec+4 > uint64(len(hdr)) { + break + } + v := binary.LittleEndian.Uint32(hdr[rec:]) + nameOff := uint64(symbolsOff) + uint64(v>>nameShift) + if nameOff >= uint64(len(hdr)) { + continue + } + name := cstr(hdr[nameOff:]) + addr, ok := byName[name] + if !ok && len(name) > 1 && name[0] == '_' { + // debug/macho's Symtab names may carry one less mangling + // underscore than the import strings. + addr = byName[name[1:]] + } + targets[i] = addr + } + info.bindTargets = targets +} diff --git a/internal/pclnpost/binary_test.go b/internal/pclnpost/binary_test.go new file mode 100644 index 0000000000..3ea8508e65 --- /dev/null +++ b/internal/pclnpost/binary_test.go @@ -0,0 +1,49 @@ +/* + * 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 pclnpost + +import "testing" + +func TestCanonicalOwner(t *testing.T) { + elf := &binaryInfo{format: "elf"} + macho := &binaryInfo{format: "macho"} + id := fnv64("example.com/p.F") + cases := []struct { + info *binaryInfo + name string + want bool + }{ + // ELF: symbol names are source-level. + {elf, "example.com/p.F", true}, + {elf, "__llgo_stub.example.com/p.F", true}, + {elf, "example.com/p.G", false}, + // Mach-O: one C-mangling underscore, and debug/macho's suffix-shared + // string table can surface one underscore more or less. + {macho, "_example.com/p.F", true}, + {macho, "example.com/p.F", true}, + {macho, "___llgo_stub.example.com/p.F", true}, + {macho, "__llgo_stub.example.com/p.F", true}, + // An LTO inline copy: record id names F but the owner is the host. + {macho, "_example.com/p.Host", false}, + {macho, "___llgo_stub.example.com/p.G", false}, + } + for _, c := range cases { + if got := canonicalOwner(c.info, c.name, id); got != c.want { + t.Errorf("canonicalOwner(%s, %q) = %v, want %v", c.info.format, c.name, got, c.want) + } + } +} diff --git a/internal/pclnpost/fixups.go b/internal/pclnpost/fixups.go new file mode 100644 index 0000000000..8af02d419d --- /dev/null +++ b/internal/pclnpost/fixups.go @@ -0,0 +1,247 @@ +/* + * 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 pclnpost + +import ( + "encoding/binary" + "fmt" + "sort" +) + +// Mach-O dyld chained fixups surgery. +// +// The on-disk pointer slots of the rewritten sections participate in dyld's +// chained-fixup page chains. If the chains are left untouched, dyld walks +// them at load time and rebases 8-byte slots inside our freshly written +// table, corrupting it — and terminating a chain early inside a zeroed +// section would also skip later fixups in the same page, corrupting +// unrelated data. unchainRanges removes every chain node that falls inside +// the given file-offset ranges: predecessors' next links (or the page_start +// table) are repointed to the first surviving successor. + +const ( + lcDyldChainedFixups = 0x80000034 + lcSegment64 = 0x19 + + chainedPtrStartNone = 0xFFFF + chainedPtrStartMulti = 0x8000 + + // pointer_format values with a 12-bit next field at bit 51, stride 4. + chainedPtr64 = 2 + chainedPtr64Offset = 6 +) + +type segRange struct { + fileOff uint64 + fileSz uint64 + vmaddr uint64 +} + +// fixupInsert asks for a rebase fixup node at fileOff whose loaded value will +// be targetVM + slide. Slot bytes are returned as pending writes so the +// caller can apply them after overwriting the section contents. +type fixupInsert struct { + fileOff uint64 + targetVM uint64 +} + +type pendingWrite struct { + fileOff uint64 + val uint64 +} + +// unchainRanges edits chain metadata in raw in place: every chain node inside +// `ranges` (file-offset [start,end) pairs) is unlinked, and the requested +// `inserts` are spliced into the page chains as rebase nodes. Because insert +// slots usually lie inside a section the caller is about to overwrite, their +// encoded slot values are returned as pending writes to apply afterwards. +func unchainRanges(raw []byte, ranges [][2]uint64, inserts []fixupInsert) ([]pendingWrite, error) { + inRange := func(off uint64) bool { + for _, r := range ranges { + if off >= r[0] && off < r[1] { + return true + } + } + return false + } + + // Locate LC_DYLD_CHAINED_FIXUPS and the segment table. + if len(raw) < 32 || binary.LittleEndian.Uint32(raw) != 0xFEEDFACF { + return nil, fmt.Errorf("not a 64-bit little-endian Mach-O") + } + ncmds := binary.LittleEndian.Uint32(raw[16:]) + off := uint64(32) + var fixOff, fixSize uint64 + var segs []segRange + for i := uint32(0); i < ncmds; i++ { + cmd := binary.LittleEndian.Uint32(raw[off:]) + size := binary.LittleEndian.Uint32(raw[off+4:]) + switch cmd { + case lcDyldChainedFixups: + fixOff = uint64(binary.LittleEndian.Uint32(raw[off+8:])) + fixSize = uint64(binary.LittleEndian.Uint32(raw[off+12:])) + case lcSegment64: + segs = append(segs, segRange{ + vmaddr: binary.LittleEndian.Uint64(raw[off+24:]), + fileOff: binary.LittleEndian.Uint64(raw[off+40:]), + fileSz: binary.LittleEndian.Uint64(raw[off+48:]), + }) + } + off += uint64(size) + } + if fixOff == 0 { + if len(inserts) > 0 { + return nil, fmt.Errorf("no chained fixups to splice inserts into") + } + return nil, nil // no chained fixups (classic dyld info); nothing to do + } + _ = fixSize + imageBase := ^uint64(0) + for _, sg := range segs { + if sg.vmaddr != 0 && sg.vmaddr < imageBase { + imageBase = sg.vmaddr + } + } + var pending []pendingWrite + consumed := make(map[uint64]bool, len(inserts)) + + hdr := raw[fixOff:] + startsOff := fixOff + uint64(binary.LittleEndian.Uint32(hdr[4:])) + segCount := binary.LittleEndian.Uint32(raw[startsOff:]) + if int(segCount) != len(segs) { + // seg_count counts all segments incl. ones without fixups; trust it. + } + for si := uint32(0); si < segCount; si++ { + segInfoOff := binary.LittleEndian.Uint32(raw[startsOff+4+uint64(si)*4:]) + if segInfoOff == 0 { + continue + } + sOff := startsOff + uint64(segInfoOff) + pageSize := uint64(binary.LittleEndian.Uint16(raw[sOff+4:])) + ptrFormat := binary.LittleEndian.Uint16(raw[sOff+6:]) + pageCount := uint64(binary.LittleEndian.Uint16(raw[sOff+20:])) + if ptrFormat != chainedPtr64 && ptrFormat != chainedPtr64Offset { + // Only reject if this segment's pages intersect our ranges. + segFile := segs[si].fileOff + touches := false + for _, r := range ranges { + if r[0] < segFile+pageCount*pageSize && r[1] > segFile { + touches = true + } + } + if !touches { + continue + } + return nil, fmt.Errorf("unsupported pointer_format %d", ptrFormat) + } + encode := func(targetVM, next uint64) uint64 { + t := targetVM + if ptrFormat == chainedPtr64Offset { + t = targetVM - imageBase + } + return (t & (1<<36 - 1)) | (next << 51) + } + segFileOff := segs[si].fileOff + for pi := uint64(0); pi < pageCount; pi++ { + psOff := sOff + 22 + pi*2 + pageFile := segFileOff + pi*pageSize + pageEnd := pageFile + pageSize + // Inserts requested for this page. + var ins []fixupInsert + for _, in := range inserts { + if in.fileOff >= pageFile && in.fileOff < pageEnd { + ins = append(ins, in) + consumed[in.fileOff] = true + } + } + pStart := binary.LittleEndian.Uint16(raw[psOff:]) + if pStart == chainedPtrStartNone && len(ins) == 0 { + continue + } + if pStart != chainedPtrStartNone && pStart&chainedPtrStartMulti != 0 { + return nil, fmt.Errorf("multi-start pages not supported") + } + // Collect the existing chain. + var nodes []uint64 + if pStart != chainedPtrStartNone { + node := pageFile + uint64(pStart) + for { + nodes = append(nodes, node) + val := binary.LittleEndian.Uint64(raw[node:]) + next := (val >> 51) & 0xFFF + if next == 0 { + break + } + node += next * 4 + } + } + // Rebuild: out-of-range survivors plus requested inserts. + type finalNode struct { + off uint64 + insert bool + targetVM uint64 + } + var final []finalNode + removed := 0 + for _, n := range nodes { + if inRange(n) { + removed++ + } else { + final = append(final, finalNode{off: n}) + } + } + for _, in := range ins { + final = append(final, finalNode{off: in.fileOff, insert: true, targetVM: in.targetVM}) + } + if removed == 0 && len(ins) == 0 { + continue + } + sort.Slice(final, func(i, j int) bool { return final[i].off < final[j].off }) + if len(final) == 0 { + binary.LittleEndian.PutUint16(raw[psOff:], chainedPtrStartNone) + continue + } + binary.LittleEndian.PutUint16(raw[psOff:], uint16(final[0].off-pageFile)) + for i, n := range final { + var next uint64 + if i+1 < len(final) { + delta := final[i+1].off - n.off + if delta%4 != 0 || delta/4 > 0xFFF { + return nil, fmt.Errorf("chain gap %d not encodable", delta) + } + next = delta / 4 + } + if n.insert { + pending = append(pending, pendingWrite{fileOff: n.off, val: encode(n.targetVM, next)}) + } else { + val := binary.LittleEndian.Uint64(raw[n.off:]) + val = (val &^ (uint64(0xFFF) << 51)) | (next << 51) + binary.LittleEndian.PutUint64(raw[n.off:], val) + } + } + } + } + // An unconsumed insert means its slot never joined a fixup chain; on a + // PIE binary the runtime would then read an unslid value. Fail loudly so + // the caller falls back to first-use construction instead. + for _, in := range inserts { + if !consumed[in.fileOff] { + return nil, fmt.Errorf("fixup insert at %#x not within any chain page", in.fileOff) + } + } + return pending, nil +} diff --git a/internal/pclnpost/pclnpost.go b/internal/pclnpost/pclnpost.go new file mode 100644 index 0000000000..c65f815054 --- /dev/null +++ b/internal/pclnpost/pclnpost.go @@ -0,0 +1,69 @@ +/* + * 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 pclnpost + +import ( + "encoding/binary" + "fmt" +) + +// Stats summarizes one rewrite. +type Stats struct { + Format string + EntryRecords int + StubRecords int + Kept int + InlineCopies int + NoSymbol int + FtabEntries int + Buckets int +} + +// Rewrite parses the linked binary's funcinfo site sections, deduplicates +// LTO inline copies against the symbol table, builds the Go-layout prebuilt +// table and rewrites the entry section in place (voiding the stub section). +// The runtime adopts the table when it sees the magic header and falls back +// to first-use construction otherwise, so failures here leave a fully +// functional binary. +func Rewrite(path string) (Stats, error) { + var st Stats + info, err := load(path) + if err != nil { + return st, err + } + st.Format = info.format + if len(info.entrySec) >= 8 && binary.LittleEndian.Uint64(info.entrySec) == prebuiltMagic { + return st, fmt.Errorf("already rewritten") + } + entries := parseRecords(info, info.entrySec) + stubs := parseRecords(info, info.stubSec) + st.EntryRecords, st.StubRecords = len(entries), len(stubs) + if len(entries) == 0 { + return st, fmt.Errorf("no entry records") + } + kept, inline, nosym := dedupe(info, append(entries, stubs...), false) + st.Kept, st.InlineCopies, st.NoSymbol = len(kept), inline, nosym + if len(kept) == 0 { + return st, fmt.Errorf("no records survived dedup") + } + ftab, buckets, err := writeBack(path, info, kept) + if err != nil { + return st, err + } + st.FtabEntries, st.Buckets = ftab, buckets + return st, nil +} diff --git a/internal/pclnpost/write.go b/internal/pclnpost/write.go new file mode 100644 index 0000000000..8ccd24d11c --- /dev/null +++ b/internal/pclnpost/write.go @@ -0,0 +1,288 @@ +/* + * 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 pclnpost + +import ( + "debug/elf" + "debug/macho" + "encoding/binary" + "fmt" + "os" + "os/exec" + "runtime" + "sort" +) + +// Prebuilt blob layout — keep in sync with runtime/internal/lib/runtime +// (runtimePrebuiltMagic and adoptPrebuiltFuncPCTable): +// +// u64 magic "LLGOFTB1"; u64 linkSectAddr; u64 base +// u32 count (incl sentinel); u32 bucketCount +// count × {u32 entryOff, u32 funcIndex} +// bucketCount × {u32 idx; 16 × u16 subbuckets} +// +// The base slot holds the runtime PC of the first table entry: on Mach-O it +// is re-linked into the dyld chained-fixup chain (dyld rebases it at load), +// on non-PIE ELF the link-time value already equals the runtime address. +const prebuiltMagic = uint64(0x314254464F474C4C) + +const ( + bucketSize = 4096 + subbucketCnt = 16 + subbucketSize = bucketSize / subbucketCnt + bucketBytes = 4 + 2*subbucketCnt +) + +type symIndexEntry struct { + id uint64 + idx uint32 +} + +// writeBack rewrites the entry-site section in place with the prebuilt table +// and voids the stub section (its records are merged into the table). +func writeBack(path string, info *binaryInfo, kept []siteRecord) (ftabCount, bucketCount int, err error) { + symIdx, err := loadSymbolIndex(path, info) + if err != nil { + return 0, 0, err + } + sort.Slice(kept, func(i, j int) bool { return kept[i].pc < kept[j].pc }) + type row struct { + pc uint64 + idx uint32 + } + rows := make([]row, 0, len(kept)) + prev := uint64(0) + for _, r := range kept { + if r.pc == prev { + continue + } + idx, ok := lookupSymIndex(symIdx, r.symbolID) + if !ok { + continue + } + prev = r.pc + rows = append(rows, row{pc: r.pc, idx: idx}) + } + if len(rows) == 0 { + return 0, 0, fmt.Errorf("no resolvable entries") + } + base := rows[0].pc + count := len(rows) + 1 // + sentinel + + // findfunctab in the runtime's uint16 layout, mirroring + // buildRuntimeFuncPCIndex (base aligned down to a bucket boundary). + alignedBase := base &^ (bucketSize - 1) + last := rows[len(rows)-1].pc + nbuckets := int((last-alignedBase)/bucketSize + 1) + pcs := make([]uint64, len(rows)) + for i, r := range rows { + pcs[i] = r.pc + } + lastLE := func(pc uint64) int { // last index with pcs[i] <= pc, clamped like the runtime + i := sort.Search(len(pcs), func(i int) bool { return pcs[i] > pc }) - 1 + if i < 0 { + i = 0 + } + return i + } + buckets := make([]byte, 0, nbuckets*bucketBytes) + for b := 0; b < nbuckets; b++ { + bucketStart := alignedBase + uint64(b)*bucketSize + baseIdx := lastLE(bucketStart) + var tmp [bucketBytes]byte + binary.LittleEndian.PutUint32(tmp[0:], uint32(baseIdx)) + for s := 0; s < subbucketCnt; s++ { + subIdx := lastLE(bucketStart + uint64(s)*subbucketSize) + delta := subIdx - baseIdx + if delta < 0 || delta > 0xffff { + return 0, 0, fmt.Errorf("subbucket delta overflow: %d", delta) + } + binary.LittleEndian.PutUint16(tmp[4+2*s:], uint16(delta)) + } + buckets = append(buckets, tmp[:]...) + } + + need := 32 + count*8 + len(buckets) + entrySize := int(info.entryVMSize) + if need > entrySize { + return 0, 0, fmt.Errorf("prebuilt blob %dB does not fit entry section %dB", need, entrySize) + } + blob := make([]byte, entrySize) // zero tail + binary.LittleEndian.PutUint64(blob[0:], prebuiltMagic) + binary.LittleEndian.PutUint64(blob[8:], info.entryVMAddr) + binary.LittleEndian.PutUint64(blob[16:], base) + binary.LittleEndian.PutUint32(blob[24:], uint32(count)) + binary.LittleEndian.PutUint32(blob[28:], uint32(len(buckets)/bucketBytes)) + off := 32 + for _, r := range rows { + binary.LittleEndian.PutUint32(blob[off:], uint32(r.pc-base)) + binary.LittleEndian.PutUint32(blob[off+4:], r.idx) + off += 8 + } + // Sentinel: end of text, funcIndex 0. + binary.LittleEndian.PutUint32(blob[off:], uint32(info.textEnd-base)) + off += 8 + copy(blob[off:], buckets) + + raw := make([]byte, len(info.raw)) + copy(raw, info.raw) + var pending []pendingWrite + if info.format == "macho" { + // Remove the rewritten sections' pointer slots from dyld's chained + // fixup page chains first: otherwise dyld rebases 8-byte slots + // inside the new table at load time, and a chain terminating early + // inside the zeroed stub section would skip unrelated fixups later + // in the same page. + ranges := [][2]uint64{{info.entryFileOff, info.entryFileOff + info.entryVMSize}} + if info.stubVMSize > 0 { + ranges = append(ranges, [2]uint64{info.stubFileOff, info.stubFileOff + info.stubVMSize}) + } + // The header's base slot is spliced back into the chain as a live + // rebase node: dyld writes the *slid* text base there at load, so + // the runtime reads a ready runtime PC with no slide arithmetic. + inserts := []fixupInsert{{fileOff: info.entryFileOff + 16, targetVM: base}} + pending, err = unchainRanges(raw, ranges, inserts) + if err != nil { + return 0, 0, fmt.Errorf("chained fixups: %w", err) + } + } + copy(raw[info.entryFileOff:], blob) + for _, pw := range pending { + binary.LittleEndian.PutUint64(raw[pw.fileOff:], pw.val) + } + // Void the stub section: zero its records so the runtime's fallback scan + // finds nothing (stub entries are already merged into the table above). + if info.stubVMSize > 0 { + zero := raw[info.stubFileOff : info.stubFileOff+info.stubVMSize] + for i := range zero { + zero[i] = 0 + } + } + st, err := os.Stat(path) + if err != nil { + return 0, 0, err + } + if err := os.WriteFile(path, raw, st.Mode()); err != nil { + return 0, 0, err + } + if info.format == "macho" && runtime.GOOS == "darwin" { + if out, err := exec.Command("codesign", "-f", "-s", "-", path).CombinedOutput(); err != nil { + return 0, 0, fmt.Errorf("codesign: %v: %s", err, out) + } + } + return count, len(buckets) / bucketBytes, nil +} + +// metaRecordMagic marks the entry-section meta record ("LLGOMET1" LE); keep +// in sync with internal/build/funcinfo_table.go. +const metaRecordMagic = uint64(0x3154454D4F474C4C) + +// metaGlobalAddrs scans the raw entry section for the meta record and +// returns the link-time addresses of the symbol-index pointer global and its +// count global. Works in +LTO binaries where the symbols are internalized +// away, because the addresses come from relocations, not the symbol table. +func metaGlobalAddrs(info *binaryInfo) (idxPtr, cntPtr uint64, ok bool) { + sec := info.entrySec + for off := 0; off+48 <= len(sec); off += 16 { + pc := binary.LittleEndian.Uint64(sec[off:]) + id := binary.LittleEndian.Uint64(sec[off+8:]) + if pc == 0 && id == metaRecordMagic { + idxPtr = decodePtrVal(info, binary.LittleEndian.Uint64(sec[off+16:])) + cntPtr = decodePtrVal(info, binary.LittleEndian.Uint64(sec[off+32:])) + return idxPtr, cntPtr, idxPtr != 0 && cntPtr != 0 + } + } + return 0, 0, false +} + +// loadSymbolIndex reads the {u64 symbolID, u32 funcIndex} table, locating it +// through the entry-section meta record (LTO-safe) with the symbol table as +// fallback for older binaries. +func loadSymbolIndex(path string, info *binaryInfo) ([]symIndexEntry, error) { + ptrAddr, cntAddr, ok := metaGlobalAddrs(info) + if !ok { + var err error + ptrAddr, err = symbolAddr(path, "__llgo_funcinfo_symbol_index") + if err != nil { + return nil, err + } + cntAddr, err = symbolAddr(path, "__llgo_funcinfo_symbol_index_count") + if err != nil { + return nil, err + } + } + dataAddr := decodePtr(info, readVM(info, ptrAddr, 8)) + count := binary.LittleEndian.Uint64(readVM(info, cntAddr, 8)) + if count == 0 || count > 1<<20 { + return nil, fmt.Errorf("bad symbol index count %d", count) + } + raw := readVM(info, dataAddr, int(count)*16) + out := make([]symIndexEntry, count) + for i := range out { + out[i] = symIndexEntry{ + id: binary.LittleEndian.Uint64(raw[i*16:]), + idx: binary.LittleEndian.Uint32(raw[i*16+8:]), + } + } + return out, nil +} + +func lookupSymIndex(idx []symIndexEntry, id uint64) (uint32, bool) { + i := sort.Search(len(idx), func(i int) bool { return idx[i].id >= id }) + if i < len(idx) && idx[i].id == id { + return idx[i].idx, true + } + return 0, false +} + +// decodePtr resolves an on-disk pointer slot (Mach-O chained fixup or plain). +func decodePtr(info *binaryInfo, b []byte) uint64 { + return decodePtrVal(info, binary.LittleEndian.Uint64(b)) +} + +func decodePtrVal(info *binaryInfo, v uint64) uint64 { + if info.format == "macho" { + if t := v & (1<<36 - 1); t != v && t >= info.imageBase { + return t + } + } + return v +} + +func symbolAddr(path, name string) (uint64, error) { + if mf, err := macho.Open(path); err == nil { + defer mf.Close() + for _, s := range mf.Symtab.Syms { + if s.Name == "_"+name || s.Name == name { + return s.Value, nil + } + } + return 0, fmt.Errorf("symbol %s not found", name) + } + ef, err := elf.Open(path) + if err != nil { + return 0, err + } + defer ef.Close() + syms, _ := ef.Symbols() + for _, s := range syms { + if s.Name == name { + return s.Value, nil + } + } + return 0, fmt.Errorf("symbol %s not found", name) +} diff --git a/internal/pclntab/pclntab.go b/internal/pclntab/pclntab.go new file mode 100644 index 0000000000..9058c14830 --- /dev/null +++ b/internal/pclntab/pclntab.go @@ -0,0 +1,121 @@ +// Package pclntab contains the Go-style findfunc bucket/index algorithm used +// by LLGo runtime metadata. It is intentionally free of LLVM dependencies so +// build-time emitters and tests share one implementation of the pclntab logic. +package pclntab + +import "fmt" + +const ( + // These constants intentionally match Go's pclntab findfunc layout: + // cmd/link builds one 4096-byte text bucket, split into 16 256-byte + // subbuckets, and runtime.findfunc starts scanning from the recorded + // bucket base plus subbucket delta. + MinFuncSize = uint32(16) + FuncTabBucketSize = uint32(256) * MinFuncSize + FindFuncSubbucket = 16 +) + +// FuncTabEntry mirrors the two pieces of data Go's linker stores in functab: +// a PC offset sorted by final text address, and an opaque function metadata +// offset. LLGo's current caller uses FuncOff as a payload index. +type FuncTabEntry struct { + EntryOff uint32 + FuncOff uint32 +} + +// FindFuncBucket mirrors runtime.findfuncbucket: one uint32 base function +// index plus 16 one-byte deltas into the sorted functab. +type FindFuncBucket struct { + Idx uint32 + Subbuckets [FindFuncSubbucket]uint8 +} + +// BuildFindFuncBuckets ports Go's cmd/link findfunctab construction for a +// sorted functab. It deliberately stays independent of LLVM so build/link code +// can use it without duplicating the algorithm. +func BuildFindFuncBuckets(ftab []FuncTabEntry, textSize uint32) ([]FindFuncBucket, error) { + if textSize == 0 { + return nil, nil + } + if len(ftab) < 2 { + return nil, fmt.Errorf("pclntab ftab needs at least one function and one sentinel") + } + for i := 1; i < len(ftab); i++ { + if ftab[i].EntryOff <= ftab[i-1].EntryOff { + return nil, fmt.Errorf("pclntab ftab entries must be strictly increasing") + } + } + if ftab[0].EntryOff != 0 { + return nil, fmt.Errorf("pclntab first entry offset must be zero") + } + if ftab[len(ftab)-1].EntryOff < textSize { + return nil, fmt.Errorf("pclntab sentinel offset %d below text size %d", ftab[len(ftab)-1].EntryOff, textSize) + } + + nbuckets := int((textSize + FuncTabBucketSize - 1) / FuncTabBucketSize) + buckets := make([]FindFuncBucket, nbuckets) + subSize := FuncTabBucketSize / FindFuncSubbucket + for b := range buckets { + bucketStart := uint32(b) * FuncTabBucketSize + baseIdx := FuncIndexForPC(ftab, bucketStart) + buckets[b].Idx = uint32(baseIdx) + for s := 0; s < FindFuncSubbucket; s++ { + pc := bucketStart + uint32(s)*subSize + if pc >= textSize { + pc = textSize - 1 + } + subIdx := FuncIndexForPC(ftab, pc) + delta := subIdx - baseIdx + if delta < 0 || delta > 255 { + return nil, fmt.Errorf("pclntab subbucket delta overflow: bucket=%d subbucket=%d delta=%d", b, s, delta) + } + buckets[b].Subbuckets[s] = uint8(delta) + } + } + return buckets, nil +} + +// FuncIndexForPC is the slow reference lookup over the sorted functab. It is +// kept for tests and for building the compact bucket table. +func FuncIndexForPC(ftab []FuncTabEntry, pcOff uint32) int { + lo, hi := 0, len(ftab)-1 // last entry is the sentinel. + for lo+1 < hi { + mid := int(uint(lo+hi) >> 1) + if ftab[mid].EntryOff <= pcOff { + lo = mid + } else { + hi = mid + } + } + for lo+1 < len(ftab) && ftab[lo+1].EntryOff <= pcOff { + lo++ + } + if lo >= len(ftab)-1 { + return len(ftab) - 2 + } + return lo +} + +// LookupFuncIndex mirrors runtime.findfunc's hot lookup: use the bucket and +// subbucket to jump near the target function, then linearly scan the remaining +// entries in that small range. +func LookupFuncIndex(ftab []FuncTabEntry, buckets []FindFuncBucket, pcOff uint32) int { + if len(ftab) < 2 || len(buckets) == 0 { + return -1 + } + bucket := pcOff / FuncTabBucketSize + if bucket >= uint32(len(buckets)) { + return -1 + } + subSize := FuncTabBucketSize / FindFuncSubbucket + sub := (pcOff % FuncTabBucketSize) / subSize + b := buckets[bucket] + idx := int(b.Idx) + int(b.Subbuckets[sub]) + for idx+1 < len(ftab) && ftab[idx+1].EntryOff <= pcOff { + idx++ + } + if idx >= len(ftab)-1 { + return len(ftab) - 2 + } + return idx +} diff --git a/internal/pclntab/pclntab_test.go b/internal/pclntab/pclntab_test.go new file mode 100644 index 0000000000..26903ebbea --- /dev/null +++ b/internal/pclntab/pclntab_test.go @@ -0,0 +1,51 @@ +package pclntab + +import "testing" + +func TestBuildFindFuncBucketsLookup(t *testing.T) { + ftab := []FuncTabEntry{ + {EntryOff: 0, FuncOff: 11}, + {EntryOff: 16, FuncOff: 22}, + {EntryOff: 64, FuncOff: 33}, + {EntryOff: 4096, FuncOff: 44}, + {EntryOff: 4352, FuncOff: 55}, + {EntryOff: 8192, FuncOff: 0}, // sentinel + } + buckets, err := BuildFindFuncBuckets(ftab, 8192) + if err != nil { + t.Fatalf("BuildFindFuncBuckets: %v", err) + } + if got, want := len(buckets), 2; got != want { + t.Fatalf("bucket count = %d, want %d", got, want) + } + for _, tt := range []struct { + pc uint32 + want int + }{ + {pc: 0, want: 0}, + {pc: 15, want: 0}, + {pc: 16, want: 1}, + {pc: 63, want: 1}, + {pc: 64, want: 2}, + {pc: 4095, want: 2}, + {pc: 4096, want: 3}, + {pc: 4351, want: 3}, + {pc: 4352, want: 4}, + {pc: 8191, want: 4}, + } { + if got := LookupFuncIndex(ftab, buckets, tt.pc); got != tt.want { + t.Fatalf("lookup(%d) = %d, want %d", tt.pc, got, tt.want) + } + } +} + +func TestBuildFindFuncBucketsRejectsOverflow(t *testing.T) { + ftab := make([]FuncTabEntry, 0, 302) + for i := 0; i < 301; i++ { + ftab = append(ftab, FuncTabEntry{EntryOff: uint32(i), FuncOff: uint32(i + 1)}) + } + ftab = append(ftab, FuncTabEntry{EntryOff: FuncTabBucketSize, FuncOff: 0}) + if _, err := BuildFindFuncBuckets(ftab, FuncTabBucketSize); err == nil { + t.Fatal("expected subbucket overflow error") + } +} diff --git a/runtime/internal/clite/debug/_wrap/debug.c b/runtime/internal/clite/debug/_wrap/debug.c index 32d87903bf..a03fb3ca1c 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,21 @@ 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_symbol(char *name) { + int saved_errno = errno; + void *ret = dlsym(RTLD_DEFAULT, name); + 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 +43,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/clite/debug/debug.go b/runtime/internal/clite/debug/debug.go index d35899cd99..d58a5f1941 100644 --- a/runtime/internal/clite/debug/debug.go +++ b/runtime/internal/clite/debug/debug.go @@ -25,6 +25,9 @@ func Address() unsafe.Pointer //go:linkname Addrinfo C.llgo_addrinfo func Addrinfo(addr unsafe.Pointer, info *Info) c.Int +//go:linkname Symbol C.llgo_symbol +func Symbol(name *c.Char) unsafe.Pointer + //go:linkname stacktrace C.llgo_stacktrace func stacktrace(skip c.Int, ctx unsafe.Pointer, fn func(ctx, pc, offset, sp unsafe.Pointer, name *c.Char) c.Int) diff --git a/runtime/internal/lib/runtime/extern.go b/runtime/internal/lib/runtime/extern.go index 1fb397dd8a..66f95de36f 100644 --- a/runtime/internal/lib/runtime/extern.go +++ b/runtime/internal/lib/runtime/extern.go @@ -6,19 +6,44 @@ 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. + if frame, ok := rtdebug.Caller(skip); ok { + file = frame.File + line = frame.Line + if file == "" { + file = "???" + } + if line == 0 { + line = 1 + } + return frame.PC, file, line, true + } 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 { + if n := rtdebug.Callers(skip, pc); n > 0 { + return n + } + return callers(skip+1, pc) +} + +func callers(skip int, pc []uintptr) int { if len(pc) == 0 { return 0 } @@ -28,6 +53,8 @@ func Callers(skip int, pc []uintptr) int { return false } pc[n] = fr.PC + recordFrameSymbol(fr.PC, fr.Offset, fr.Name) + rtdebug.BindCallerLocation(fr.PC, fr.Name) n++ return true }) diff --git a/runtime/internal/lib/runtime/nanotime_darwin_llgo.go b/runtime/internal/lib/runtime/nanotime_darwin_llgo.go new file mode 100644 index 0000000000..7d487edadf --- /dev/null +++ b/runtime/internal/lib/runtime/nanotime_darwin_llgo.go @@ -0,0 +1,36 @@ +//go:build darwin && !baremetal + +/* + * 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" +) + +// Mirrors Go's runtime.nanotime1 on Darwin (sys_darwin.go): read +// CLOCK_UPTIME_RAW through clock_gettime_nsec_np. Darwin serves +// clock_gettime(CLOCK_MONOTONIC) with only microsecond granularity, while +// CLOCK_UPTIME_RAW is mach_absolute_time with full nanosecond resolution. +const _CLOCK_UPTIME_RAW = 8 + +//go:linkname c_clock_gettime_nsec_np C.clock_gettime_nsec_np +func c_clock_gettime_nsec_np(clockID int32) uint64 + +func nanotime1() int64 { + return int64(c_clock_gettime_nsec_np(_CLOCK_UPTIME_RAW)) +} diff --git a/runtime/internal/lib/runtime/nanotime_linux_llgo.go b/runtime/internal/lib/runtime/nanotime_linux_llgo.go new file mode 100644 index 0000000000..a07def21eb --- /dev/null +++ b/runtime/internal/lib/runtime/nanotime_linux_llgo.go @@ -0,0 +1,40 @@ +//go:build linux && !baremetal + +/* + * 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" + + c "github.com/goplus/llgo/runtime/internal/clite" + ct "github.com/goplus/llgo/runtime/internal/clite/time" +) + +// Linux CLOCK_MONOTONIC (see ), which has nanosecond +// resolution. Deliberately a local constant: ct.CLOCK_MONOTONIC carries +// Darwin's id (6), which Linux interprets as CLOCK_MONOTONIC_COARSE — a +// millisecond-granularity clock that quantized every monotonic timestamp +// the runtime produced. +const _CLOCK_MONOTONIC = 1 + +// nanotime1 mirrors Go's runtime.nanotime1 on Linux. +func nanotime1() int64 { + tv := (*ct.Timespec)(c.Alloca(unsafe.Sizeof(ct.Timespec{}))) + ct.ClockGettime(ct.ClockidT(_CLOCK_MONOTONIC), tv) + return int64(tv.Sec)*1e9 + int64(tv.Nsec) +} diff --git a/runtime/internal/lib/runtime/nanotime_other_llgo.go b/runtime/internal/lib/runtime/nanotime_other_llgo.go new file mode 100644 index 0000000000..8478b3ec8f --- /dev/null +++ b/runtime/internal/lib/runtime/nanotime_other_llgo.go @@ -0,0 +1,33 @@ +//go:build !darwin && !linux && !baremetal + +/* + * 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" + + c "github.com/goplus/llgo/runtime/internal/clite" + ct "github.com/goplus/llgo/runtime/internal/clite/time" +) + +// nanotime1 keeps the previous behavior on remaining platforms. +func nanotime1() int64 { + tv := (*ct.Timespec)(c.Alloca(unsafe.Sizeof(ct.Timespec{}))) + ct.ClockGettime(ct.CLOCK_MONOTONIC, tv) + return int64(tv.Sec)*1e9 + int64(tv.Nsec) +} diff --git a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go index 5b8155d0e8..3da596f877 100644 --- a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go +++ b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go @@ -84,6 +84,155 @@ func NumGoroutine() int { func SetCPUProfileRate(hz int) {} +const funcForPCCacheSets = 1024 +const funcForPCCacheWays = 4 + +type funcForPCCacheEntry struct { + pc uintptr + fn *Func +} + +var funcForPCCache [funcForPCCacheSets][funcForPCCacheWays]funcForPCCacheEntry +var funcForPCCacheNext [funcForPCCacheSets]uint8 +var funcForPCLast funcForPCCacheEntry + func FuncForPC(pc uintptr) *Func { - return nil + if fn := funcForPCLast.fn; fn != nil && funcForPCLast.pc == pc { + return fn + } + set := &funcForPCCache[funcForPCCacheIndex(pc)] + for i := 0; i < funcForPCCacheWays; i++ { + if fn := set[i].fn; fn != nil && set[i].pc == pc { + funcForPCLast = funcForPCCacheEntry{pc: pc, fn: fn} + return fn + } + } + return funcForPCSlow(pc) +} + +func funcForPCSlow(pc uintptr) *Func { + if pc&3 != 0 { + if sym := frameSymbol(pc); sym.ok { + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn + } + } else if pc != 0 { + // Cold fast path: before the entry frame table has been built, resolve + // an exact function-entry PC without paying first-use table + // construction. First a bounded linear scan of the raw entry-site + // section (compile-time data, no dynamic-loader query), then one + // dladdr as fallback. Requiring an exact entry match means a + // stripped-local misattribution (dladdr returning the nearest + // exported symbol) can never be accepted, so this path only ever + // answers true function-value PCs. The path is intentionally capped: + // each cold lookup costs microseconds, so after a handful of them the + // sorted table is the cheaper answer and we fall through to build it. + if !runtimeFuncPCFramesBuilt() && coldFuncPCLookupBudget() { + if sym, ok := coldFuncInfoEntryLookup(pc); ok { + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn + } + if sym := addrInfoSymbol(pc); sym.ok && sym.entry == pc && sym.function != "" { + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn + } + } + // Function-value PCs point at the real function entry. ELF funcinfo + // entry-site anchors are emitted from LLVM IR and can land after the + // backend prologue, so an exact entry PC may sort before its anchor. + // Prefer the section table when it can match within the entry slack; + // native symbol lookup is kept only as a fallback. + if sym, ok := funcPCFrameForEntryPC(pc); ok { + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn + } + if sym := addrInfoSymbol(pc); sym.ok && sym.entry == pc && sym.function != "" { + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn + } + } + if sym, ok := funcPCFrameForPC(pc); ok { + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn + } + sym := frameSymbol(pc) + fn := newFuncForPC(pc, sym) + cacheFuncForPC(pc, fn) + return fn +} + +func newFuncForPC(pc uintptr, sym pcSymbol) *Func { + if !sym.ok && sym.function == "" { + return &Func{entry: pc, name: unknownFunctionName(pc), pc: pc} + } + name := sym.function + if name == "" { + name = unknownFunctionName(pc) + } + entry := sym.entry + if entry == 0 { + entry = pc + } + return &Func{ + entry: entry, + name: name, + pc: pc, + file: sym.file, + line: sym.line, + } +} + +// frameFuncForPC returns the *Func for a frame PC that Frames.Next already +// symbolized, going through the FuncForPC cache so repeated CallersFrames +// walks over the same PCs stop allocating a Func per frame. +func frameFuncForPC(pc uintptr, sym pcSymbol, name string) *Func { + if fn := funcForPCLast.fn; fn != nil && funcForPCLast.pc == pc { + return fn + } + set := &funcForPCCache[funcForPCCacheIndex(pc)] + for i := 0; i < funcForPCCacheWays; i++ { + if fn := set[i].fn; fn != nil && set[i].pc == pc { + return fn + } + } + fn := &Func{ + entry: sym.entry, + name: name, + pc: pc, + file: sym.file, + line: sym.line, + } + // FuncForPC's own constructor falls back to entry == pc; keep frames with + // an unresolved entry out of the shared cache so a later FuncForPC(pc) + // does not observe Entry() == 0. + if sym.entry != 0 { + cacheFuncForPC(pc, fn) + } + return fn +} + +func cacheFuncForPC(pc uintptr, fn *Func) { + setIndex := funcForPCCacheIndex(pc) + set := &funcForPCCache[setIndex] + for i := 0; i < funcForPCCacheWays; i++ { + if set[i].fn == nil || set[i].pc == pc { + set[i] = funcForPCCacheEntry{pc: pc, fn: fn} + funcForPCLast = set[i] + return + } + } + way := funcForPCCacheNext[setIndex] & (funcForPCCacheWays - 1) + funcForPCCacheNext[setIndex] = way + 1 + set[way] = funcForPCCacheEntry{pc: pc, fn: fn} + funcForPCLast = set[way] +} + +func funcForPCCacheIndex(pc uintptr) uintptr { + return (pc >> 4) & (funcForPCCacheSets - 1) } 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..37d5606c38 100644 --- a/runtime/internal/lib/runtime/symtab.go +++ b/runtime/internal/lib/runtime/symtab.go @@ -9,6 +9,9 @@ import ( c "github.com/goplus/llgo/runtime/internal/clite" clitedebug "github.com/goplus/llgo/runtime/internal/clite/debug" + cliteos "github.com/goplus/llgo/runtime/internal/clite/os" + latomic "github.com/goplus/llgo/runtime/internal/lib/sync/atomic" + rtdebug "github.com/goplus/llgo/runtime/internal/runtime" ) // Frames may be used to get function/file/line information for a @@ -105,6 +108,1874 @@ 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 == "" || isPCSiteSymbol(name) { + return + } + i := (pc >> 4) & (frameSymbolCacheSize - 1) + frameSymbolCache[i] = frameSymbolCacheEntry{pc: pc, offset: offset, name: name} +} + +type runtimeFuncInfoRecord struct { + symbolPkg uint16 + symbolName uint16 + namePkg uint16 + nameName uint16 + fileRoot uint16 + fileName uint16 + line uint32 +} + +//go:linkname runtimeFuncInfoTable __llgo_funcinfo_table +var runtimeFuncInfoTable *runtimeFuncInfoRecord + +//go:linkname runtimeFuncInfoStrings __llgo_funcinfo_strings +var runtimeFuncInfoStrings *c.Char + +//go:linkname runtimeFuncInfoStringOffsets __llgo_funcinfo_string_offsets +var runtimeFuncInfoStringOffsets *uint32 + +//go:linkname runtimeFuncInfoStringCount __llgo_funcinfo_string_count +var runtimeFuncInfoStringCount uintptr + +//go:linkname runtimeFuncInfoHash __llgo_funcinfo_hash +var runtimeFuncInfoHash *uint16 + +//go:linkname runtimeFuncInfoCount __llgo_funcinfo_count +var runtimeFuncInfoCount uintptr + +//go:linkname runtimeFuncInfoHashMask __llgo_funcinfo_hash_mask +var runtimeFuncInfoHashMask uintptr + +type runtimeFuncInfoSymbolIndexRecord struct { + symbolID uint64 + funcIndex uint32 +} + +//go:linkname runtimeFuncInfoSymbolIndex __llgo_funcinfo_symbol_index +var runtimeFuncInfoSymbolIndex *runtimeFuncInfoSymbolIndexRecord + +//go:linkname runtimeFuncInfoSymbolIndexCount __llgo_funcinfo_symbol_index_count +var runtimeFuncInfoSymbolIndexCount uintptr + +//go:linkname runtimeFuncInfoStubIndexes __llgo_funcinfo_stub_indexes +var runtimeFuncInfoStubIndexes *uint32 + +//go:linkname runtimeFuncInfoStubCount __llgo_funcinfo_stub_count +var runtimeFuncInfoStubCount uintptr + +type runtimeFuncInfoEntryRecord struct { + pc uintptr + symbolID uint64 +} + +//go:linkname runtimeFuncInfoEntryStart __llgo_funcinfo_entry_start +var runtimeFuncInfoEntryStart *runtimeFuncInfoEntryRecord + +//go:linkname runtimeFuncInfoEntryEnd __llgo_funcinfo_entry_end +var runtimeFuncInfoEntryEnd *runtimeFuncInfoEntryRecord + +type runtimeFuncInfoStubSiteRecord struct { + pc uintptr + symbolID uint64 +} + +//go:linkname runtimeFuncInfoStubSiteStart __llgo_funcinfo_stubsite_start +var runtimeFuncInfoStubSiteStart *runtimeFuncInfoStubSiteRecord + +//go:linkname runtimeFuncInfoStubSiteEnd __llgo_funcinfo_stubsite_end +var runtimeFuncInfoStubSiteEnd *runtimeFuncInfoStubSiteRecord + +type runtimePCLineRecord struct { + id uint64 + funcIndex uint32 + file uint32 + line uint32 +} + +//go:linkname runtimePCLineTable __llgo_pcline_table +var runtimePCLineTable *runtimePCLineRecord + +//go:linkname runtimePCLineCount __llgo_pcline_count +var runtimePCLineCount uintptr + +type runtimePCSiteRecord struct { + pc uintptr + id uint64 +} + +//go:linkname runtimePCSiteStart __llgo_pcsite_start +var runtimePCSiteStart *runtimePCSiteRecord + +//go:linkname runtimePCSiteEnd __llgo_pcsite_end +var runtimePCSiteEnd *runtimePCSiteRecord + +type runtimePCLineFrame struct { + pc uintptr + entry uintptr + function string + file string + line int + startLine int +} + +var runtimePCLineInitState uint32 +var runtimePCLineFrames []runtimePCLineFrame +var runtimePCLineIndex runtimePCFindIndex + +type runtimeFuncPCFrame struct { + entry uintptr + funcIndex uint32 +} + +type runtimePCFindBucket struct { + idx uint32 + subbuckets [runtimePCFindSubbucket]uint16 +} + +type runtimePCFindIndex struct { + base uintptr + buckets []runtimePCFindBucket +} + +const ( + // Keep the lookup geometry aligned with Go's pclntab findfunc table: + // 4096-byte buckets and 16 subbuckets. Go stores one-byte subbucket + // deltas because its linker guarantees a 16-byte minimum function size; + // LLGo has no minimum size for function entries and indexes call-site + // records that can sit a few bytes apart, so it stores two-byte deltas. + // A delta counts distinct PCs inside one 4096-byte bucket and therefore + // can never exceed 4096, which makes uint16 overflow impossible and the + // index unconditional. LLGo builds the index at first use after reading + // DCE-safe entry PC sections, because the LLVM IR stage does not yet own + // final text addresses the way cmd/link does for Go. + runtimePCMinFuncSize = uintptr(16) + runtimePCFindBucketSize = uintptr(256) * runtimePCMinFuncSize + runtimePCFindSubbucket = 16 + runtimeFuncPCEntrySlack = 64 +) + +var runtimeFuncPCInitState uint32 +var runtimeFuncPCFrames []runtimeFuncPCFrame +var runtimeFuncPCEntries []uintptr +var runtimeFuncPCIndex runtimePCFindIndex + +const ( + runtimeFuncInfoInitUninit uint32 = iota + runtimeFuncInfoInitDone + runtimeFuncInfoInitBusy + runtimeClosureStubPrefix = "__llgo_stub." + runtimePublicClosureStubPrefix = "_llgo_stub." +) + +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 isPCSiteSymbol(name string) bool { + for i := 0; i < len(name) && name[i] == '_'; i++ { + if hasStringPrefix(name[i:], "__llgo_pcsite_") { + return true + } + } + return false +} + +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 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 cStringLen(cstr *c.Char) int { + if cstr == nil { + return 0 + } + ptr := unsafe.Pointer(cstr) + for i := 0; ; i++ { + if *(*byte)(unsafe.Add(ptr, i)) == 0 { + return i + } + } +} + +func cStringAppend(dst []byte, cstr *c.Char) []byte { + if cstr == nil { + return dst + } + ptr := unsafe.Pointer(cstr) + for i := 0; ; i++ { + c := *(*byte)(unsafe.Add(ptr, i)) + if c == 0 { + return dst + } + dst = append(dst, c) + } +} + +func funcInfoCString(id uint16) *c.Char { + if runtimeFuncInfoStrings == nil || runtimeFuncInfoStringOffsets == nil || + uintptr(id) >= runtimeFuncInfoStringCount { + return nil + } + off := *(*uint32)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoStringOffsets), uintptr(id)*unsafe.Sizeof(*runtimeFuncInfoStringOffsets))) + 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 pcLineAt(i uintptr) *runtimePCLineRecord { + size := unsafe.Sizeof(*runtimePCLineTable) + return (*runtimePCLineRecord)(unsafe.Add(unsafe.Pointer(runtimePCLineTable), i*size)) +} + +func funcInfoStubIndexAt(i uintptr) uint32 { + size := unsafe.Sizeof(*runtimeFuncInfoStubIndexes) + return *(*uint32)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoStubIndexes), 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 funcInfoSymbolEqual(rec *runtimeFuncInfoRecord, symbol string) bool { + pkg := funcInfoCString(rec.symbolPkg) + name := funcInfoCString(rec.symbolName) + pkgLen := cStringLen(pkg) + nameLen := cStringLen(name) + if pkgLen == 0 { + return cStringCompare(name, symbol) == 0 + } + if len(symbol) != pkgLen+1+nameLen { + return false + } + if cStringCompare(pkg, symbol[:pkgLen]) != 0 || symbol[pkgLen] != '.' { + return false + } + return cStringCompare(name, symbol[pkgLen+1:]) == 0 +} + +func funcInfoJoinName(pkgID, nameID uint16) string { + pkg := funcInfoCString(pkgID) + name := funcInfoCString(nameID) + pkgLen := cStringLen(pkg) + nameLen := cStringLen(name) + if pkgLen == 0 { + return safeGoString(name, "") + } + if nameLen == 0 { + return safeGoString(pkg, "") + } + buf := make([]byte, 0, pkgLen+1+nameLen) + buf = cStringAppend(buf, pkg) + buf = append(buf, '.') + buf = cStringAppend(buf, name) + return string(buf) +} + +func funcInfoNameLen(pkgID, nameID uint16) int { + pkgLen := cStringLen(funcInfoCString(pkgID)) + nameLen := cStringLen(funcInfoCString(nameID)) + if pkgLen == 0 { + return nameLen + } + if nameLen == 0 { + return pkgLen + } + return pkgLen + 1 + nameLen +} + +func appendFuncInfoName(dst []byte, pkgID, nameID uint16) []byte { + pkg := funcInfoCString(pkgID) + name := funcInfoCString(nameID) + pkgLen := cStringLen(pkg) + nameLen := cStringLen(name) + if pkgLen == 0 { + return cStringAppend(dst, name) + } + if nameLen == 0 { + return cStringAppend(dst, pkg) + } + dst = cStringAppend(dst, pkg) + dst = append(dst, '.') + return cStringAppend(dst, name) +} + +func funcInfoJoinFile(rootID, nameID uint16) string { + root := funcInfoCString(rootID) + name := funcInfoCString(nameID) + rootLen := cStringLen(root) + nameLen := cStringLen(name) + if rootLen == 0 { + return safeGoString(name, "") + } + if nameLen == 0 { + return safeGoString(root, "") + } + buf := make([]byte, 0, rootLen+nameLen) + buf = cStringAppend(buf, root) + buf = cStringAppend(buf, name) + return string(buf) +} + +func funcInfoPackedFile(file uint32) string { + return funcInfoJoinFile(uint16(file>>16), uint16(file)) +} + +func maxFuncInfoSymbolLen() int { + maxLen := 0 + for i := uintptr(0); i < runtimeFuncInfoCount; i++ { + fn := funcInfoAt(i) + if n := funcInfoNameLen(fn.symbolPkg, fn.symbolName); n > maxLen { + maxLen = n + } + } + return maxLen +} + +func symbolPCBytes(name []byte) uintptr { + if len(name) == 0 { + return 0 + } + name = append(name, 0) + return uintptr(clitedebug.Symbol((*c.Char)(unsafe.Pointer(&name[0])))) +} + +func symbolPCFuncInfoName(buf []byte, pkgID, nameID uint16) uintptr { + name := appendFuncInfoName(buf[:0], pkgID, nameID) + return symbolPCBytes(name) +} + +func symbolPCPrefixedFuncInfoName(buf []byte, prefix string, pkgID, nameID uint16) uintptr { + name := append(buf[:0], prefix...) + name = appendFuncInfoName(name, pkgID, nameID) + return symbolPCBytes(name) +} + +func funcInfoFunctionName(fn *runtimeFuncInfoRecord) string { + if fn == nil { + return "" + } + if function := publicFunctionName(funcInfoJoinName(fn.namePkg, fn.nameName)); function != "" { + return function + } + return publicFunctionName(funcInfoJoinName(fn.symbolPkg, fn.symbolName)) +} + +func funcInfoFileName(fn *runtimeFuncInfoRecord) string { + if fn == nil { + return "" + } + return funcInfoJoinFile(fn.fileRoot, fn.fileName) +} + +func funcInfoForSymbol(symbol string) *runtimeFuncInfoRecord { + if symbol == "" || runtimeFuncInfoTable == nil || runtimeFuncInfoCount == 0 { + return nil + } + if runtimeFuncInfoStrings == nil || runtimeFuncInfoStringOffsets == nil || runtimeFuncInfoCount > 1<<20 || runtimeFuncInfoHashMask > 1<<22 { + return nil + } + if runtimeFuncInfoHash != nil && runtimeFuncInfoHashMask != 0 { + slot := funcInfoHashString(symbol) & runtimeFuncInfoHashMask + for probes := uintptr(0); probes <= runtimeFuncInfoHashMask; probes++ { + idx := *(*uint16)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoHash), slot*unsafe.Sizeof(*runtimeFuncInfoHash))) + if idx == 0 { + return nil + } + if uintptr(idx) <= runtimeFuncInfoCount { + rec := funcInfoAt(uintptr(idx) - 1) + if funcInfoSymbolEqual(rec, symbol) { + return rec + } + } + slot = (slot + 1) & runtimeFuncInfoHashMask + } + return nil + } + for i := uintptr(0); i < runtimeFuncInfoCount; i++ { + rec := funcInfoAt(i) + if funcInfoSymbolEqual(rec, symbol) { + return rec + } + } + return nil +} + +func funcInfoForRuntimeSymbol(symbol string) *runtimeFuncInfoRecord { + if rec := funcInfoForSymbol(symbol); rec != nil { + return rec + } + if hasStringPrefix(symbol, runtimeClosureStubPrefix) { + return funcInfoForSymbol(symbol[len(runtimeClosureStubPrefix):]) + } + if hasStringPrefix(symbol, runtimePublicClosureStubPrefix) { + return funcInfoForSymbol(symbol[len(runtimePublicClosureStubPrefix):]) + } + return nil +} + +func applyFuncInfo(sym *pcSymbol, rawFunction string) { + rec := funcInfoForRuntimeSymbol(rawFunction) + if rec == nil { + public := publicFunctionName(rawFunction) + if public != rawFunction { + rec = funcInfoForRuntimeSymbol(public) + } + } + if rec == nil { + return + } + if name := funcInfoJoinName(rec.namePkg, rec.nameName); name != "" { + sym.function = publicFunctionName(name) + } + if file := funcInfoJoinFile(rec.fileRoot, rec.fileName); 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 + if isPCSiteSymbol(rawFn) { + return pcSymbol{pc: pc} + } + 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 isPCSiteSymbol(rawFn) { + return pcSymbol{pc: pc} + } + 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 initRuntimeFuncPCFrames() { + if latomic.LoadUint32(&runtimeFuncPCInitState) == runtimeFuncInfoInitDone { + return + } + initRuntimeFuncPCFramesSlow() +} + +// runtimeFuncPCFramesBuilt reports whether the entry frame table has already +// been constructed, without triggering its construction. +func runtimeFuncPCFramesBuilt() bool { + return latomic.LoadUint32(&runtimeFuncPCInitState) == runtimeFuncInfoInitDone +} + +// Set LLGO_FUNCINFO_DEBUG=1 to print one line per lazily built runtime +// metadata table. This is how benchmarks and bug reports can tell whether a +// lookup used the compact find index or a degraded full-table fallback. +var runtimeFuncInfoDebugState uint32 + +var runtimeFuncPCFramesFromSites bool +var runtimeFuncPCStubsFromSites bool + +func runtimeFuncInfoDebugEnabled() bool { + state := latomic.LoadUint32(&runtimeFuncInfoDebugState) + if state == 0 { + state = 1 + if p := cliteos.Getenv(c.AllocaCStr("LLGO_FUNCINFO_DEBUG")); p != nil { + if v := c.GoString(p); v != "" && v != "0" { + state = 2 + } + } + latomic.StoreUint32(&runtimeFuncInfoDebugState, state) + } + return state == 2 +} + +func runtimeFuncInfoDebugSource(fromSites bool) string { + if fromSites { + return "sites" + } + return "dlsym" +} + +func runtimeFuncInfoDebugIndex(index runtimePCFindIndex) string { + if len(index.buckets) != 0 { + return "built" + } + return "fallback" +} + +func reportRuntimeFuncPCDebug() { + if !runtimeFuncInfoDebugEnabled() { + return + } + entrySrc := runtimeFuncInfoDebugSource(runtimeFuncPCFramesFromSites) + stubSrc := runtimeFuncInfoDebugSource(runtimeFuncPCStubsFromSites) + if runtimeFuncPCFramesPrebuilt { + entrySrc = "prebuilt" + stubSrc = "prebuilt" + } + frameCount := len(runtimeFuncPCFrames) + if runtimeFuncPCFramesPrebuilt { + frameCount = prebuiltFrameCount() + } + println("llgo funcinfo: func table frames=", frameCount, + " buckets=", len(runtimeFuncPCIndex.buckets), + " index=", runtimeFuncInfoDebugIndex(runtimeFuncPCIndex), + " entries=", entrySrc, + " stubs=", stubSrc) +} + +func reportRuntimePCLineDebug() { + if !runtimeFuncInfoDebugEnabled() { + return + } + println("llgo funcinfo: pcline table frames=", len(runtimePCLineFrames), + " buckets=", len(runtimePCLineIndex.buckets), + " index=", runtimeFuncInfoDebugIndex(runtimePCLineIndex)) +} + +func initRuntimeFuncPCFramesSlow() { + for { + state := latomic.LoadUint32(&runtimeFuncPCInitState) + switch state { + case runtimeFuncInfoInitDone: + return + case runtimeFuncInfoInitUninit: + if latomic.CompareAndSwapUint32(&runtimeFuncPCInitState, runtimeFuncInfoInitUninit, runtimeFuncInfoInitBusy) { + initRuntimeFuncPCFramesOnce() + latomic.StoreUint32(&runtimeFuncPCInitState, runtimeFuncInfoInitDone) + reportRuntimeFuncPCDebug() + return + } + } + c.Usleep(1) + } +} + +// Prebuilt table format written into the entry-site section by the +// link-phase tool (chore/pclnpost -write). Layout, all little-endian, +// 8-byte aligned at the section start: +// +// u64 magic "LLGOFTB1" +// u64 linkSectAddr link-time vmaddr of this section (informational) +// u64 base runtime PC of the first table entry +// u32 count ftab entries incl. trailing sentinel +// u32 bucketCount findfunctab buckets (runtime uint16 layout) +// count × {u32 entryOff /* relative to base */, u32 funcIndex} +// bucketCount × {u32 idx; 16 × u16 subbuckets} +// +// The base slot is a live relocation: on Mach-O the rewriter splices it back +// into the dyld chained-fixup page chain (so dyld both pre-touches the +// table's pages at load and writes the slid address), and on non-PIE ELF the +// link-time value already equals the runtime address. Either way the slot +// holds a runtime PC — no slide arithmetic here. +// +// The tool sorts, deduplicates LTO inline copies against the symbol table, +// and normalizes entries to true symbol starts, so adopting the table also +// retires first-use sorting and the dlsym/stub fallbacks. +const runtimePrebuiltMagic = uint64(0x314254464F474C4C) // "LLGOFTB1" little-endian +const runtimePrebuiltHeaderSize = 8 + 8 + 8 + 4 + 4 + +type runtimePrebuiltFtabEntry struct { + entryOff uint32 + funcIndex uint32 +} + +var runtimeFuncPCFramesPrebuilt bool + +// Zero-copy view of the prebuilt table: lookups binary-search the on-disk +// ftab directly; nothing is materialized at adoption time. +var runtimePrebuiltBase uintptr +var runtimePrebuiltFtab []runtimePrebuiltFtabEntry +var runtimePrebuiltEntriesOnce uint32 + +// adoptPrebuiltFuncPCTable installs a zero-copy view over the prebuilt table +// if the entry section carries the magic header. Returns false to fall back +// to first-use construction. +func adoptPrebuiltFuncPCTable() bool { + if runtimeFuncInfoEntryStart == nil || runtimeFuncInfoEntryEnd == nil { + return false + } + start := uintptr(unsafe.Pointer(runtimeFuncInfoEntryStart)) + end := uintptr(unsafe.Pointer(runtimeFuncInfoEntryEnd)) + if end < start+runtimePrebuiltHeaderSize { + return false + } + if *(*uint64)(unsafe.Pointer(start)) != runtimePrebuiltMagic { + return false + } + base := uintptr(*(*uint64)(unsafe.Pointer(start + 16))) + count := *(*uint32)(unsafe.Pointer(start + 24)) + bucketCount := *(*uint32)(unsafe.Pointer(start + 28)) + need := uintptr(runtimePrebuiltHeaderSize) + uintptr(count)*8 + + uintptr(bucketCount)*unsafe.Sizeof(runtimePCFindBucket{}) + if count < 2 || end < start+need || uintptr(count) > runtimeFuncInfoCount*16+1 { + return false + } + runtimePrebuiltBase = base + runtimePrebuiltFtab = unsafe.Slice((*runtimePrebuiltFtabEntry)(unsafe.Pointer(start+runtimePrebuiltHeaderSize)), count) + runtimeFuncPCIndex = runtimePCFindIndex{ + base: base &^ (runtimePCFindBucketSize - 1), + buckets: unsafe.Slice((*runtimePCFindBucket)(unsafe.Pointer(start+runtimePrebuiltHeaderSize+uintptr(count)*8)), bucketCount), + } + runtimeFuncPCFramesPrebuilt = true + runtimeFuncPCFramesFromSites = true + runtimeFuncPCStubsFromSites = true + return true +} + +// prebuiltFrame returns the ftab row as a runtimeFuncPCFrame view. +func prebuiltFrame(i int) runtimeFuncPCFrame { + e := runtimePrebuiltFtab[i] + return runtimeFuncPCFrame{entry: runtimePrebuiltBase + uintptr(e.entryOff), funcIndex: e.funcIndex} +} + +// prebuiltFrameCount excludes the trailing sentinel. +func prebuiltFrameCount() int { + return len(runtimePrebuiltFtab) - 1 +} + +// materializePrebuiltEntries lazily builds the funcIndex -> entry map that +// only the pcline initializer consumes; FuncForPC lookups never pay for it. +// Two-phase (busy/done) so concurrent losers wait for the winner's store. +func materializePrebuiltEntries() { + for { + switch latomic.LoadUint32(&runtimePrebuiltEntriesOnce) { + case 2: + return + case 0: + if !latomic.CompareAndSwapUint32(&runtimePrebuiltEntriesOnce, 0, 1) { + continue + } + entries := make([]uintptr, runtimeFuncInfoCount+1) + for _, e := range runtimePrebuiltFtab[:prebuiltFrameCount()] { + if e.funcIndex == 0 || uintptr(e.funcIndex) > runtimeFuncInfoCount { + continue + } + pc := runtimePrebuiltBase + uintptr(e.entryOff) + if entries[e.funcIndex] == 0 || pc < entries[e.funcIndex] { + entries[e.funcIndex] = pc + } + } + runtimeFuncPCEntries = entries + latomic.StoreUint32(&runtimePrebuiltEntriesOnce, 2) + return + default: + c.Usleep(1) + } + } +} + +func initRuntimeFuncPCFramesOnce() { + if runtimeFuncInfoTable == nil || + runtimeFuncInfoCount == 0 || + runtimeFuncInfoStrings == nil || + runtimeFuncInfoStringOffsets == nil { + return + } + if runtimeFuncInfoCount > 1<<20 { + return + } + if adoptPrebuiltFuncPCTable() { + return + } + frames := make([]runtimeFuncPCFrame, 0, runtimeFuncInfoCount) + entries := make([]uintptr, runtimeFuncInfoCount+1) + frames, usedEntrySites := appendRuntimeFuncInfoEntryFrames(frames, entries) + symbolBuf := []byte(nil) + if !usedEntrySites { + symbolBuf = make([]byte, 0, maxFuncInfoSymbolLen()+len(runtimeClosureStubPrefix)+1) + for i := uintptr(0); i < runtimeFuncInfoCount; i++ { + fn := funcInfoAt(i) + pc := symbolPCFuncInfoName(symbolBuf, fn.symbolPkg, fn.symbolName) + if pc == 0 { + continue + } + index := uint32(i + 1) + frames = append(frames, runtimeFuncPCFrame{ + entry: pc, + funcIndex: index, + }) + if entries[index] == 0 || pc < entries[index] { + entries[index] = pc + } + } + } + frames, usedStubSites := appendRuntimeFuncInfoStubSiteFrames(frames) + // Closure stubs are an ABI adapter and may go away in a future closure + // lowering. Keep the fallback compatibility table light: it stores only + // target funcinfo record indexes. When the stub-site section is present it + // is authoritative (linkers do not expose local stubs through dlsym), and + // skipping the dlsym loop below matters: each dlsym is a dynamic-loader + // query, and one query per stub used to dominate first-use latency. + if !usedStubSites && runtimeFuncInfoStubIndexes != nil && runtimeFuncInfoStubCount != 0 && runtimeFuncInfoStubCount <= runtimeFuncInfoCount { + if symbolBuf == nil { + symbolBuf = make([]byte, 0, maxFuncInfoSymbolLen()+len(runtimeClosureStubPrefix)+1) + } + for i := uintptr(0); i < runtimeFuncInfoStubCount; i++ { + index := funcInfoStubIndexAt(i) + if index == 0 || uintptr(index) > runtimeFuncInfoCount { + continue + } + fn := funcInfoAt(uintptr(index) - 1) + pc := symbolPCPrefixedFuncInfoName(symbolBuf, runtimeClosureStubPrefix, fn.symbolPkg, fn.symbolName) + if pc == 0 { + continue + } + frames = append(frames, runtimeFuncPCFrame{ + entry: pc, + funcIndex: index, + }) + } + } + sortRuntimeFuncPCFrames(frames) + frames = uniqueRuntimeFuncPCFrames(frames) + runtimeFuncPCFrames = frames + runtimeFuncPCEntries = entries + runtimeFuncPCIndex = buildRuntimeFuncPCIndex(frames) + runtimeFuncPCFramesFromSites = usedEntrySites + runtimeFuncPCStubsFromSites = usedStubSites +} + +func appendRuntimeFuncInfoEntryFrames(frames []runtimeFuncPCFrame, entries []uintptr) ([]runtimeFuncPCFrame, bool) { + if runtimeFuncInfoEntryStart == nil || runtimeFuncInfoEntryEnd == nil { + return frames, false + } + start := uintptr(unsafe.Pointer(runtimeFuncInfoEntryStart)) + end := uintptr(unsafe.Pointer(runtimeFuncInfoEntryEnd)) + size := unsafe.Sizeof(*runtimeFuncInfoEntryStart) + if end <= start || size == 0 || (end-start)%size != 0 { + return frames, false + } + nsite := (end - start) / size + if nsite > runtimeFuncInfoCount*16 || nsite > 1<<20 { + return frames, false + } + used := false + for i := uintptr(0); i < nsite; i++ { + site := (*runtimeFuncInfoEntryRecord)(unsafe.Pointer(start + i*size)) + if site == nil || site.pc == 0 || site.symbolID == 0 { + continue + } + funcIndex := funcInfoIndexForSymbolID(site.symbolID) + if funcIndex == 0 || uintptr(funcIndex) > runtimeFuncInfoCount { + continue + } + frames = append(frames, runtimeFuncPCFrame{ + entry: site.pc, + funcIndex: funcIndex, + }) + if entries[funcIndex] == 0 || site.pc < entries[funcIndex] { + entries[funcIndex] = site.pc + } + used = true + } + return frames, used +} + +func appendRuntimeFuncInfoStubSiteFrames(frames []runtimeFuncPCFrame) ([]runtimeFuncPCFrame, bool) { + if runtimeFuncInfoStubSiteStart == nil || runtimeFuncInfoStubSiteEnd == nil { + return frames, false + } + start := uintptr(unsafe.Pointer(runtimeFuncInfoStubSiteStart)) + end := uintptr(unsafe.Pointer(runtimeFuncInfoStubSiteEnd)) + size := unsafe.Sizeof(*runtimeFuncInfoStubSiteStart) + if end <= start || size == 0 || (end-start)%size != 0 { + return frames, false + } + nsite := (end - start) / size + if nsite > runtimeFuncInfoCount*16 || nsite > 1<<20 { + return frames, false + } + used := false + for i := uintptr(0); i < nsite; i++ { + site := (*runtimeFuncInfoStubSiteRecord)(unsafe.Pointer(start + i*size)) + if site == nil || site.pc == 0 || site.symbolID == 0 { + continue + } + funcIndex := funcInfoIndexForSymbolID(site.symbolID) + if funcIndex == 0 || uintptr(funcIndex) > runtimeFuncInfoCount { + continue + } + frames = append(frames, runtimeFuncPCFrame{ + entry: site.pc, + funcIndex: funcIndex, + }) + used = true + } + return frames, used +} + +func funcInfoIndexForSymbolID(symbolID uint64) uint32 { + if symbolID == 0 || runtimeFuncInfoSymbolIndex == nil || runtimeFuncInfoSymbolIndexCount == 0 { + return 0 + } + if runtimeFuncInfoSymbolIndexCount > runtimeFuncInfoCount || runtimeFuncInfoSymbolIndexCount > 1<<20 { + return 0 + } + lo, hi := uintptr(0), runtimeFuncInfoSymbolIndexCount + size := unsafe.Sizeof(*runtimeFuncInfoSymbolIndex) + for lo < hi { + mid := (lo + hi) >> 1 + rec := (*runtimeFuncInfoSymbolIndexRecord)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoSymbolIndex), mid*size)) + if rec.symbolID >= symbolID { + hi = mid + } else { + lo = mid + 1 + } + } + if lo >= runtimeFuncInfoSymbolIndexCount { + return 0 + } + rec := (*runtimeFuncInfoSymbolIndexRecord)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoSymbolIndex), lo*size)) + if rec.symbolID != symbolID || rec.funcIndex == 0 || uintptr(rec.funcIndex) > runtimeFuncInfoCount { + return 0 + } + return rec.funcIndex +} + +func sortRuntimeFuncPCFrames(frames []runtimeFuncPCFrame) { + if len(frames) < 2 { + return + } + quickSortRuntimeFuncPCFrames(frames, 0, len(frames)-1) +} + +func quickSortRuntimeFuncPCFrames(frames []runtimeFuncPCFrame, lo, hi int) { + for hi-lo > 16 { + mid := int(uint(lo+hi) >> 1) + if frames[mid].entry < frames[lo].entry { + frames[mid], frames[lo] = frames[lo], frames[mid] + } + if frames[hi].entry < frames[mid].entry { + frames[hi], frames[mid] = frames[mid], frames[hi] + } + if frames[mid].entry < frames[lo].entry { + frames[mid], frames[lo] = frames[lo], frames[mid] + } + pivot := frames[mid].entry + i, j := lo, hi + for { + for frames[i].entry < pivot { + i++ + } + for frames[j].entry > pivot { + j-- + } + if i >= j { + break + } + frames[i], frames[j] = frames[j], frames[i] + i++ + j-- + } + if j-lo < hi-i { + quickSortRuntimeFuncPCFrames(frames, lo, j) + lo = i + } else { + quickSortRuntimeFuncPCFrames(frames, i, hi) + hi = j + } + } + for i := lo + 1; i <= hi; i++ { + x := frames[i] + j := i - 1 + for j >= lo && frames[j].entry > x.entry { + frames[j+1] = frames[j] + j-- + } + frames[j+1] = x + } +} + +func uniqueRuntimeFuncPCFrames(frames []runtimeFuncPCFrame) []runtimeFuncPCFrame { + if len(frames) < 2 { + return frames + } + out := frames[:1] + for i := 1; i < len(frames); i++ { + if frames[i].entry == out[len(out)-1].entry { + out[len(out)-1] = frames[i] + continue + } + out = append(out, frames[i]) + } + return out +} + +// buildRuntimeFuncPCIndex is the runtime counterpart of Go's linker-built +// findfunctab. The table shape and lookup behavior are Go-style; the build time +// differs because LLGo's final function PCs are discovered from associated +// sections after link/load instead of being sorted directly by cmd/link. +func buildRuntimeFuncPCIndex(frames []runtimeFuncPCFrame) runtimePCFindIndex { + if len(frames) == 0 { + return runtimePCFindIndex{} + } + if uintptr(len(frames)) > ^uintptr(0)>>1 { + return runtimePCFindIndex{} + } + base := frames[0].entry &^ (runtimePCFindBucketSize - 1) + last := frames[len(frames)-1].entry + if last < base { + return runtimePCFindIndex{} + } + nbuckets := (last-base)/runtimePCFindBucketSize + 1 + if nbuckets > 1<<20 && nbuckets > uintptr(len(frames))*64 { + return runtimePCFindIndex{} + } + buckets := make([]runtimePCFindBucket, nbuckets) + subSize := runtimePCFindBucketSize / runtimePCFindSubbucket + for b := range buckets { + bucketStart := base + uintptr(b)*runtimePCFindBucketSize + baseIdx := runtimeFuncPCFrameIndexBinary(frames, bucketStart) + if baseIdx < 0 { + baseIdx = 0 + } + if baseIdx > len(frames)-1 { + baseIdx = len(frames) - 1 + } + buckets[b].idx = uint32(baseIdx) + for s := 0; s < runtimePCFindSubbucket; s++ { + pc := bucketStart + uintptr(s)*subSize + subIdx := runtimeFuncPCFrameIndexBinary(frames, pc) + if subIdx < 0 { + subIdx = 0 + } + if subIdx > len(frames)-1 { + subIdx = len(frames) - 1 + } + // delta counts deduplicated PCs inside one bucket, so it is + // bounded by the bucket size and always fits in uint16. + delta := subIdx - baseIdx + if delta < 0 || delta > 0xffff { + return runtimePCFindIndex{} + } + buckets[b].subbuckets[s] = uint16(delta) + } + } + return runtimePCFindIndex{base: base, buckets: buckets} +} + +func runtimePCFindRange(index runtimePCFindIndex, n int, pc uintptr) (int, int, bool) { + if n == 0 || len(index.buckets) == 0 || pc < index.base { + return 0, 0, false + } + off := pc - index.base + bucket := off / runtimePCFindBucketSize + if bucket >= uintptr(len(index.buckets)) { + return 0, 0, false + } + subSize := runtimePCFindBucketSize / runtimePCFindSubbucket + sub := (off % runtimePCFindBucketSize) / subSize + b := index.buckets[bucket] + lo := int(b.idx) + int(b.subbuckets[sub]) + hi := n + if sub+1 < runtimePCFindSubbucket { + hi = int(b.idx) + int(b.subbuckets[sub+1]) + } else if bucket+1 < uintptr(len(index.buckets)) { + hi = int(index.buckets[bucket+1].idx) + } + if lo > 0 { + lo-- + } + if hi < lo { + hi = lo + } + hi += 2 + if hi > n { + hi = n + } + if lo > n { + lo = n + } + return lo, hi, true +} + +// runtimeFuncPCFrameIndex mirrors runtime.findfunc: use the compact bucket +// table to jump near the containing function, then scan the sorted frame table +// inside that narrow range. +func runtimeFuncPCFrameIndex(pc uintptr) int { + if runtimeFuncPCFramesPrebuilt { + return prebuiltFrameIndex(pc) + } + frames := runtimeFuncPCFrames + if len(frames) == 0 { + return -1 + } + if lo, hi, ok := runtimePCFindRange(runtimeFuncPCIndex, len(frames), pc); ok { + for lo < hi { + mid := int(uint(lo+hi) >> 1) + if frames[mid].entry > pc { + hi = mid + } else { + lo = mid + 1 + } + } + idx := lo - 1 + if idx < 0 || frames[idx].entry > pc { + return -1 + } + return idx + } + return runtimeFuncPCFrameIndexBinary(frames, pc) +} + +func runtimeFuncPCFrameIndexBinary(frames []runtimeFuncPCFrame, pc uintptr) int { + lo, hi := 0, len(frames) + for lo < hi { + mid := int(uint(lo+hi) >> 1) + if frames[mid].entry > pc { + hi = mid + } else { + lo = mid + 1 + } + } + idx := lo - 1 + if idx < 0 { + return -1 + } + return idx +} + +// prebuiltFrameIndex is runtimeFuncPCFrameIndex over the zero-copy ftab: +// bucket narrowing via the shared find index, then binary search on +// entryOff. Returns the index of the last entry with PC <= pc, or -1. +func prebuiltFrameIndex(pc uintptr) int { + n := prebuiltFrameCount() + if n <= 0 || pc < runtimePrebuiltBase { + return -1 + } + off := uint32(pc - runtimePrebuiltBase) + lo, hi := 0, n + if l, h, ok := runtimePCFindRange(runtimeFuncPCIndex, n, pc); ok { + lo, hi = l, h + } + for lo < hi { + mid := int(uint(lo+hi) >> 1) + if runtimePrebuiltFtab[mid].entryOff > off { + hi = mid + } else { + lo = mid + 1 + } + } + idx := lo - 1 + if idx < 0 || runtimePrebuiltFtab[idx].entryOff > off { + return -1 + } + return idx +} + +func funcEntryForIndex(index uint32) uintptr { + if index == 0 { + return 0 + } + initRuntimeFuncPCFrames() + if runtimeFuncPCFramesPrebuilt { + materializePrebuiltEntries() + } + if uintptr(index) >= uintptr(len(runtimeFuncPCEntries)) { + return 0 + } + return runtimeFuncPCEntries[index] +} + +// coldFuncInfoEntryLookup resolves an exact function-entry PC by scanning the +// raw entry-site and stub-site sections, without building the sorted frame +// table and without any dynamic-loader query. Function values can point at +// either a real function entry or its closure stub, so both sections are +// scanned. The scan is linear, so it is capped: for larger binaries the +// dladdr cold path is cheaper than streaming the whole section. +const coldFuncInfoEntryScanLimit = 4096 + +// coldFuncInfoScanRange scans one {pc, symbolID} record section for the +// anchor nearest at-or-after pc within the warm path's entry slack (anchors +// are emitted from LLVM IR and land after the backend prologue). It returns +// the matched funcinfo index and delta, or (0, maxDelta) on miss. +func coldFuncInfoScanRange(start, end, size, pc uintptr, bestDelta uintptr) (uint32, uintptr) { + if start == 0 || end <= start || size == 0 || (end-start)%size != 0 { + return 0, bestDelta + } + nsite := (end - start) / size + if nsite > coldFuncInfoEntryScanLimit || nsite > runtimeFuncInfoCount*16 { + return 0, bestDelta + } + bestIndex := uint32(0) + for i := uintptr(0); i < nsite; i++ { + site := (*runtimeFuncInfoEntryRecord)(unsafe.Pointer(start + i*size)) + if site.symbolID == 0 || site.pc < pc { + continue + } + delta := site.pc - pc + if delta >= bestDelta { + continue + } + funcIndex := funcInfoIndexForSymbolID(site.symbolID) + if funcIndex == 0 || uintptr(funcIndex) > runtimeFuncInfoCount { + continue + } + bestDelta = delta + bestIndex = funcIndex + if delta == 0 { + break + } + } + return bestIndex, bestDelta +} + +// coldFuncPCLookupBudget grants a small number of table-free cold lookups per +// process; past that, building the sorted table amortizes better than more +// linear scans or dladdr calls. +var coldFuncPCLookupCount uint32 + +func coldFuncPCLookupBudget() bool { + if prebuiltFuncPCTablePresent() { + // The prebuilt table makes first-use initialization cheap; skip the + // scan/dladdr path entirely and let the caller fall through to it. + return false + } + return latomic.AddUint32(&coldFuncPCLookupCount, 1) <= 8 +} + +// prebuiltFuncPCTablePresent reports whether the entry section carries the +// link-phase prebuilt table, in which case the cold scan must not interpret +// its bytes as site records — and does not need to: adopting the prebuilt +// table is itself cheap. +func prebuiltFuncPCTablePresent() bool { + if runtimeFuncInfoEntryStart == nil || runtimeFuncInfoEntryEnd == nil { + return false + } + start := uintptr(unsafe.Pointer(runtimeFuncInfoEntryStart)) + end := uintptr(unsafe.Pointer(runtimeFuncInfoEntryEnd)) + return end >= start+8 && *(*uint64)(unsafe.Pointer(start)) == runtimePrebuiltMagic +} + +// runtimeFuncInfoWarmSink keeps the warm-up loads observable. +var runtimeFuncInfoWarmSink byte + +// init pre-warms the prebuilt function table, mirroring Go: Go's pclntab +// pages are touched by the runtime itself (traceback, GC) long before user +// code queries it, so its "first" FuncForPC never pays page-in. Touching the +// pages the lookup path reads — adopted blob, funcinfo records, string +// offsets, strings — moves first-touch page-in (plus, on darwin, +// code-signature validation) from the first user lookup to process startup +// (tens of µs once, on binaries that carry funcinfo tables). Without a +// prebuilt table everything stays lazy: first-use construction allocates, +// which has no place in init, and programs that never introspect pay +// nothing. +func init() { + if !prebuiltFuncPCTablePresent() { + return + } + initRuntimeFuncPCFrames() // zero-copy adoption, sub-µs + touch := func(base unsafe.Pointer, n uintptr) { + if base == nil || n == 0 { + return + } + const pageStep = 4096 + sink := runtimeFuncInfoWarmSink + p := uintptr(base) + for off := uintptr(0); off < n; off += pageStep { + sink += *(*byte)(unsafe.Pointer(p + off)) + } + sink += *(*byte)(unsafe.Pointer(p + n - 1)) + runtimeFuncInfoWarmSink = sink + } + start := uintptr(unsafe.Pointer(runtimeFuncInfoEntryStart)) + count := *(*uint32)(unsafe.Pointer(start + 24)) + bucketCount := *(*uint32)(unsafe.Pointer(start + 28)) + need := uintptr(runtimePrebuiltHeaderSize) + uintptr(count)*8 + + uintptr(bucketCount)*unsafe.Sizeof(runtimePCFindBucket{}) + touch(unsafe.Pointer(runtimeFuncInfoEntryStart), need) + touch(unsafe.Pointer(runtimeFuncInfoTable), + runtimeFuncInfoCount*unsafe.Sizeof(runtimeFuncInfoRecord{})) + touch(unsafe.Pointer(runtimeFuncInfoStringOffsets), + runtimeFuncInfoStringCount*unsafe.Sizeof(uint32(0))) + if runtimeFuncInfoStrings != nil && runtimeFuncInfoStringCount > 0 { + last := uintptr(*(*uint32)(unsafe.Add(unsafe.Pointer(runtimeFuncInfoStringOffsets), + (runtimeFuncInfoStringCount-1)*unsafe.Sizeof(uint32(0))))) + lastStr := funcInfoCString(uint16(runtimeFuncInfoStringCount - 1)) + touch(unsafe.Pointer(runtimeFuncInfoStrings), last+uintptr(cStringLen(lastStr))+1) + } + // One synthetic lookup warms the code paths themselves (allocator size + // classes, lookup caches), not just the data pages. + if prebuiltFrameCount() > 0 { + frame := prebuiltFrame(0) + if sym, ok := pcSymbolForFuncInfoIndex(frame.entry, frame.entry, frame.funcIndex); ok { + runtimeFuncInfoWarmSink += byte(len(sym.function)) + } + } + // Write-warm the FuncForPC cache: its first stores otherwise take + // zero-fill write faults, one per page, on the first few lookups. + for i := 0; i < funcForPCCacheSets; i += 4096 / int(unsafe.Sizeof(funcForPCCache[0])) { + funcForPCCache[i][0].pc = 0 + } + funcForPCCache[funcForPCCacheSets-1][0].pc = 0 +} + +func coldFuncInfoEntryLookup(pc uintptr) (pcSymbol, bool) { + if pc == 0 || prebuiltFuncPCTablePresent() { + return pcSymbol{}, false + } + bestDelta := uintptr(runtimeFuncPCEntrySlack) + 1 + bestIndex := uint32(0) + if runtimeFuncInfoEntryStart != nil && runtimeFuncInfoEntryEnd != nil { + bestIndex, bestDelta = coldFuncInfoScanRange( + uintptr(unsafe.Pointer(runtimeFuncInfoEntryStart)), + uintptr(unsafe.Pointer(runtimeFuncInfoEntryEnd)), + unsafe.Sizeof(*runtimeFuncInfoEntryStart), pc, bestDelta) + } + if bestDelta != 0 && runtimeFuncInfoStubSiteStart != nil && runtimeFuncInfoStubSiteEnd != nil { + if idx, _ := coldFuncInfoScanRange( + uintptr(unsafe.Pointer(runtimeFuncInfoStubSiteStart)), + uintptr(unsafe.Pointer(runtimeFuncInfoStubSiteEnd)), + unsafe.Sizeof(*runtimeFuncInfoStubSiteStart), pc, bestDelta); idx != 0 { + bestIndex = idx + } + } + if bestIndex == 0 { + return pcSymbol{}, false + } + return pcSymbolForFuncInfoIndex(pc, pc, bestIndex) +} + +func funcPCFrameForPC(pc uintptr) (pcSymbol, bool) { + if pc == 0 { + return pcSymbol{}, false + } + initRuntimeFuncPCFrames() + idx := runtimeFuncPCFrameIndex(pc) + if idx < 0 { + return pcSymbol{}, false + } + var frame runtimeFuncPCFrame + if runtimeFuncPCFramesPrebuilt { + frame = prebuiltFrame(idx) + } else { + frame = runtimeFuncPCFrames[idx] + } + return pcSymbolForFuncInfoIndex(pc, frame.entry, frame.funcIndex) +} + +func funcPCFrameForEntryPC(pc uintptr) (pcSymbol, bool) { + if pc == 0 { + return pcSymbol{}, false + } + initRuntimeFuncPCFrames() + if runtimeFuncPCFramesPrebuilt { + // Prebuilt entries are true symbol starts (normalized against the + // symbol table by the link-phase tool), so no slack is needed. + idx := prebuiltFrameIndex(pc) + if idx < 0 { + return pcSymbol{}, false + } + frame := prebuiltFrame(idx) + if frame.entry != pc { + return pcSymbol{}, false + } + return pcSymbolForFuncInfoIndex(pc, pc, frame.funcIndex) + } + frames := runtimeFuncPCFrames + if len(frames) == 0 { + return pcSymbol{}, false + } + lo, hi := 0, len(frames) + for lo < hi { + mid := int(uint(lo+hi) >> 1) + if frames[mid].entry >= pc { + hi = mid + } else { + lo = mid + 1 + } + } + if lo >= len(frames) { + return pcSymbol{}, false + } + frame := frames[lo] + if frame.entry != pc && frame.entry-pc > runtimeFuncPCEntrySlack { + return pcSymbol{}, false + } + return pcSymbolForFuncInfoIndex(pc, pc, frame.funcIndex) +} + +func pcSymbolForFuncInfoIndex(pc, entry uintptr, funcIndex uint32) (pcSymbol, bool) { + if funcIndex == 0 || uintptr(funcIndex) > runtimeFuncInfoCount { + return pcSymbol{}, false + } + fn := funcInfoAt(uintptr(funcIndex) - 1) + line := int(fn.line) + return pcSymbol{ + pc: pc, + entry: entry, + function: funcInfoFunctionName(fn), + file: funcInfoFileName(fn), + line: line, + startLine: line, + ok: true, + }, true +} + +func initRuntimePCLineFrames() { + if latomic.LoadUint32(&runtimePCLineInitState) == runtimeFuncInfoInitDone { + return + } + initRuntimePCLineFramesSlow() +} + +func initRuntimePCLineFramesSlow() { + for { + state := latomic.LoadUint32(&runtimePCLineInitState) + switch state { + case runtimeFuncInfoInitDone: + return + case runtimeFuncInfoInitUninit: + if latomic.CompareAndSwapUint32(&runtimePCLineInitState, runtimeFuncInfoInitUninit, runtimeFuncInfoInitBusy) { + initRuntimePCLineFramesOnce() + latomic.StoreUint32(&runtimePCLineInitState, runtimeFuncInfoInitDone) + reportRuntimePCLineDebug() + return + } + } + c.Usleep(1) + } +} + +func initRuntimePCLineFramesOnce() { + if runtimePCLineTable == nil || + runtimePCLineCount == 0 || + runtimePCSiteStart == nil || + runtimePCSiteEnd == nil || + runtimeFuncInfoTable == nil || + runtimeFuncInfoCount == 0 || + runtimeFuncInfoStrings == nil || + runtimeFuncInfoStringOffsets == nil { + return + } + if runtimePCLineCount > 1<<20 || runtimePCLineCount > runtimeFuncInfoCount*1024 { + return + } + start := uintptr(unsafe.Pointer(runtimePCSiteStart)) + end := uintptr(unsafe.Pointer(runtimePCSiteEnd)) + size := unsafe.Sizeof(*runtimePCSiteStart) + if end <= start || size == 0 || (end-start)%size != 0 { + return + } + nsite := (end - start) / size + if nsite > runtimePCLineCount*1024 || nsite > 1<<22 { + return + } + frames := make([]runtimePCLineFrame, 0, nsite) + symbolBuf := make([]byte, 0, maxFuncInfoSymbolLen()+1) + // Sites vastly outnumber distinct functions and files, so materialize the + // per-function strings and entry PCs once and the packed file strings once + // per file ID. Building them per site used to dominate first-use latency. + type pcLineFuncInfo struct { + entry uintptr + function string + file string + line int + resolved bool + } + funcCache := make([]pcLineFuncInfo, runtimeFuncInfoCount+1) + fileCache := make(map[uint32]string) + for i := uintptr(0); i < nsite; i++ { + site := (*runtimePCSiteRecord)(unsafe.Pointer(start + i*size)) + if site == nil || site.id == 0 || site.pc == 0 { + continue + } + rec := pcLineInfoForID(site.id) + if rec == nil || rec.funcIndex == 0 || uintptr(rec.funcIndex) > runtimeFuncInfoCount { + continue + } + pc := site.pc + fn := funcInfoAt(uintptr(rec.funcIndex) - 1) + fc := &funcCache[rec.funcIndex] + if !fc.resolved { + fc.entry = funcEntryForIndex(rec.funcIndex) + if fc.entry == 0 { + fc.entry = symbolPCFuncInfoName(symbolBuf, fn.symbolPkg, fn.symbolName) + } + fc.function = publicFunctionName(funcInfoJoinName(fn.namePkg, fn.nameName)) + if fc.function == "" { + fc.function = publicFunctionName(funcInfoJoinName(fn.symbolPkg, fn.symbolName)) + } + fc.file = funcInfoJoinFile(fn.fileRoot, fn.fileName) + fc.line = int(fn.line) + fc.resolved = true + } + entry := fc.entry + if entry == 0 { + sym := addrInfoSymbol(pc) + entry = sym.entry + } + file := "" + if rec.file != 0 { + var ok bool + if file, ok = fileCache[rec.file]; !ok { + file = funcInfoPackedFile(rec.file) + fileCache[rec.file] = file + } + } + if file == "" { + file = fc.file + } + line := int(rec.line) + if line == 0 { + line = fc.line + } + frames = append(frames, runtimePCLineFrame{ + pc: pc, + entry: entry, + function: fc.function, + file: file, + line: line, + startLine: fc.line, + }) + } + sortRuntimePCLineFrames(frames) + frames = uniqueRuntimePCLineFrames(frames) + runtimePCLineFrames = frames + runtimePCLineIndex = buildRuntimePCLineIndex(frames) +} + +func pcLineInfoForID(id uint64) *runtimePCLineRecord { + lo, hi := uintptr(0), runtimePCLineCount + for lo < hi { + mid := (lo + hi) >> 1 + rec := pcLineAt(mid) + if rec.id >= id { + hi = mid + } else { + lo = mid + 1 + } + } + if lo >= runtimePCLineCount { + return nil + } + rec := pcLineAt(lo) + if rec.id != id { + return nil + } + return rec +} + +func symbolPC(symbol string) uintptr { + if symbol == "" { + return 0 + } + buf := make([]byte, len(symbol)+1) + copy(buf, symbol) + return uintptr(clitedebug.Symbol((*c.Char)(unsafe.Pointer(&buf[0])))) +} + +func sortRuntimePCLineFrames(frames []runtimePCLineFrame) { + if len(frames) < 2 { + return + } + quickSortRuntimePCLineFrames(frames, 0, len(frames)-1) +} + +func quickSortRuntimePCLineFrames(frames []runtimePCLineFrame, lo, hi int) { + for hi-lo > 16 { + mid := int(uint(lo+hi) >> 1) + if frames[mid].pc < frames[lo].pc { + frames[mid], frames[lo] = frames[lo], frames[mid] + } + if frames[hi].pc < frames[mid].pc { + frames[hi], frames[mid] = frames[mid], frames[hi] + } + if frames[mid].pc < frames[lo].pc { + frames[mid], frames[lo] = frames[lo], frames[mid] + } + pivot := frames[mid].pc + i, j := lo, hi + for { + for frames[i].pc < pivot { + i++ + } + for frames[j].pc > pivot { + j-- + } + if i >= j { + break + } + frames[i], frames[j] = frames[j], frames[i] + i++ + j-- + } + if j-lo < hi-i { + quickSortRuntimePCLineFrames(frames, lo, j) + lo = i + } else { + quickSortRuntimePCLineFrames(frames, i, hi) + hi = j + } + } + for i := lo + 1; i <= hi; i++ { + x := frames[i] + j := i - 1 + for j >= lo && frames[j].pc > x.pc { + frames[j+1] = frames[j] + j-- + } + frames[j+1] = x + } +} + +func uniqueRuntimePCLineFrames(frames []runtimePCLineFrame) []runtimePCLineFrame { + if len(frames) < 2 { + return frames + } + out := frames[:1] + for i := 1; i < len(frames); i++ { + if frames[i].pc == out[len(out)-1].pc { + out[len(out)-1] = frames[i] + continue + } + out = append(out, frames[i]) + } + return out +} + +// buildRuntimePCLineIndex reuses the same Go-style bucket geometry for +// statement PC-line sites. Go stores dense per-function pcdata; LLGo keeps +// statement sites as a separate sorted table for now, but the hot PC lookup +// follows the same bucket/subbucket narrowing. +func buildRuntimePCLineIndex(frames []runtimePCLineFrame) runtimePCFindIndex { + if len(frames) == 0 { + return runtimePCFindIndex{} + } + base := frames[0].pc &^ (runtimePCFindBucketSize - 1) + last := frames[len(frames)-1].pc + if last < base { + return runtimePCFindIndex{} + } + nbuckets := (last-base)/runtimePCFindBucketSize + 1 + if nbuckets > 1<<20 && nbuckets > uintptr(len(frames))*64 { + return runtimePCFindIndex{} + } + buckets := make([]runtimePCFindBucket, nbuckets) + subSize := runtimePCFindBucketSize / runtimePCFindSubbucket + for b := range buckets { + bucketStart := base + uintptr(b)*runtimePCFindBucketSize + baseIdx := runtimePCLineFrameIndexBinary(frames, bucketStart, false) + if baseIdx < 0 { + baseIdx = 0 + } + if baseIdx > len(frames)-1 { + baseIdx = len(frames) - 1 + } + buckets[b].idx = uint32(baseIdx) + for s := 0; s < runtimePCFindSubbucket; s++ { + pc := bucketStart + uintptr(s)*subSize + subIdx := runtimePCLineFrameIndexBinary(frames, pc, false) + if subIdx < 0 { + subIdx = 0 + } + if subIdx > len(frames)-1 { + subIdx = len(frames) - 1 + } + // delta counts deduplicated PCs inside one bucket, so it is + // bounded by the bucket size and always fits in uint16. + delta := subIdx - baseIdx + if delta < 0 || delta > 0xffff { + return runtimePCFindIndex{} + } + buckets[b].subbuckets[s] = uint16(delta) + } + } + return runtimePCFindIndex{base: base, buckets: buckets} +} + +func runtimePCLineFrameRange(pc uintptr) (int, int) { + frames := runtimePCLineFrames + if lo, hi, ok := runtimePCFindRange(runtimePCLineIndex, len(frames), pc); ok { + return lo, hi + } + return 0, len(frames) +} + +func runtimePCLineFrameIndex(pc uintptr, exact bool) int { + frames := runtimePCLineFrames + if len(frames) == 0 { + return -1 + } + lo, hi := runtimePCLineFrameRange(pc) + return runtimePCLineFrameIndexInRange(frames, pc, exact, lo, hi) +} + +func runtimePCLineFrameIndexBinary(frames []runtimePCLineFrame, pc uintptr, exact bool) int { + return runtimePCLineFrameIndexInRange(frames, pc, exact, 0, len(frames)) +} + +func runtimePCLineFrameIndexInRange(frames []runtimePCLineFrame, pc uintptr, exact bool, lo, hi int) int { + for lo < hi { + mid := int(uint(lo+hi) >> 1) + if frames[mid].pc > pc || (exact && frames[mid].pc == pc) { + hi = mid + } else { + lo = mid + 1 + } + } + if exact { + if lo >= len(frames) || frames[lo].pc != pc { + return -1 + } + return lo + } + idx := lo - 1 + if idx < 0 { + return -1 + } + return idx +} + +func pcLineFrameForPC(pc, entry uintptr) (pcSymbol, bool) { + if pc == 0 { + return pcSymbol{}, false + } + initRuntimePCLineFrames() + frames := runtimePCLineFrames + idx := runtimePCLineFrameIndex(pc, false) + if idx < 0 { + return pcSymbol{}, false + } + frame := frames[idx] + // When the caller knows the function entry, only accept a site from the + // same function. A site with an unresolved entry cannot prove it belongs + // to the queried function, so it must be rejected too — otherwise a + // nearest-below hit from a neighboring function leaks its file/line. + if entry != 0 && frame.entry != entry { + return pcSymbol{}, false + } + return pcSymbol{ + pc: pc, + entry: frame.entry, + function: frame.function, + file: frame.file, + line: frame.line, + startLine: frame.startLine, + ok: true, + }, true +} + +func pcLineFrameForExactPC(pc uintptr) (pcSymbol, bool) { + if pc == 0 { + return pcSymbol{}, false + } + initRuntimePCLineFrames() + frames := runtimePCLineFrames + idx := runtimePCLineFrameIndex(pc, true) + if idx < 0 { + return pcSymbol{}, false + } + frame := frames[idx] + return pcSymbol{ + pc: pc, + entry: frame.entry, + function: frame.function, + file: frame.file, + line: frame.line, + startLine: frame.startLine, + ok: true, + }, true +} + +func mergePCLineSymbol(base, line pcSymbol) pcSymbol { + if line.entry == 0 { + line.entry = base.entry + } + if line.function == "" { + line.function = base.function + } + if line.file == "" { + line.file = base.file + } + if line.line == 0 { + line.line = base.line + } + if line.startLine == 0 { + line.startLine = base.startLine + } + line.ok = true + return line +} + +func frameSymbol(pc uintptr) pcSymbol { + if pc&3 != 0 { + if frame, ok := rtdebug.FrameForPC(pc); ok { + return pcSymbol{ + pc: pc, + entry: frame.Entry, + function: frame.Function, + file: frame.File, + line: frame.Line, + startLine: frame.StartLine, + ok: true, + } + } + } + if pc == 0 { + sym := addrInfoSymbol(pc) + if frame, ok := rtdebug.FrameForPC(pc); ok { + return pcSymbol{ + pc: pc, + entry: frame.Entry, + function: frame.Function, + file: frame.File, + line: frame.Line, + startLine: frame.StartLine, + ok: true, + } + } + return sym + } + if lineSym, ok := pcLineFrameForExactPC(pc); ok { + return lineSym + } + if lineSym, ok := pcLineFrameForExactPC(pc - 1); ok { + lineSym.pc = pc + return lineSym + } + sym := addrInfoSymbol(pc) + if lineSym, ok := pcLineFrameForPC(pc, sym.entry); ok { + return mergePCLineSymbol(sym, lineSym) + } + if sym.entry == 0 || pc > sym.entry { + if callSym := addrInfoSymbol(pc - 1); callSym.ok { + if lineSym, ok := pcLineFrameForPC(pc-1, callSym.entry); ok { + lineSym.pc = pc + return mergePCLineSymbol(callSym, lineSym) + } + callSym.pc = pc + return callSym + } + } + if !sym.ok { + if funcSym, ok := funcPCFrameForPC(pc); ok { + return funcSym + } + } + if frame, ok := rtdebug.FrameForPC(pc); ok { + return pcSymbol{ + pc: pc, + entry: frame.Entry, + function: frame.Function, + file: frame.File, + line: frame.Line, + startLine: frame.StartLine, + ok: true, + } + } + return sym +} + func (ci *Frames) Next() (frame Frame, more bool) { for len(ci.frames) < 2 { // Find the next frame. @@ -119,8 +1990,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 +2002,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 = frameFuncForPC(pc, sym, 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 +2052,33 @@ 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 + pc uintptr + file string + line 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) { - var info clitedebug.Info - if pc == 0 || clitedebug.Addrinfo(unsafe.Pointer(pc), &info) == 0 { - return "", 0 + if f != nil && f.pc == pc && (f.file != "" || f.line != 0) { + return f.file, f.line } - return safeGoString(info.Fname, ""), 0 + sym := frameSymbol(pc) + return sym.file, sym.line } // moduledata records information about the layout of the executable diff --git a/runtime/internal/lib/runtime/time_llgo.go b/runtime/internal/lib/runtime/time_llgo.go index 0a09c98e07..3f60f7d348 100644 --- a/runtime/internal/lib/runtime/time_llgo.go +++ b/runtime/internal/lib/runtime/time_llgo.go @@ -547,7 +547,5 @@ func timeSleepWake(arg any, _ uintptr) { } func runtimeNano() int64 { - tv := (*ct.Timespec)(c.Alloca(unsafe.Sizeof(ct.Timespec{}))) - ct.ClockGettime(ct.CLOCK_MONOTONIC, tv) - return int64(tv.Sec)*1e9 + int64(tv.Nsec) + return nanotime1() } diff --git a/runtime/internal/lib/runtime/time_llgo_go123.go b/runtime/internal/lib/runtime/time_llgo_go123.go index ceb2d73ed4..65bbff2f4d 100644 --- a/runtime/internal/lib/runtime/time_llgo_go123.go +++ b/runtime/internal/lib/runtime/time_llgo_go123.go @@ -581,7 +581,5 @@ func resetTimer(t *timeTimer, when, period int64) bool { } func runtimeNano() int64 { - tv := (*ct.Timespec)(c.Alloca(unsafe.Sizeof(ct.Timespec{}))) - ct.ClockGettime(ct.CLOCK_MONOTONIC, tv) - return int64(tv.Sec)*1e9 + int64(tv.Nsec) + return nanotime1() } diff --git a/runtime/internal/runtime/caller.go b/runtime/internal/runtime/caller.go new file mode 100644 index 0000000000..341158e825 --- /dev/null +++ b/runtime/internal/runtime/caller.go @@ -0,0 +1,493 @@ +/* + * 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" + + clitedebug "github.com/goplus/llgo/runtime/internal/clite/debug" + "github.com/goplus/llgo/runtime/internal/clite/tls" +) + +type CallerFrame struct { + PC uintptr + Entry uintptr + Function string + File string + Line int + StartLine int + // captured memoizes the interned synthetic PC base (seq << 2) for this + // exact frame content. It is cleared whenever the frame's line info + // changes, so repeated Caller/Callers walks over an unchanged stack skip + // the intern hash probe entirely. Only meaningful inside shadow-stack + // slots; ignored by frame comparison and hashing. + captured uintptr +} + +const callerLocationLimit = 4096 + +const ( + callerPCMask = uintptr(3) + callerPCValue = uintptr(1) + callersPCValue = uintptr(3) + callerPCHashInit = 64 +) + +type callerLocationStore struct { + frames []CallerFrame + stack []CallerFrame + synthetic []CallerFrame + syntheticHash []uintptr + // Memoized synthetic PC bases for the static frames emitted around every + // Callers walk. Per-store because synthetic sequences are per-store. + callersPCBase uintptr + mainPCBase uintptr + goexitPCBase uintptr +} + +var callerLocationTLS = tls.Alloc[*callerLocationStore](nil) + +func PushCallerLocationFrame(entry uintptr, name, file string, startLine int) int { + store := callerLocationStoreForThread() + mark := len(store.stack) + store.stack = append(store.stack, CallerFrame{ + PC: entry, + Entry: entry, + Function: name, + File: file, + Line: startLine, + StartLine: startLine, + }) + return mark +} + +func PopCallerLocationFrame(mark int) { + store := callerLocationTLS.Get() + if store == nil { + return + } + oldLen := len(store.stack) + if mark < 0 || mark > oldLen { + return + } + var zero CallerFrame + for i := mark; i < oldLen; i++ { + store.stack[i] = zero + } + store.stack = store.stack[:mark] +} + +func RecordCallerLocation(entry uintptr, name, file string, line int) { + if entry == 0 || line <= 0 { + return + } + updateCurrentFrame(entry, name, file, line) + recordPCLocation(0, entry, name, file, line) +} + +func RecordPanicLocation(entry uintptr, name, file string, line int) { + if entry == 0 || line <= 0 { + return + } + updateCurrentFrame(entry, name, file, line) + recordPCLocation(0, entry, name, file, line) +} + +func updateCurrentFrame(entry uintptr, name, file string, line int) { + store := callerLocationTLS.Get() + if store == nil { + return + } + for i := len(store.stack) - 1; i >= 0; i-- { + frame := &store.stack[i] + if frame.Entry == entry { + frame.Function = name + frame.File = file + // For one entry the instrumented name/file operands are + // constants; only the line changes between call sites. Comparing + // just the line keeps this per-call path free of string + // comparisons while still invalidating the capture memo whenever + // the frame content can differ. + if frame.Line != line { + frame.Line = line + frame.captured = 0 + } + return + } + } +} + +func recordPCLocation(pc, entry uintptr, name, file string, line int) { + store := callerLocationStoreForThread() + for i := range store.frames { + frame := &store.frames[i] + if (pc != 0 && frame.PC == pc) || (pc == 0 && frame.PC == 0 && frame.Entry == entry) { + frame.PC = pc + frame.Entry = entry + frame.Function = name + frame.File = file + frame.Line = line + return + } + } + if len(store.frames) >= callerLocationLimit { + copy(store.frames, store.frames[1:]) + store.frames[len(store.frames)-1] = CallerFrame{} + store.frames = store.frames[:len(store.frames)-1] + } + store.frames = append(store.frames, CallerFrame{ + PC: pc, + Entry: entry, + Function: name, + File: file, + Line: line, + }) +} + +func Caller(skip int) (CallerFrame, bool) { + if skip < 0 { + return CallerFrame{}, false + } + store := callerLocationTLS.Get() + if store == nil || len(store.stack) == 0 { + return CallerFrame{}, false + } + if skip < len(store.stack) { + return store.captureFrameAt(&store.stack[len(store.stack)-1-skip], callerPCValue), true + } + switch skip - len(store.stack) { + case 0: + return store.captureFrame(runtimeMainFrame, callerPCValue), true + case 1: + return store.captureFrame(runtimeGoexitFrame, callerPCValue), true + default: + return CallerFrame{}, false + } +} + +func Callers(skip int, pcs []uintptr) int { + if len(pcs) == 0 { + return 0 + } + if skip < 0 { + skip = 0 + } + store := callerLocationTLS.Get() + if store == nil || len(store.stack) == 0 { + return 0 + } + // Unrolled emit sequence: no closure so nothing escapes, and frames the + // skip count drops are never captured at all. + n := 0 + if skip > 0 { + skip-- + } else { + pcs[n] = store.staticPC(runtimeCallersFrame, &store.callersPCBase, callersPCValue) + n++ + } + for i := len(store.stack) - 1; i >= 0; i-- { + if skip > 0 { + skip-- + continue + } + if n >= len(pcs) { + return n + } + pcs[n] = store.capturePC(&store.stack[i], callersPCValue) + n++ + } + if skip > 0 { + skip-- + } else { + if n >= len(pcs) { + return n + } + pcs[n] = store.staticPC(runtimeMainFrame, &store.mainPCBase, callersPCValue) + n++ + } + if skip <= 0 && n < len(pcs) { + pcs[n] = store.staticPC(runtimeGoexitFrame, &store.goexitPCBase, callersPCValue) + n++ + } + return n +} + +func SavePanicCallerFrames() { +} + +func BindCallerLocation(pc uintptr, rawName string) { + store := callerLocationTLS.Get() + if store == nil || pc == 0 { + return + } + if frame, ok := callerLocationByName(store, rawName); ok { + bindCallerLocationPC(pc, frame) + return + } +} + +var ( + runtimeCallersFrame = CallerFrame{Function: "runtime.Callers"} + runtimeMainFrame = CallerFrame{Function: "runtime.main"} + runtimeGoexitFrame = CallerFrame{Function: "runtime.goexit"} +) + +func callerLocationByName(store *callerLocationStore, rawName string) (CallerFrame, bool) { + if rawName == "" { + return CallerFrame{}, false + } + name := normalizeRuntimeFuncName(rawName) + for i := len(store.frames) - 1; i >= 0; i-- { + frame := store.frames[i] + if frame.PC == 0 && frame.Function == name && frame.Line != 0 { + return frame, true + } + } + return CallerFrame{}, false +} + +func bindCallerLocationPC(pc uintptr, frame CallerFrame) { + recordPCLocation(pc, frame.Entry, frame.Function, frame.File, frame.Line) + if pc > 0 { + recordPCLocation(pc-1, frame.Entry, frame.Function, frame.File, frame.Line) + } +} + +func FrameForPC(pc uintptr) (CallerFrame, bool) { + if pc&callerPCMask != 0 { + if frame, ok := syntheticFrameForPC(pc); ok { + return frame, true + } + } + store := callerLocationTLS.Get() + if store == nil || pc == 0 { + return CallerFrame{}, false + } + for i := len(store.frames) - 1; i >= 0; i-- { + frame := store.frames[i] + if frame.PC == pc { + return frame, true + } + } + entry := entryForPC(pc) + if entry == 0 { + return CallerFrame{}, false + } + var best CallerFrame + for _, frame := range store.frames { + if frame.PC == 0 || frame.PC > pc || frame.Entry != entry { + continue + } + if best.PC == 0 || frame.PC > best.PC { + best = frame + } + } + if best.PC != 0 { + best.PC = pc + return best, true + } + for i := len(store.frames) - 1; i >= 0; i-- { + frame := store.frames[i] + if frame.PC == 0 && frame.Entry == entry { + frame.PC = pc + return frame, true + } + } + return CallerFrame{}, false +} + +func syntheticFrameForPC(pc uintptr) (CallerFrame, bool) { + store := callerLocationTLS.Get() + if store == nil { + return CallerFrame{}, false + } + seq := pc >> 2 + if seq == 0 || seq > uintptr(len(store.synthetic)) { + return CallerFrame{}, false + } + frame := store.synthetic[seq-1] + if frame.PC>>2 != seq { + return CallerFrame{}, false + } + frame.PC = pc + if frame.Entry == 0 { + frame.Entry = pc + } + return frame, true +} + +func callerLocationStoreForThread() *callerLocationStore { + store := callerLocationTLS.Get() + if store == nil { + store = new(callerLocationStore) + callerLocationTLS.Set(store) + } + return store +} + +func (s *callerLocationStore) captureFrame(frame CallerFrame, pcValue uintptr) CallerFrame { + idx := s.internSyntheticFrame(frame) + rec := s.synthetic[idx] + seq := uintptr(idx + 1) + rec.PC = (seq << 2) | pcValue + if rec.Entry == 0 { + rec.Entry = rec.PC + } + return rec +} + +// capturePC returns the synthetic PC for a shadow-stack slot, memoizing the +// interned base in the slot so an unchanged frame costs two loads instead of +// a hash probe plus frame comparison. +func (s *callerLocationStore) capturePC(frame *CallerFrame, pcValue uintptr) uintptr { + if frame.captured != 0 { + return frame.captured | pcValue + } + idx := s.internSyntheticFrame(*frame) + base := uintptr(idx+1) << 2 + frame.captured = base + return base | pcValue +} + +// captureFrameAt is capturePC plus the full frame copy Caller needs. +func (s *callerLocationStore) captureFrameAt(frame *CallerFrame, pcValue uintptr) CallerFrame { + pc := s.capturePC(frame, pcValue) + rec := s.synthetic[(pc>>2)-1] + rec.PC = pc + if rec.Entry == 0 { + rec.Entry = rec.PC + } + return rec +} + +// staticPC memoizes the synthetic PC base of a process-static frame (e.g. +// runtime.main) in the per-store cache slot. +func (s *callerLocationStore) staticPC(frame CallerFrame, cache *uintptr, pcValue uintptr) uintptr { + if *cache == 0 { + *cache = uintptr(s.internSyntheticFrame(frame)+1) << 2 + } + return *cache | pcValue +} + +func (s *callerLocationStore) internSyntheticFrame(frame CallerFrame) int { + frame.captured = 0 + if len(s.syntheticHash) == 0 { + s.syntheticHash = make([]uintptr, callerPCHashInit) + } + if len(s.synthetic)*2 >= len(s.syntheticHash) { + s.rehashSyntheticFrames(len(s.syntheticHash) * 2) + } + slot := s.syntheticSlot(frame) + for { + idx := s.syntheticHash[slot] + if idx == 0 { + frame.PC = (uintptr(len(s.synthetic)+1) << 2) | callerPCValue + s.synthetic = append(s.synthetic, frame) + s.syntheticHash[slot] = uintptr(len(s.synthetic)) + return len(s.synthetic) - 1 + } + existing := s.synthetic[idx-1] + if sameSyntheticFrame(existing, frame) { + return int(idx - 1) + } + slot = (slot + 1) & (uintptr(len(s.syntheticHash)) - 1) + } +} + +func (s *callerLocationStore) rehashSyntheticFrames(size int) { + old := s.syntheticHash + s.syntheticHash = make([]uintptr, size) + for _, idx := range old { + if idx == 0 { + continue + } + frame := s.synthetic[idx-1] + slot := s.syntheticSlot(frame) + for s.syntheticHash[slot] != 0 { + slot = (slot + 1) & (uintptr(len(s.syntheticHash)) - 1) + } + s.syntheticHash[slot] = idx + } +} + +func (s *callerLocationStore) syntheticSlot(frame CallerFrame) uintptr { + h := frame.Entry ^ (uintptr(frame.Line) << 12) ^ (uintptr(frame.StartLine) << 24) + h ^= uintptr(len(frame.Function)) << 4 + h ^= uintptr(len(frame.File)) << 8 + return h & (uintptr(len(s.syntheticHash)) - 1) +} + +func sameSyntheticFrame(a, b CallerFrame) bool { + return a.Entry == b.Entry && + a.Function == b.Function && + a.File == b.File && + a.Line == b.Line && + a.StartLine == b.StartLine +} + +func entryForPC(pc uintptr) uintptr { + var info clitedebug.Info + if clitedebug.Addrinfo(unsafe.Pointer(pc), &info) == 0 { + return 0 + } + return uintptr(info.Saddr) +} + +func normalizeRuntimeFuncName(name string) string { + const commandLineArguments = "command-line-arguments." + if hasPrefix(name, commandLineArguments) { + name = "main." + name[len(commandLineArguments):] + } + if len(name) > 0 && name[0] == '_' { + name = name[1:] + } + return normalizeRuntimeAnonFuncName(name) +} + +func normalizeRuntimeAnonFuncName(name string) string { + dollar := lastIndexByte(name, '$') + if dollar < 0 || dollar == len(name)-1 { + return name + } + for i := dollar + 1; i < len(name); i++ { + if name[i] < '0' || name[i] > '9' { + return name + } + } + return name[:dollar] + ".func" + name[dollar+1:] +} + +func hasPrefix(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 lastIndexByte(s string, c byte) int { + for i := len(s) - 1; i >= 0; i-- { + if s[i] == c { + return i + } + } + return -1 +} diff --git a/runtime/internal/runtime/errors.go b/runtime/internal/runtime/errors.go index b92d302761..5b855063cc 100644 --- a/runtime/internal/runtime/errors.go +++ b/runtime/internal/runtime/errors.go @@ -200,6 +200,52 @@ func (e *TypeAssertionError) Error() string { ": missing method " + e.missingMethod } +func TypeAssertError(have, want, iface *Type) any { + if have == nil { + return &TypeAssertionError{iface, nil, want, ""} + } + missingMethod := "" + if want.Kind() == abi.Interface { + missingMethod = typeAssertMissingMethod((*interfacetype)(unsafe.Pointer(want)), have) + } + return &TypeAssertionError{iface, have, want, missingMethod} +} + +func typeAssertMissingMethod(inter *interfacetype, typ *_type) string { + if len(inter.Methods) == 0 { + return "" + } + if typ.Kind() == abi.Interface { + v := (*interfacetype)(unsafe.Pointer(typ)) + for _, tm := range inter.Methods { + if !ifaceHasMethod(v.Methods, tm) { + return tm.Name() + } + } + return "" + } + u := typ.Uncommon() + if u == nil { + return inter.Methods[0].Name() + } + methods := u.Methods() + for _, tm := range inter.Methods { + if _, ok := findMethod(methods, tm); !ok { + return tm.Name() + } + } + return "" +} + +func ifaceHasMethod(methods []abi.Imethod, target abi.Imethod) bool { + for _, method := range methods { + if method.Name_ == target.Name_ && method.Typ_ == target.Typ_ { + return true + } + } + return false +} + func pkgpath(t *_type) string { if u := t.Uncommon(); u != nil { return u.PkgPath_ diff --git a/runtime/internal/runtime/z_default.go b/runtime/internal/runtime/z_default.go index cffe6cbf01..a7e91f612f 100644 --- a/runtime/internal/runtime/z_default.go +++ b/runtime/internal/runtime/z_default.go @@ -16,13 +16,14 @@ var ( // Rethrow rethrows a panic. func Rethrow(link *Defer) { - if ptr := excepKey.Get(); ptr != nil { + if ptr := panicKey.Get(); ptr != nil { + node := (*panicNode)(ptr) if link == nil { - TracePanic(*(*any)(ptr)) + TracePanic(node.arg) debug.PrintStack(2) - c.Free(ptr) c.Exit(2) } else { + node.frame = link.Frame c.Siglongjmp(link.Addr, 1) } } else if ptr := goexitKey.Get(); ptr != nil { diff --git a/runtime/internal/runtime/z_rt.go b/runtime/internal/runtime/z_rt.go index 3b17c951e1..c7d988e972 100644 --- a/runtime/internal/runtime/z_rt.go +++ b/runtime/internal/runtime/z_rt.go @@ -28,38 +28,83 @@ import ( // Defer presents defer statements in a function. type Defer struct { - Addr unsafe.Pointer // sigjmpbuf - Bits uintptr - Link *Defer - Reth unsafe.Pointer // block address after Rethrow - Rund unsafe.Pointer // block address after RunDefers - Args unsafe.Pointer // defer func and args links + Addr unsafe.Pointer // sigjmpbuf + Bits uintptr + Link *Defer + Reth unsafe.Pointer // block address after Rethrow + Rund unsafe.Pointer // block address after RunDefers + Args unsafe.Pointer // defer func and args links + Frame unsafe.Pointer +} + +type panicNode struct { + prev unsafe.Pointer + frame unsafe.Pointer + arg any } // Recover recovers a panic. -func Recover() (ret any) { - ptr := excepKey.Get() +func Recover(frame unsafe.Pointer) (ret any) { + if frame == nil || frame != recoverFrameKey.Get() { + return nil + } + ptr := panicKey.Get() if ptr != nil { - excepKey.Set(nil) - ret = *(*any)(ptr) - c.Free(ptr) + node := (*panicNode)(ptr) + if frame != node.frame { + return nil + } + panicKey.Set(node.prev) + recoverFrameKey.Set(nil) + ret = node.arg + c.Free(unsafe.Pointer(node)) } return } +// StartRecoverFrame enables a direct recover call made by the deferred function +// about to be called from the current frame. +func StartRecoverFrame(frame unsafe.Pointer) unsafe.Pointer { + old := recoverFrameKey.Get() + recoverFrameKey.Set(frame) + return old +} + +// StartRecoverWrapperFrame forwards a direct recover permission through a +// compiler-generated wrapper only when the wrapper is itself the deferred call. +func StartRecoverWrapperFrame(caller, frame unsafe.Pointer) unsafe.Pointer { + old := recoverFrameKey.Get() + if old == caller { + recoverFrameKey.Set(frame) + } + return old +} + +// EndRecoverFrame restores the direct recover frame after a deferred call +// returns normally. +func EndRecoverFrame(frame unsafe.Pointer) { + recoverFrameKey.Set(frame) +} + // Panic panics with a value. func Panic(v any) { - ptr := c.Malloc(unsafe.Sizeof(v)) - *(*any)(ptr) = v - excepKey.Set(ptr) + SavePanicCallerFrames() + ptr := (*panicNode)(c.Malloc(unsafe.Sizeof(panicNode{}))) + ptr.prev = panicKey.Get() + if d := (*Defer)(c.GoDeferData()); d != nil { + ptr.frame = d.Frame + } + ptr.arg = v + panicKey.Set(unsafe.Pointer(ptr)) Rethrow((*Defer)(c.GoDeferData())) } var ( - excepKey pthread.Key - goexitKey pthread.Key - mainThread pthread.Thread + panicKey pthread.Key + recoverFrameKey pthread.Key + goexitKey pthread.Key + mainThread pthread.Thread ) func Goexit() { @@ -68,7 +113,8 @@ func Goexit() { } func init() { - excepKey.Create(nil) + panicKey.Create(nil) + recoverFrameKey.Create(nil) goexitKey.Create(nil) mainThread = pthread.Self() } diff --git a/runtime/internal/runtime/z_signal.go b/runtime/internal/runtime/z_signal.go index 1283dff626..e0e98bce9a 100644 --- a/runtime/internal/runtime/z_signal.go +++ b/runtime/internal/runtime/z_signal.go @@ -38,11 +38,15 @@ const ( // For wasm platform compatibility, signal handling is excluded via build tags. // See PR #1059 for wasm platform requirements. func init() { - signal.Signal(SIGSEGV, func(v c.Int) { - if v == SIGSEGV { + handleFault := func(v c.Int) { + if v == SIGSEGV || v == SIGBUS { panic(errorString("invalid memory address or nil pointer dereference")) } var buf [20]byte panic(errorString("unexpected signal value: " + string(itoa(buf[:], uint64(v))))) - }) + } + signal.Signal(SIGSEGV, handleFault) + if SIGBUS != 0 { + signal.Signal(SIGBUS, handleFault) + } } diff --git a/runtime/internal/runtime/z_signal_darwin.go b/runtime/internal/runtime/z_signal_darwin.go new file mode 100644 index 0000000000..d34ee86607 --- /dev/null +++ b/runtime/internal/runtime/z_signal_darwin.go @@ -0,0 +1,7 @@ +//go:build darwin && !wasm && !baremetal + +package runtime + +import c "github.com/goplus/llgo/runtime/internal/clite" + +const SIGBUS = c.Int(0xa) diff --git a/runtime/internal/runtime/z_signal_linux.go b/runtime/internal/runtime/z_signal_linux.go new file mode 100644 index 0000000000..ff74f64db4 --- /dev/null +++ b/runtime/internal/runtime/z_signal_linux.go @@ -0,0 +1,7 @@ +//go:build linux && !wasm && !baremetal + +package runtime + +import c "github.com/goplus/llgo/runtime/internal/clite" + +const SIGBUS = c.Int(0x7) diff --git a/runtime/internal/runtime/z_signal_other.go b/runtime/internal/runtime/z_signal_other.go new file mode 100644 index 0000000000..5be18c7bb6 --- /dev/null +++ b/runtime/internal/runtime/z_signal_other.go @@ -0,0 +1,7 @@ +//go:build !darwin && !linux && !wasm && !baremetal + +package runtime + +import c "github.com/goplus/llgo/runtime/internal/clite" + +const SIGBUS = c.Int(0) 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/eh.go b/ssa/eh.go index b8ead4eb64..c7c2b5cdf3 100644 --- a/ssa/eh.go +++ b/ssa/eh.go @@ -69,6 +69,18 @@ func (p Program) tyStackrestore() *types.Signature { return p.stackRestoreTy } +// func(level int32) unsafe.Pointer +func (p Program) tyFrameaddress() *types.Signature { + if p.frameAddressTy == nil { + paramLevel := types.NewParam(token.NoPos, nil, "", types.Typ[types.Int32]) + paramPtr := types.NewParam(token.NoPos, nil, "", p.VoidPtr().raw.Type) + params := types.NewTuple(paramLevel) + results := types.NewTuple(paramPtr) + p.frameAddressTy = types.NewSignatureType(nil, nil, nil, params, results, false) + } + return p.frameAddressTy +} + func (b Builder) AllocaSigjmpBuf() Expr { prog := b.Prog sigjmpBufTy := prog.rtType("SigjmpBuf") // Get type from runtime (target architecture) @@ -90,6 +102,12 @@ func (b Builder) StackRestore(sp Expr) { b.impl.CreateIntrinsic(b.Prog.Void().ll, llvm.LookupIntrinsicID("llvm.stackrestore"), []llvm.Value{sp.impl}, "") } +// declare ptr @llvm.frameaddress.p0(i32 immarg) +func (b Builder) FrameAddress(level uint64) Expr { + fn := b.Pkg.cFunc("llvm.frameaddress.p0", b.Prog.tyFrameaddress()) + return b.InlineCall(fn, b.Prog.IntVal(level, b.Prog.Int32())) +} + // addReturnsTwiceAttr adds the returns_twice attribute to a function. // This attribute tells LLVM that the function returns twice (once directly, once via longjmp), // ensuring that variables used across setjmp/longjmp boundaries are placed in @@ -179,12 +197,14 @@ const ( // 3: reth voidptr: block address after Rethrow // 4: rund voidptr: block address after RunDefers // 5: func and args links + // 6: stack frame that owns this defer chain deferSigjmpbuf = iota deferBits deferLink deferRethrow deferRunDefers deferArgs + deferFrame ) func (b Builder) getDefer(kind DoAction) *aDefer { @@ -265,9 +285,10 @@ func (b Builder) initDeferState(procBlk, rethrowBlk BasicBlock) (*aDefer, Expr, self := b.Func prog := b.Prog zero := prog.Val(uintptr(0)) + nilPtr := prog.Nil(prog.VoidPtr()) link := b.Call(b.Pkg.rtFunc("GetThreadDefer")) jb := b.AllocaSigjmpBuf() - ptr := b.aggregateAllocU(prog.Defer(), jb.impl, zero.impl, link.impl, procBlk.Addr().impl) + ptr := b.aggregateAllocU(prog.Defer(), jb.impl, zero.impl, link.impl, procBlk.Addr().impl, nilPtr.impl, nilPtr.impl, b.FrameAddress(0).impl) deferData := Expr{ptr, prog.DeferPtr()} b.Call(b.Pkg.rtFunc("SetThreadDefer"), deferData) bitsPtr := b.FieldAddr(deferData, deferBits) @@ -514,7 +535,9 @@ func (b Builder) saveDeferArgsTo(argsPtr Expr, kind DoAction, id Expr, fn Expr, func (b Builder) callDefer(self *aDefer, typ Type, buildCall func(Builder, Expr, ...Expr) Expr, fn Expr, args []Expr) { if typ == nil { - buildCall(b, fn, args...) + b.callRecoverScopedDefer(fn, func() { + buildCall(b, fn, args...) + }) return } prog := b.Prog @@ -537,10 +560,61 @@ func (b Builder) callDefer(self *aDefer, typ Type, buildCall func(Builder, Expr, args[i] = b.getField(data, i+offset) } b.Call(b.Pkg.rtFunc("FreeDeferNode"), ptr) - buildCall(b, fn, args...) + b.callRecoverScopedDefer(fn, func() { + buildCall(b, fn, args...) + }) }) } +func (b Builder) callRecoverScopedDefer(fn Expr, call func()) { + if isRecoverBuiltin(fn) { + call() + return + } + prev := b.Call(b.Pkg.rtFunc("StartRecoverFrame"), b.FrameAddress(0)) + call() + if b.currentBlockEndsUnreachable() { + return + } + b.Call(b.Pkg.rtFunc("EndRecoverFrame"), prev) +} + +func (b Builder) MaskRecoverCall(fn Expr, buildCall func(Builder, Expr, ...Expr) Expr, args ...Expr) Expr { + prev := b.Call(b.Pkg.rtFunc("StartRecoverFrame"), b.Prog.Nil(b.Prog.VoidPtr())) + ret := buildCall(b, fn, args...) + if b.currentBlockEndsUnreachable() { + return ret + } + b.Call(b.Pkg.rtFunc("EndRecoverFrame"), prev) + return ret +} + +func (b Builder) ForwardRecoverFrameCall(fn Expr, buildCall func(Builder, Expr, ...Expr) Expr, args ...Expr) Expr { + prev := b.Call(b.Pkg.rtFunc("StartRecoverWrapperFrame"), b.FrameAddress(1), b.FrameAddress(0)) + ret := buildCall(b, fn, args...) + if b.currentBlockEndsUnreachable() { + return ret + } + b.Call(b.Pkg.rtFunc("EndRecoverFrame"), prev) + return ret +} + +func (b Builder) currentBlockEndsUnreachable() bool { + lastInst := b.impl.GetInsertBlock().LastInstruction() + return !lastInst.IsNil() && !lastInst.IsAUnreachableInst().IsNil() +} + +func isRecoverBuiltin(fn Expr) bool { + if fn.IsNil() { + return false + } + if fn.kind != vkBuiltin { + return false + } + bi, ok := fn.raw.Type.(*builtinTy) + return ok && bi.name == "recover" +} + // RunDefers emits instructions to run deferred instructions. func (b Builder) RunDefers() { self := b.getDefer(DeferInCond) @@ -610,8 +684,7 @@ func (b Builder) Unreachable() { // Recover emits a recover instruction. func (b Builder) Recover() Expr { dbgInstrln("Recover") - // TODO(xsw): recover can't be a function call in Go - return b.Call(b.Pkg.rtFunc("Recover")) + return b.Call(b.Pkg.rtFunc("Recover"), b.FrameAddress(1)) } // Panic emits a panic instruction. diff --git a/ssa/eh_defer_test.go b/ssa/eh_defer_test.go index 5f99729b1e..774aa6b908 100644 --- a/ssa/eh_defer_test.go +++ b/ssa/eh_defer_test.go @@ -132,6 +132,59 @@ func TestPlainDeferWithoutSavedArgsIR(t *testing.T) { } } +func TestDeferredRecoverBuiltinDoesNotStartNestedFrameIR(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + pkg := prog.NewPackage("foo", "foo") + + fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) + b := fn.MakeBody(1) + fn.SetRecover(fn.MakeBlock()) + b.Defer(ssa.DeferAlways, ssa.Builtin("recover"), ssa.Builder.Call) + b.RunDefers() + b.Return() + b.EndBuild() + + ir := pkg.Module().String() + if strings.Contains(ir, "StartRecoverFrame") || strings.Contains(ir, "EndRecoverFrame") { + t.Fatalf("direct deferred recover should not be wrapped in a nested recover frame, got:\n%s", ir) + } + if !strings.Contains(ir, "runtime/internal/runtime.Recover") { + t.Fatalf("expected deferred recover call in IR, got:\n%s", ir) + } +} + +func TestRecoverFrameCallHelpersIR(t *testing.T) { + prog := ssatest.NewProgram(t, nil) + pkg := prog.NewPackage("foo", "foo") + + callee := pkg.NewFunc("callee", ssa.NoArgsNoRet, ssa.InGo) + cb := callee.MakeBody(1) + cb.Return() + cb.EndBuild() + + fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) + b := fn.MakeBody(1) + b.MaskRecoverCall(callee.Expr, ssa.Builder.Call) + b.ForwardRecoverFrameCall(callee.Expr, ssa.Builder.Call) + b.Return() + b.EndBuild() + + ir := pkg.Module().String() + for _, want := range []string{ + "runtime/internal/runtime.StartRecoverFrame", + "runtime/internal/runtime.StartRecoverWrapperFrame", + "runtime/internal/runtime.EndRecoverFrame", + "llvm.frameaddress.p0", + } { + if !strings.Contains(ir, want) { + t.Fatalf("missing %s in recover-frame helper IR:\n%s", want, ir) + } + } + if got := strings.Count(ir, "runtime/internal/runtime.EndRecoverFrame"); got < 2 { + t.Fatalf("EndRecoverFrame calls = %d, want at least 2 in IR:\n%s", got, ir) + } +} + func TestConditionalDeferIR(t *testing.T) { prog := ssatest.NewProgram(t, nil) pkg := prog.NewPackage("foo", "foo") diff --git a/ssa/expr.go b/ssa/expr.go index eafadf2767..635666fe4a 100644 --- a/ssa/expr.go +++ b/ssa/expr.go @@ -59,6 +59,12 @@ func (v Expr) SetOrdering(ordering AtomicOrdering) Expr { return v } +// SetVolatile marks a load or store as volatile. +func (v Expr) SetVolatile(volatile bool) Expr { + v.impl.SetVolatile(volatile) + return v +} + func (v Expr) SetName(alias string) Expr { v.impl.SetName(alias) return v diff --git a/ssa/funcinfo.go b/ssa/funcinfo.go new file mode 100644 index 0000000000..7e3979a9be --- /dev/null +++ b/ssa/funcinfo.go @@ -0,0 +1,107 @@ +/* + * 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" + PCLineMetadataName = "llgo.pcline" + funcInfoVersion = 1 + pcLineVersion = 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 +} + +// EnableFuncInfoSites controls emission of the per-function site records +// (entry/stub/pc-line inline-asm fragments inside function bodies). They are +// gated separately from the funcinfo metadata tables because the +// body-embedded anchors shift instruction/scope layout enough to confuse +// debuggers; debug builds keep the tables (FuncForPC name/FileLine fidelity +// via the dlsym path) but drop the sites. +func (p Program) EnableFuncInfoSites(enable bool) { + p.enableFuncInfoSites = enable +} + +func (p Program) FuncInfoSitesEnabled() bool { + return p.enableFuncInfoSites +} + +// 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(), + }), + ) +} + +// EmitPCLineInfo records a PC label id and its source position. The id names a +// zero-byte label emitted in the function body; keeping the metadata string-only +// lets dead functions be removed without the line table holding address +// references to them. +func (p Package) EmitPCLineInfo(id uint64, symbol, file string, line, column int) { + if id == 0 || symbol == "" { + return + } + if line < 0 { + line = 0 + } + if column < 0 { + column = 0 + } + i32 := p.Prog.Int32().ll + i64 := p.Prog.Int64().ll + p.mod.AddNamedMetadataOperand(PCLineMetadataName, + p.Prog.ctx.MDNode([]llvm.Metadata{ + llvm.ConstInt(i32, pcLineVersion, false).ConstantAsMetadata(), + llvm.ConstInt(i64, id, false).ConstantAsMetadata(), + p.Prog.ctx.MDString(symbol), + p.Prog.ctx.MDString(file), + llvm.ConstInt(i32, uint64(line), false).ConstantAsMetadata(), + llvm.ConstInt(i32, uint64(column), false).ConstantAsMetadata(), + }), + ) +} diff --git a/ssa/interface.go b/ssa/interface.go index 2539c3187e..9c951b0515 100644 --- a/ssa/interface.go +++ b/ssa/interface.go @@ -331,8 +331,7 @@ func (b Builder) TypeAssert(x Expr, assertedTyp Type, commaOk bool) Expr { blks := b.Func.MakeBlocks(2) b.If(eq, blks[0], blks[1]) b.SetBlockEx(blks[1], AtEnd, false) - b.Call(b.Pkg.rtFunc("PanicTypeAssert"), tx, b.Str(assertedTyp.RawType().String()), b.Str(typeAssertMissingMethod(assertedTyp))) - b.Unreachable() + b.Panic(b.InlineCall(b.Pkg.rtFunc("TypeAssertError"), tx, tabi, b.abiType(x.RawType()))) b.SetBlockEx(blks[0], AtEnd, false) b.blk.last = blks[0].last return val() diff --git a/ssa/package.go b/ssa/package.go index 74256eddcc..1fd4f1285f 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -211,6 +211,7 @@ type aProgram struct { memsetInlineTy *types.Signature stackSaveTy *types.Signature stackRestoreTy *types.Signature + frameAddressTy *types.Signature createKeyTy *types.Signature getSpecTy *types.Signature @@ -233,6 +234,9 @@ type aProgram struct { is32Bits bool enableGoGlobalDCE bool + + enableFuncInfoMetadata bool + enableFuncInfoSites bool } type AbiSymbol struct { diff --git a/ssa/ssa_test.go b/ssa/ssa_test.go index 74d4242286..38160c8696 100644 --- a/ssa/ssa_test.go +++ b/ssa/ssa_test.go @@ -200,6 +200,150 @@ func TestNewFuncExLLVMUsed(t *testing.T) { } } +func TestFuncInfoMetadataDoesNotPreserveFunctions(t *testing.T) { + testFuncInfoMetadataDoesNotPreserveFunctions(t) +} + +func TestPCLineMetadataEmission(t *testing.T) { + testPCLineMetadataEmission(t) +} + +func testPCLineMetadataEmission(t *testing.T) { + t.Helper() + + prog := NewProgram(nil) + pkg := prog.NewPackage("main", "main") + + pkg.EmitPCLineInfo(0, "ignored", "ignored.go", -1, -1) + pkg.EmitPCLineInfo(0x1234, "", "ignored.go", -1, -1) + if ir := pkg.String(); strings.Contains(ir, PCLineMetadataName) { + t.Fatalf("invalid pcline rows should not emit metadata:\n%s", ir) + } + + pkg.EmitPCLineInfo(0x1234, "main.live", "call.go", 23, 5) + pkg.EmitPCLineInfo(0x5678, "main.negative", "negative.go", -7, -1) + ir := pkg.String() + for _, want := range []string{ + `!llgo.pcline = !{!`, + `i64 4660`, + `!"main.live"`, + `!"call.go"`, + `i32 23`, + `i32 5`, + `i64 22136`, + `!"main.negative"`, + `!"negative.go"`, + `i32 0`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("missing pcline field %s:\n%s", want, ir) + } + } + if strings.Contains(ir, `ptr @main.live`) || strings.Contains(ir, `ptr @"main.live"`) { + t.Fatalf("pcline metadata must not contain function pointer operands:\n%s", ir) + } +} + +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) + prog.EnableFuncInfoSites(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 TestDevLTOGlobalDCEPCLineMetadata(t *testing.T) { + requireGoGlobalDCE(t) + testPCLineMetadataEmission(t) +} + func requireGoGlobalDCE(t *testing.T) { t.Helper() } diff --git a/test/go/recover_defer_test.go b/test/go/recover_defer_test.go new file mode 100644 index 0000000000..aa73f70e29 --- /dev/null +++ b/test/go/recover_defer_test.go @@ -0,0 +1,286 @@ +package gotest + +import ( + "reflect" + "runtime" + "testing" +) + +func recoverIndirect() any { + return recover() +} + +func recoverRecursive(n int) any { + if n == 0 { + return recoverRecursive(1) + } + return recover() +} + +func TestRecoverOnlyDirectDeferredCall(t *testing.T) { + var indirect, direct, second any + func() { + defer func() { + indirect = recoverIndirect() + direct = recover() + second = recover() + }() + panic("direct-sentinel") + }() + + if indirect != nil { + t.Fatalf("indirect recover = %v, want nil", indirect) + } + if direct != "direct-sentinel" { + t.Fatalf("direct recover = %v, want direct-sentinel", direct) + } + if second != nil { + t.Fatalf("second recover = %v, want nil", second) + } +} + +func TestRecoverRejectsRecursiveIndirectCall(t *testing.T) { + var indirect, direct any + func() { + defer func() { + indirect = recoverRecursive(0) + direct = recover() + }() + panic("recursive-sentinel") + }() + + if indirect != nil { + t.Fatalf("recursive indirect recover = %v, want nil", indirect) + } + if direct != "recursive-sentinel" { + t.Fatalf("direct recover = %v, want recursive-sentinel", direct) + } +} + +func TestNestedPanicRecoverStack(t *testing.T) { + var recovered []any + func() { + defer func() { + recovered = append(recovered, recover()) + }() + defer func() { + defer func() { + recovered = append(recovered, recover()) + }() + panic("inner") + }() + panic("outer") + }() + + want := []any{"inner", "outer"} + if !reflect.DeepEqual(recovered, want) { + t.Fatalf("recover stack = %v, want %v", recovered, want) + } +} + +func TestDeferredRecoverBuiltinKeepsNestedPanicForNextDefer(t *testing.T) { + var recovered []any + func() { + defer func() { + recovered = append(recovered, recover()) + }() + defer func() { + defer func() { + recovered = append(recovered, recover()) + }() + defer recover() + panic("inner") + }() + panic("outer") + }() + + want := []any{"inner", "outer"} + if !reflect.DeepEqual(recovered, want) { + t.Fatalf("recover stack after deferred recover builtin = %v, want %v", recovered, want) + } +} + +func TestDeferredRecoverBuiltinCanRecoverOuterPanicAfterNestedRecover(t *testing.T) { + var recovered []any + func() { + defer func() { + recovered = append(recovered, recover()) + }() + defer func() { + defer recover() + defer func() { + recovered = append(recovered, recover()) + }() + panic("inner") + }() + panic("outer") + }() + + want := []any{"inner", nil} + if !reflect.DeepEqual(recovered, want) { + t.Fatalf("recover stack after outer deferred recover builtin = %v, want %v", recovered, want) + } +} + +func TestRecoverAfterPanicDoesNotKeepPartialResultWrites(t *testing.T) { + if got := recoverAfterResultAssignmentPanic(); got { + t.Fatalf("assignment result = %v, want false", got) + } + if got, _ := recoverAfterReturnExpressionPanic(); got { + t.Fatalf("return expression result = %v, want false", got) + } + if got, _ := recoverAfterNamedReturnExpressionPanic(); got { + t.Fatalf("named return expression result = %v, want false", got) + } +} + +func recoverAfterResultAssignmentPanic() (bad bool) { + defer func() { + recover() + }() + var p *int + bad, _ = true, *p + return +} + +func recoverAfterReturnExpressionPanic() (bool, int) { + defer func() { + recover() + }() + var p *int + return true, *p +} + +func recoverAfterNamedReturnExpressionPanic() (_ bool, _ int) { + defer func() { + recover() + }() + var p *int + return true, *p +} + +type recoverValueMethod uintptr + +var methodWrapperRecovered any + +func (recoverValueMethod) recoverViaValueMethod() { + methodWrapperRecovered = recover() +} + +func TestRecoverThroughDeferredPointerToValueMethodWrapper(t *testing.T) { + methodWrapperRecovered = nil + var x recoverValueMethod + func() { + defer (*recoverValueMethod).recoverViaValueMethod(&x) + panic("method-wrapper-sentinel") + }() + + if methodWrapperRecovered != "method-wrapper-sentinel" { + t.Fatalf("method wrapper recover = %v, want method-wrapper-sentinel", methodWrapperRecovered) + } +} + +func TestRecoverThroughMethodWrapperStillRequiresDirectDeferredCall(t *testing.T) { + methodWrapperRecovered = "unset" + var direct any + var x recoverValueMethod + func() { + defer func() { + (*recoverValueMethod).recoverViaValueMethod(&x) + direct = recover() + }() + panic("outer-sentinel") + }() + + if methodWrapperRecovered != nil { + t.Fatalf("nested method wrapper recover = %v, want nil", methodWrapperRecovered) + } + if direct != "outer-sentinel" { + t.Fatalf("direct recover after nested method wrapper = %v, want outer-sentinel", direct) + } +} + +type embeddedRecoverTarget int + +// Keep issue73917/issue73920 as a real helper call outside the wrapper target. +// +//go:noinline +func recoverForEmbeddedWrapper() { + if r := recover(); r != nil { + methodWrapperRecovered = r + } +} + +func (*embeddedRecoverTarget) recoverViaIndirectCall() { + recoverForEmbeddedWrapper() +} + +type embeddedValueWrapper struct{ *embeddedRecoverTarget } +type embeddedPointerWrapper struct{ *embeddedRecoverTarget } + +func requireGo126RecoverWrapperSemantics(t *testing.T) { + t.Helper() + + const prefix = "go1." + version := runtime.Version() + if len(version) <= len(prefix) || version[:len(prefix)] != prefix { + return + } + + minor := 0 + for _, c := range version[len(prefix):] { + if c < '0' || c > '9' { + break + } + minor = minor*10 + int(c-'0') + } + if minor != 0 && minor < 26 { + t.Skipf("%s has pre-Go 1.26 embedded wrapper recover semantics", version) + } +} + +func TestDeferredEmbeddedValueMethodWrapperKeepsIndirectRecoverNil(t *testing.T) { + requireGo126RecoverWrapperSemantics(t) + + methodWrapperRecovered = nil + var direct any + x := embeddedValueWrapper{new(embeddedRecoverTarget)} + fn := embeddedValueWrapper.recoverViaIndirectCall + func() { + defer func() { + direct = recover() + }() + defer fn(x) + panic("embedded-value-wrapper-sentinel") + }() + + if methodWrapperRecovered != nil { + t.Fatalf("indirect recover through embedded value wrapper = %v, want nil", methodWrapperRecovered) + } + if direct != "embedded-value-wrapper-sentinel" { + t.Fatalf("direct recover after embedded value wrapper = %v, want embedded-value-wrapper-sentinel", direct) + } +} + +func TestDeferredEmbeddedPointerMethodWrapperKeepsIndirectRecoverNil(t *testing.T) { + requireGo126RecoverWrapperSemantics(t) + + methodWrapperRecovered = nil + var direct any + x := &embeddedPointerWrapper{new(embeddedRecoverTarget)} + fn := (*embeddedPointerWrapper).recoverViaIndirectCall + func() { + defer func() { + direct = recover() + }() + defer fn(x) + panic("embedded-pointer-wrapper-sentinel") + }() + + if methodWrapperRecovered != nil { + t.Fatalf("indirect recover through embedded pointer wrapper = %v, want nil", methodWrapperRecovered) + } + if direct != "embedded-pointer-wrapper-sentinel" { + t.Fatalf("direct recover after embedded pointer wrapper = %v, want embedded-pointer-wrapper-sentinel", direct) + } +} diff --git a/test/go/recover_fault_unix_test.go b/test/go/recover_fault_unix_test.go new file mode 100644 index 0000000000..96ecaa2a91 --- /dev/null +++ b/test/go/recover_fault_unix_test.go @@ -0,0 +1,49 @@ +//go:build linux || darwin + +package gotest + +import ( + "runtime/debug" + "syscall" + "testing" +) + +func faultCopy(dst, src []byte) (n int, err error) { + defer func() { + if r, ok := recover().(error); ok { + err = r + } + }() + + for i := 0; i < len(dst) && i < len(src); i++ { + dst[i] = src[i] + n++ + } + return +} + +func TestRecoverAfterFaultPreservesNamedResult(t *testing.T) { + old := debug.SetPanicOnFault(true) + defer debug.SetPanicOnFault(old) + + size := syscall.Getpagesize() + data, err := syscall.Mmap(-1, 0, 16*size, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_ANON|syscall.MAP_PRIVATE) + if err != nil { + t.Fatalf("mmap: %v", err) + } + defer syscall.Munmap(data) + + hole := data[len(data)/2 : 3*(len(data)/4)] + if err := syscall.Mprotect(hole, syscall.PROT_NONE); err != nil { + t.Fatalf("mprotect: %v", err) + } + + const offset = 5 + n, err := faultCopy(data[offset:], make([]byte, len(data))) + if err == nil { + t.Fatal("no error from copy across memory hole") + } + if want := len(data)/2 - offset; n != want { + t.Fatalf("copy returned %d, want %d", n, want) + } +} diff --git a/test/go/runtime_error_recover_test.go b/test/go/runtime_error_recover_test.go index 546deee367..51985b844a 100644 --- a/test/go/runtime_error_recover_test.go +++ b/test/go/runtime_error_recover_test.go @@ -6,116 +6,157 @@ import ( "testing" ) +type runtimeErrorMissingMethod interface { + runtimeErrorMissingMethod() +} + var ( + runtimeErrorSink any runtimeErrorIntSink int - runtimeErrorAnySink any runtimeErrorArrayPtr *[10]int runtimeErrorBigArrayPtr *[10000]int ) -func TestRecoverRuntimeErrorClassification(t *testing.T) { - var zero int - var zero64 int64 +func TestRecoveredRuntimePanicsAreErrors(t *testing.T) { var index = 99999 arrayPtr := new([10]int) - var slice []int - var iface any = 1 tests := []struct { name string - want string + want []string f func() }{ { - name: "int-div-zero", - want: "integer divide by zero", + name: "index", + want: []string{"runtime error:", "index out of range"}, f: func() { - runtimeErrorIntSink = 1 / zero + s := []byte{1} + i := 2 + runtimeErrorSink = s[i] }, }, { - name: "int64-div-zero", - want: "integer divide by zero", + name: "array bounds", + want: []string{"runtime error:", "index out of range"}, f: func() { - runtimeErrorIntSink = int(1 / zero64) + runtimeErrorIntSink = arrayPtr[index] }, }, { - name: "nil-array-pointer-index-zero", - want: "nil pointer dereference", + name: "slice", + want: []string{"runtime error:", "slice bounds out of range"}, f: func() { - runtimeErrorIntSink = runtimeErrorArrayPtr[0] + s := []byte{1} + hi := 2 + runtimeErrorSink = s[:hi] }, }, { - name: "nil-array-pointer-index-one", - want: "nil pointer dereference", + name: "divide", + want: []string{"runtime error:", "integer divide by zero"}, f: func() { - runtimeErrorIntSink = runtimeErrorArrayPtr[1] + z := 0 + runtimeErrorSink = 1 / z }, }, { - name: "nil-array-pointer-index-large", - want: "nil pointer dereference", + name: "nil dereference", + want: []string{"runtime error:", "nil pointer dereference"}, f: func() { - runtimeErrorIntSink = runtimeErrorBigArrayPtr[5000] + var p *int + runtimeErrorSink = *p }, }, { - name: "array-bounds", - want: "index out of range", + name: "nil array pointer index zero", + want: []string{"runtime error:", "nil pointer dereference"}, f: func() { - runtimeErrorIntSink = arrayPtr[index] + runtimeErrorIntSink = runtimeErrorArrayPtr[0] }, }, { - name: "slice-bounds", - want: "index out of range", + name: "nil array pointer index one", + want: []string{"runtime error:", "nil pointer dereference"}, f: func() { - runtimeErrorIntSink = slice[index] + runtimeErrorIntSink = runtimeErrorArrayPtr[1] }, }, { - name: "type-concrete", - want: "int, not string", + name: "nil array pointer index large", + want: []string{"runtime error:", "nil pointer dereference"}, f: func() { - runtimeErrorAnySink = iface.(string) + runtimeErrorIntSink = runtimeErrorBigArrayPtr[5000] }, }, { - name: "type-interface", - want: "missing method runtimeErrorMissingMethod", + name: "slice to array", + want: []string{"runtime error:", "cannot convert slice with length 1 to array or pointer to array with length 2"}, f: func() { - runtimeErrorAnySink = iface.(runtimeErrorMissingMethod) + s := []byte{1} + runtimeErrorSink = [2]byte(s) }, }, } - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - expectRecoverRuntimeError(t, tt.want, tt.f) + err := recoverRuntimeErrorValue(t, tt.f) + assertRuntimeErrorContains(t, err, tt.want...) }) } } -func expectRecoverRuntimeError(t *testing.T, want string, f func()) { +func TestRecoveredTypeAssertionPanicsAreRuntimeErrors(t *testing.T) { + t.Run("concrete", func(t *testing.T) { + var v any = 1 + err := recoverRuntimeErrorValue(t, func() { + runtimeErrorSink = v.(string) + }) + assertRuntimeErrorContains(t, err, "interface conversion", "int", "not string") + }) + + t.Run("nil interface", func(t *testing.T) { + var v any + err := recoverRuntimeErrorValue(t, func() { + runtimeErrorSink = v.(string) + }) + assertRuntimeErrorContains(t, err, "interface conversion", "is nil", "not string") + }) + + t.Run("missing method", func(t *testing.T) { + var v any = 1 + err := recoverRuntimeErrorValue(t, func() { + runtimeErrorSink = v.(runtimeErrorMissingMethod) + }) + assertRuntimeErrorContains(t, err, "interface conversion", "int is not", "missing method runtimeErrorMissingMethod") + }) +} + +func recoverRuntimeErrorValue(t *testing.T, f func()) runtime.Error { t.Helper() - defer func() { - err := recover() - if err == nil { - t.Fatalf("expected runtime panic containing %q", want) - } - runtimeErr, ok := err.(runtime.Error) - if !ok { - t.Fatalf("panic type = %T, want runtime.Error", err) - } - if got := runtimeErr.Error(); !strings.Contains(got, want) { - t.Fatalf("panic = %q, want contains %q", got, want) - } + var rec any + func() { + defer func() { + rec = recover() + }() + f() }() - f() + if rec == nil { + t.Fatal("expected panic") + } + err := rec.(error) + rerr := rec.(runtime.Error) + if err.Error() != rerr.Error() { + t.Fatalf("error text mismatch: error=%q runtime.Error=%q", err.Error(), rerr.Error()) + } + return rerr } -type runtimeErrorMissingMethod interface { - runtimeErrorMissingMethod() +func assertRuntimeErrorContains(t *testing.T, err error, wants ...string) { + t.Helper() + got := err.Error() + for _, want := range wants { + if !strings.Contains(got, want) { + t.Fatalf("panic = %q, want contains %q", got, want) + } + } } diff --git a/test/go/runtime_lineinfo_stack_test.go b/test/go/runtime_lineinfo_stack_test.go new file mode 100644 index 0000000000..52fffcabef --- /dev/null +++ b/test/go/runtime_lineinfo_stack_test.go @@ -0,0 +1,320 @@ +/* + * 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 ( + "reflect" + "strconv" + "runtime" + "runtime/debug" + "strings" + _ "unsafe" +) + +func main() { + checkCaller() + checkCallerSkip() + checkFrames() // FRAMES_MAIN_MARK + checkFuncForPC() + checkFuncForPCFunctionValue() + 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() // CALLER_SKIP_MARK +} + +//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_CHECK_MARK + frames := runtime.CallersFrames(pcs[:n]) + seenCheckFrames := false + seenMain := false + for { + frame, more := frames.Next() + if frame.Function == "main.checkFrames" { + if !strings.HasSuffix(frame.File, "main.go") || frame.Line != FRAMES_CHECK_LINE { + panic("bad checkFrames frame: " + frame.File + ":" + strconv.Itoa(frame.Line)) + } + seenCheckFrames = true + } + if frame.Function == "main.main" { + if !strings.HasSuffix(frame.File, "main.go") || frame.Line != FRAMES_MAIN_LINE { + panic("bad main frame: " + frame.File + ":" + strconv.Itoa(frame.Line)) + } + seenMain = true + } + if seenCheckFrames && seenMain { + 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 entryPCTarget() int { + return 7 // FUNC_ENTRY_TARGET_MARK +} + +//go:noinline +func checkFuncForPCFunctionValue() { + if entryPCTarget() != 7 { + panic("bad target") + } + pc := reflect.ValueOf(entryPCTarget).Pointer() + if pc == 0 { + panic("missing function value pc") + } + fn := runtime.FuncForPC(pc) + if fn == nil { + panic("missing function value func") + } + if name := fn.Name(); name != "main.entryPCTarget" { + panic("bad function value func: " + name) + } + if entry := fn.Entry(); entry == 0 { + panic("missing function value entry") + } + file, line := fn.FileLine(pc) + if !strings.HasSuffix(file, "main.go") || line != FUNC_ENTRY_TARGET_LINE { + panic("bad function value 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()) // DEBUG_STACK_CALL_MARK + 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, "CALLER_MARK"))) + source = strings.ReplaceAll(source, "CALLER_SKIP_LINE", strconv.Itoa(markerLine(source, "CALLER_SKIP_MARK"))) + source = strings.ReplaceAll(source, "FRAMES_MAIN_LINE", strconv.Itoa(markerLine(source, "FRAMES_MAIN_MARK"))) + source = strings.ReplaceAll(source, "FRAMES_CHECK_LINE", strconv.Itoa(markerLine(source, "FRAMES_CHECK_MARK"))) + source = strings.ReplaceAll(source, "FUNC_FILELINE_LINE", strconv.Itoa(markerLine(source, "FUNC_FILELINE_MARK"))) + source = strings.ReplaceAll(source, "FUNC_ENTRY_TARGET_LINE", strconv.Itoa(markerLine(source, "FUNC_ENTRY_TARGET_MARK"))) + source = strings.ReplaceAll(source, "RUNTIME_STACK_LINE", strconv.Itoa(markerLine(source, "RUNTIME_STACK_MARK"))) + source = strings.ReplaceAll(source, "DEBUG_STACK_LINE", strconv.Itoa(markerLine(source, "DEBUG_STACK_CALL_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) + } +} + +const runtimeFuncInfoConcurrentFirstUseProbe = `package main + +import ( + "runtime" + "strconv" + "strings" + "sync" +) + +func main() { + const n = 32 + start := make(chan struct{}) + errc := make(chan string, n) + var wg sync.WaitGroup + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + <-start + errc <- checkRuntimeInfo() + }() + } + close(start) + wg.Wait() + close(errc) + for err := range errc { + if err != "" { + panic(err) + } + } +} + +//go:noinline +func checkRuntimeInfo() string { + pc, file, line, ok := runtime.Caller(0) // CONCURRENT_CALLER_MARK + if !ok || !strings.HasSuffix(file, "main.go") || line != CONCURRENT_CALLER_LINE { + return "bad caller: " + file + ":" + strconv.Itoa(line) + } + fn := runtime.FuncForPC(pc) + if fn == nil || fn.Name() != "main.checkRuntimeInfo" { + name := "" + if fn != nil { + name = fn.Name() + } + return "bad func: " + name + } + file, line = fn.FileLine(pc) + if !strings.HasSuffix(file, "main.go") || line != CONCURRENT_CALLER_LINE { + return "bad fileline: " + file + ":" + strconv.Itoa(line) + } + var pcs [8]uintptr + n := runtime.Callers(0, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function == "main.checkRuntimeInfo" { + if !strings.HasSuffix(frame.File, "main.go") || frame.Line == 0 { + return "bad frame: " + frame.File + ":" + strconv.Itoa(frame.Line) + } + return "" + } + if !more { + return "missing frame" + } + } +} +` + +func TestRuntimeFuncInfoConcurrentFirstUse(t *testing.T) { + source := runtimeFuncInfoConcurrentFirstUseProbe + source = strings.ReplaceAll(source, "CONCURRENT_CALLER_LINE", strconv.Itoa(markerLine(source, "CONCURRENT_CALLER_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 concurrent funcinfo 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/go/runtime_statement_line_test.go b/test/go/runtime_statement_line_test.go new file mode 100644 index 0000000000..81430b49f0 --- /dev/null +++ b/test/go/runtime_statement_line_test.go @@ -0,0 +1,211 @@ +/* + * 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 runtimeStatementLineProbe = `package main + +import ( + "runtime" + "runtime/debug" + "strconv" + "strings" +) + +type Wrapper struct { + a []int +} + +func (w Wrapper) Get(i int) int { + return w.a[i] +} + +func main() { + checkCallerStatement() + checkCallersFramesStatement() + checkInterfaceIndirectCaller() + checkClosureIndirectCaller() + checkAdjacentRuntimeStack() + checkRecoveredDebugStackBounds() +} + +//go:noinline +func checkCallerStatement() { + _, file, line, ok := runtime.Caller(0) // CALLER_STMT_MARK + if !ok || !strings.HasSuffix(file, "main.go") || line != CALLER_STMT_LINE { + panic("bad caller statement: " + file + ":" + strconv.Itoa(line)) + } +} + +//go:noinline +func checkCallersFramesStatement() { + var pcs [16]uintptr + n := runtime.Callers(0, pcs[:]) // CALLERS_STMT_MARK + frames := runtime.CallersFrames(pcs[:n]) + for { + frame, more := frames.Next() + if frame.Function == "main.checkCallersFramesStatement" { + if !strings.HasSuffix(frame.File, "main.go") || frame.Line != CALLERS_STMT_LINE { + panic("bad callers frame: " + frame.File + ":" + strconv.Itoa(frame.Line)) + } + fn := runtime.FuncForPC(frame.PC - 1) + if fn == nil || fn.Name() != "main.checkCallersFramesStatement" { + name := "" + if fn != nil { + name = fn.Name() + } + panic("bad FuncForPC(pc-1): " + name) + } + file, line := fn.FileLine(frame.PC - 1) + if !strings.HasSuffix(file, "main.go") || line != CALLERS_STMT_LINE { + panic("bad Func.FileLine(pc-1): " + file + ":" + strconv.Itoa(line)) + } + return + } + if !more { + break + } + } + panic("missing callers frame") +} + +type indirectCaller interface { + call() +} + +type indirectCallerImpl struct{} + +//go:noinline +func checkInterfaceIndirectCaller() { + var c indirectCaller = indirectCallerImpl{} + c.call() // INTERFACE_CALL_MARK +} + +//go:noinline +func (indirectCallerImpl) call() { + interfaceMiddle() +} + +//go:noinline +func interfaceMiddle() { + checkCallerLine("interface", 2, INTERFACE_CALL_LINE) +} + +//go:noinline +func checkClosureIndirectCaller() { + f := closureLayer(closureLayer(func() { + checkCallerLine("closure", 3, CLOSURE_CALL_LINE) + })) + f() // CLOSURE_CALL_MARK +} + +//go:noinline +func closureLayer(next func()) func() { + return func() { + next() + } +} + +//go:noinline +func checkCallerLine(kind string, skip, want int) { + _, file, line, ok := runtime.Caller(skip) + if !ok || !strings.HasSuffix(file, "main.go") || line != want { + panic("bad " + kind + " indirect caller line: " + file + ":" + strconv.Itoa(line)) + } +} + +//go:noinline +func checkAdjacentRuntimeStack() { + var buf1, buf2 [4096]byte + n1 := runtime.Stack(buf1[:], false) // STACK_ONE_MARK + n2 := runtime.Stack(buf2[:], false) // STACK_TWO_MARK + line1 := stackLineFor(string(buf1[:n1]), "main.checkAdjacentRuntimeStack") + line2 := stackLineFor(string(buf2[:n2]), "main.checkAdjacentRuntimeStack") + if line1 != STACK_ONE_LINE || line2 != STACK_TWO_LINE || line1+1 != line2 { + panic("bad adjacent stack lines: " + strconv.Itoa(line1) + "," + strconv.Itoa(line2)) + } +} + +//go:noinline +func checkRecoveredDebugStackBounds() { + defer func() { + if recover() == nil { + panic("missing bounds panic") + } + stack := string(debug.Stack()) + if !strings.Contains(stack, "main.go:BOUNDS_LINE") { + panic("bad recovered stack: " + stack) + } + }() + foo := Wrapper{a: []int{0, 1, 2}} + _ = foo.Get(3) // BOUNDS_MARK +} + +func stackLineFor(stack, fn string) int { + lines := strings.Split(stack, "\n") + for i := 0; i+1 < len(lines); i++ { + if strings.TrimSpace(lines[i]) == fn+"()" { + loc := strings.TrimSpace(lines[i+1]) + colon := strings.LastIndexByte(loc, ':') + if colon < 0 { + return 0 + } + rest := loc[colon+1:] + end := strings.IndexByte(rest, ' ') + if end >= 0 { + rest = rest[:end] + } + n, _ := strconv.Atoi(rest) + return n + } + } + return 0 +} +` + +func TestRuntimeStatementLineInfo(t *testing.T) { + source := runtimeStatementLineProbe + source = strings.ReplaceAll(source, "CALLER_STMT_LINE", strconv.Itoa(markerLine(source, "CALLER_STMT_MARK"))) + source = strings.ReplaceAll(source, "CALLERS_STMT_LINE", strconv.Itoa(markerLine(source, "CALLERS_STMT_MARK"))) + source = strings.ReplaceAll(source, "INTERFACE_CALL_LINE", strconv.Itoa(markerLine(source, "INTERFACE_CALL_MARK"))) + source = strings.ReplaceAll(source, "CLOSURE_CALL_LINE", strconv.Itoa(markerLine(source, "CLOSURE_CALL_MARK"))) + source = strings.ReplaceAll(source, "STACK_ONE_LINE", strconv.Itoa(markerLine(source, "STACK_ONE_MARK"))) + source = strings.ReplaceAll(source, "STACK_TWO_LINE", strconv.Itoa(markerLine(source, "STACK_TWO_MARK"))) + source = strings.ReplaceAll(source, "BOUNDS_LINE", strconv.Itoa(markerLine(source, "BOUNDS_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 statement line probe failed: %v\n%s", err, out) + } +} diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index d16ea32788..3b43c16c56 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -1743,17 +1743,13 @@ xfails: directive: run case: noinit.go reason: current main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: recover2.go - reason: current main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: runoutput case: rangegen.go reason: current main goroot runoutput failure on darwin/arm64 - platform: darwin/arm64 directive: run - case: zerodivide.go + case: switch.go reason: current main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run @@ -1801,11 +1797,6 @@ xfails: directive: run case: noinit.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: recover2.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -1816,11 +1807,6 @@ xfails: directive: run case: switch.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: zerodivide.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -1866,11 +1852,6 @@ xfails: directive: run case: noinit.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: recover2.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -1881,11 +1862,6 @@ xfails: directive: run case: switch.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: zerodivide.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -1936,11 +1912,6 @@ xfails: directive: run case: noinit.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: recover2.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -1951,11 +1922,6 @@ xfails: directive: run case: switch.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: zerodivide.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -2091,14 +2057,6 @@ xfails: directive: run case: recover.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: recover1.go - reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: recover4.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: stackobj.go @@ -2159,10 +2117,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 @@ -2400,16 +2354,6 @@ xfails: directive: run case: recover.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: recover1.go - reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: recover4.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -2526,11 +2470,6 @@ xfails: directive: run case: mallocfin.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: recover1.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -2611,11 +2550,6 @@ xfails: directive: run case: fixedbugs/issue4562.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: recover4.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -2696,16 +2630,6 @@ xfails: directive: run case: mallocfin.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: recover1.go - reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: recover4.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -2982,16 +2906,6 @@ xfails: directive: run case: fixedbugs/issue47928.go reason: current main goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue73917.go - reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue73920.go - reason: go1.26 goroot run failure on linux/amd64 - platform: linux/amd64 directive: run case: fixedbugs/issue8048.go