diff --git a/cl/builtin_test.go b/cl/builtin_test.go index 211becc7a4..dd33396215 100644 --- a/cl/builtin_test.go +++ b/cl/builtin_test.go @@ -859,24 +859,18 @@ func TestHandleExportDiffName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Save and restore global state - oldEnableExportRename := enableExportRename - defer func() { - EnableExportRename(oldEnableExportRename) - }() - EnableExportRename(tt.enableExportRename) - // Setup context prog := llssa.NewProgram(nil) pkg := prog.NewPackage("test", "test") ctx := &context{ - prog: prog, - pkg: pkg, + prog: prog, + pkg: pkg, + options: Options{ExportRename: tt.enableExportRename}, } // Call initLinkname with closure that mimics initLinknameByDoc behavior ret := ctx.initLinkname(tt.line, true, func(name string, isExport bool) (string, bool, bool) { - return tt.fullName, false, name == tt.inPkgName || (isExport && enableExportRename) + return tt.fullName, false, name == tt.inPkgName || (isExport && ctx.options.ExportRename) }) // Verify result @@ -944,7 +938,7 @@ func TestInitLinknameByDocExportDiffNames(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Without enableExportRename, export with different name will panic + // Without ExportRename, export with different names will panic. if !tt.wantExported && !tt.enableExportRename { defer func() { if r := recover(); r == nil { @@ -953,19 +947,13 @@ func TestInitLinknameByDocExportDiffNames(t *testing.T) { }() } - // Save and restore global state - oldEnableExportRename := enableExportRename - defer func() { - EnableExportRename(oldEnableExportRename) - }() - EnableExportRename(tt.enableExportRename) - // Setup context prog := llssa.NewProgram(nil) pkg := prog.NewPackage("test", "test") ctx := &context{ - prog: prog, - pkg: pkg, + prog: prog, + pkg: pkg, + options: Options{ExportRename: tt.enableExportRename}, } // Call initLinknameByDoc @@ -1018,17 +1006,12 @@ func TestInitLinkExportDiffNames(t *testing.T) { }() } - oldEnableExportRename := enableExportRename - defer func() { - EnableExportRename(oldEnableExportRename) - }() - EnableExportRename(tt.enableExportRename) - prog := llssa.NewProgram(nil) pkg := prog.NewPackage("test", "test") ctx := &context{ - prog: prog, - pkg: pkg, + prog: prog, + pkg: pkg, + options: Options{ExportRename: tt.enableExportRename}, } ctx.initLinkname(tt.line, true, func(inPkgName string, isExport bool) (fullName string, isVar, ok bool) { diff --git a/cl/caller_frame_test.go b/cl/caller_frame_test.go index b4689e53ed..b9d78eea23 100644 --- a/cl/caller_frame_test.go +++ b/cl/caller_frame_test.go @@ -499,7 +499,6 @@ func TestRuntimeFrameNameNormalization(t *testing.T) { } func TestCompileRuntimeCallerFrameInstrumentation(t *testing.T) { - t.Setenv("LLGO_SHADOW_STACK", "1") ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo import "runtime/debug" @@ -508,7 +507,9 @@ func f() { } `) prog := newLLSSAProg(t) - pkg, err := NewPackage(prog, ssapkg, files) + pkg, _, err := NewPackageExWithEmbedMetaOptions( + prog, nil, nil, nil, ssapkg, files, nil, false, Options{ShadowStack: true}, + ) if err != nil { t.Fatal(err) } @@ -742,7 +743,6 @@ func top() { } func TestCompileRuntimeCallerFrameUsesGoNameForLinkname(t *testing.T) { - t.Setenv("LLGO_SHADOW_STACK", "1") ssapkg, files := buildCallerFrameSSAPackage(t, "command-line-arguments", `package main import "runtime" @@ -753,7 +753,9 @@ func renamedPC() uintptr { `) prog := newLLSSAProg(t) prog.SetLinkname("command-line-arguments.renamedPC", "main.renamedPCSymbol") - pkg, err := NewPackage(prog, ssapkg, files) + pkg, _, err := NewPackageExWithEmbedMetaOptions( + prog, nil, nil, nil, ssapkg, files, nil, false, Options{ShadowStack: true}, + ) if err != nil { t.Fatal(err) } @@ -821,7 +823,6 @@ func f() { _ = runtime.FuncForPC(0) } } func TestCompileRuntimeCallerLocationOnlyForRuntimePaths(t *testing.T) { - t.Setenv("LLGO_SHADOW_STACK", "1") ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo import "runtime" @@ -833,7 +834,9 @@ func f() { } `) prog := newLLSSAProg(t) - pkg, err := NewPackage(prog, ssapkg, files) + pkg, _, err := NewPackageExWithEmbedMetaOptions( + prog, nil, nil, nil, ssapkg, files, nil, false, Options{ShadowStack: true}, + ) if err != nil { t.Fatal(err) } diff --git a/cl/caller_tracking_precompute_test.go b/cl/caller_tracking_precompute_test.go index 9413110a82..3524d9a05a 100644 --- a/cl/caller_tracking_precompute_test.go +++ b/cl/caller_tracking_precompute_test.go @@ -19,6 +19,7 @@ package cl import ( + "reflect" "sync" "testing" @@ -72,6 +73,38 @@ func Logs() { dep.Where() } wg.Wait() } +func TestCallerTrackingPrecomputeMatchesLazyAnalysis(t *testing.T) { + dep, root := buildCallerFrameSSAProgram(t, + "example.com/dep", `package dep +import "runtime" +func Where() { runtime.Caller(0) } +func Quiet() {} +`, + "example.com/root", `package root +import "example.com/dep" +func Logs() { dep.Where() } +func Plain() { dep.Quiet() } +`) + pkgs := []*gossa.Package{root, dep} + lazy := NewCallerTracking() + for _, pkg := range pkgs { + runtimeCallerBaseSet(lazy, pkg) + } + for _, pkg := range pkgs { + runtimeCallerFuncSet(lazy, pkg) + } + precomputed := NewCallerTracking() + precomputed.Precompute(pkgs) + for _, pkg := range pkgs { + if got, want := precomputed.base[pkg], lazy.base[pkg]; !reflect.DeepEqual(got, want) { + t.Fatalf("precomputed base set for %s differs from lazy set", pkg.Pkg.Path()) + } + if got, want := precomputed.extended[pkg], lazy.extended[pkg]; !reflect.DeepEqual(got, want) { + t.Fatalf("precomputed extended set for %s differs from lazy set", pkg.Pkg.Path()) + } + } +} + func TestCallerTrackingPrecomputeRejectsLatePackages(t *testing.T) { dep, root := buildCallerFrameSSAProgram(t, "example.com/dep", `package dep diff --git a/cl/cltest/cltest.go b/cl/cltest/cltest.go index 4a1fc60bef..c6832ef121 100644 --- a/cl/cltest/cltest.go +++ b/cl/cltest/cltest.go @@ -760,13 +760,6 @@ func filterRunOutput(in []byte) []byte { 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 { @@ -792,9 +785,12 @@ func CompileIREx(t *testing.T, src any, fname string, dbg bool, configure func(l configure(prog) } - ret, err := cl.NewPackage(prog, foo, files) + ret, _, err := cl.NewPackageExWithEmbedMetaOptions( + prog, nil, nil, nil, foo, files, nil, false, + cl.Options{Debug: dbg, DebugSymbols: dbg}, + ) if err != nil { - t.Fatal("cl.NewPackage failed:", err) + t.Fatal("cl.NewPackageExWithEmbedMetaOptions failed:", err) } return ret.String() } diff --git a/cl/compile.go b/cl/compile.go index ed0f24e3a4..e99574105c 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -51,23 +51,12 @@ const ( ) var ( - debugInstr bool - debugGoSSA bool - - enableCallTracing bool - enableDbg bool - enableDbgSyms bool - disableInline bool - - // enableExportRename enables //export to use different C symbol names than Go function names. - // This is for TinyGo compatibility when using -target flag for embedded targets. - // Currently, using -target implies TinyGo embedded target mode. - enableExportRename bool + debugInstr bool + debugGoSSA bool + disableInline bool ) -// Options contains frontend behavior for one package compilation. Drivers that -// may host multiple builds in one process should pass Options explicitly -// instead of changing the legacy package-level Enable* settings. +// Options contains frontend behavior for one package compilation. type Options struct { Debug bool DebugSymbols bool @@ -79,16 +68,6 @@ type Options struct { PreloadedSyntax bool } -func legacyOptions() Options { - return Options{ - Debug: enableDbg, - DebugSymbols: enableDbgSyms, - Trace: enableCallTracing, - ExportRename: enableExportRename, - ShadowStack: os.Getenv("LLGO_SHADOW_STACK") == "1", - } -} - // SetDebug sets debug flags. func SetDebug(dbgFlags dbgFlags) { debugInstr = (dbgFlags & DbgFlagInstruction) != 0 @@ -139,31 +118,6 @@ func dbgGoSSAln(args ...any) { } } -// EnableDebug changes the legacy process-wide default. -// Deprecated: pass Options to NewPackageExWithEmbedMetaOptions. -func EnableDebug(b bool) { - enableDbg = b -} - -// EnableDbgSyms changes the legacy process-wide default. -// Deprecated: pass Options to NewPackageExWithEmbedMetaOptions. -func EnableDbgSyms(b bool) { - enableDbgSyms = b -} - -// EnableTrace changes the legacy process-wide default. -// Deprecated: pass Options to NewPackageExWithEmbedMetaOptions. -func EnableTrace(b bool) { - enableCallTracing = b -} - -// EnableExportRename enables or disables //export with different C symbol names. -// This is enabled when using -target flag for TinyGo compatibility. -// Deprecated: pass Options to NewPackageExWithEmbedMetaOptions. -func EnableExportRename(b bool) { - enableExportRename = b -} - // ----------------------------------------------------------------------------- type instrOrValue interface { @@ -213,7 +167,6 @@ type context struct { runtimeCallerFuncs map[*ssa.Function]bool pcLineSeq uint64 options Options - optionsSet bool recoverSlots map[*ssa.Alloc]none patches Patches @@ -248,13 +201,6 @@ type context struct { locality localityLowering } -func (p *context) frontendOptions() Options { - if p != nil && p.optionsSet { - return p.options - } - return legacyOptions() -} - func (p *context) rewriteValue(name string) (string, bool) { if p.rewrites == nil { return "", false @@ -699,8 +645,8 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun if f.Recover != nil { // set recover block fn.SetRecover(fn.Block(f.Recover.Index)) } - dbgEnabled := p.frontendOptions().Debug - dbgSymsEnabled := p.frontendOptions().DebugSymbols && (f == nil || f.Origin() == nil) + dbgEnabled := p.options.Debug + dbgSymsEnabled := p.options.DebugSymbols && (f == nil || f.Origin() == nil) p.inits = append(p.inits, func() { oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark := p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark oldLocalityFunction := p.locality.function @@ -958,11 +904,11 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do if block.Index == 0 && p.shouldTrackCallerFrames() { p.pushCallerLocationFrame(b, block.Parent()) } - if block.Index == 0 && p.frontendOptions().Trace && !strings.HasPrefix(fn.Name(), "github.com/goplus/llgo/runtime/internal/runtime.Print") { + if block.Index == 0 && p.options.Trace && !strings.HasPrefix(fn.Name(), "github.com/goplus/llgo/runtime/internal/runtime.Print") { b.Printf("call " + fn.Name() + "\n\x00") } // place here to avoid wrong current-block - if p.frontendOptions().DebugSymbols && block.Parent().Origin() == nil && block.Index == 0 { + if p.options.DebugSymbols && block.Parent().Origin() == nil && block.Index == 0 { p.debugParams(b, block.Parent()) } @@ -1800,7 +1746,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { if _, ok := p.staticInitInstrs[instr]; ok { return } - if p.frontendOptions().Debug && instr.Parent().Origin() == nil { + if p.options.Debug && instr.Parent().Origin() == nil { if _, isDebugRef := instr.(*ssa.DebugRef); !isDebugRef { scope := p.getDebugLocScope(instr.Parent(), instr.Pos()) if scope != nil { @@ -1920,7 +1866,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { p.recordPanicLocation(b, v.Pos()) b.Send(ch, x) case *ssa.DebugRef: - if p.frontendOptions().DebugSymbols && v.Parent().Origin() == nil { + if p.options.DebugSymbols && v.Parent().Origin() == nil { p.debugRef(b, v) } default: @@ -1983,7 +1929,7 @@ func (p *context) compileValue(b llssa.Builder, v ssa.Value) llssa.Expr { if isCgoVar(varName) { p.cgoSymbols = append(p.cgoSymbols, val.Name()) } - if p.frontendOptions().DebugSymbols && p.localityAllowsGlobalDebug(v) { + if p.options.DebugSymbols && p.localityAllowsGlobalDebug(v) { pos := p.fset.Position(v.Pos()) b.DIGlobal(val, v.Name(), pos) } @@ -2242,7 +2188,7 @@ func NewPackage(prog llssa.Program, pkg *ssa.Package, files []*ast.File) (ret ll // only affects string-typed globals defined in the current package. // Deprecated: use NewPackageExWithEmbedMetaOptions with explicit Options. func NewPackageEx(prog llssa.Program, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File) (ret llssa.Package, externs []string, err error) { - return newPackageEx(prog, nil, patches, rewrites, pkg, files, nil, false, legacyOptions()) + return newPackageEx(prog, nil, patches, rewrites, pkg, files, nil, false, Options{}) } // NewPackageExWithEmbed compiles a package using pre-loaded go:embed metadata. @@ -2254,13 +2200,13 @@ func NewPackageEx(prog llssa.Program, patches Patches, rewrites map[string]strin // instance is created for this call. // Deprecated: use NewPackageExWithEmbedMetaOptions with explicit Options. func NewPackageExWithEmbed(prog llssa.Program, ct *CallerTracking, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File, embedMap goembed.VarMap) (ret llssa.Package, externs []string, err error) { - return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, false, legacyOptions()) + return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, false, Options{}) } // NewPackageExWithEmbedMeta compiles a package and optionally collects metadata. // Deprecated: use NewPackageExWithEmbedMetaOptions with explicit Options. func NewPackageExWithEmbedMeta(prog llssa.Program, ct *CallerTracking, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File, embedMap goembed.VarMap, metaCollect bool) (ret llssa.Package, externs []string, err error) { - return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, metaCollect, legacyOptions()) + return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, metaCollect, Options{}) } // NewPackageExWithEmbedMetaOptions is NewPackageExWithEmbedMeta with explicit @@ -2312,7 +2258,6 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri goPkg: pkg, patches: patches, options: options, - optionsSet: true, skips: make(map[string]none), vargs: make(map[*ssa.Alloc][]llssa.Expr), funcs: make(map[*ssa.Function]llssa.Function), diff --git a/cl/debug_compile_test.go b/cl/debug_compile_test.go index 1338ba715c..2d5ba3f153 100644 --- a/cl/debug_compile_test.go +++ b/cl/debug_compile_test.go @@ -19,45 +19,6 @@ import ( "golang.org/x/tools/go/ssa/ssautil" ) -func TestFrontendOptions(t *testing.T) { - oldDebug := enableDbg - oldDebugSymbols := enableDbgSyms - oldTrace := enableCallTracing - oldExportRename := enableExportRename - t.Cleanup(func() { - enableDbg = oldDebug - enableDbgSyms = oldDebugSymbols - enableCallTracing = oldTrace - enableExportRename = oldExportRename - }) - - EnableDebug(true) - EnableDbgSyms(true) - EnableTrace(true) - EnableExportRename(true) - t.Setenv("LLGO_SHADOW_STACK", "1") - - wantLegacy := Options{ - Debug: true, - DebugSymbols: true, - Trace: true, - ExportRename: true, - ShadowStack: true, - } - if got := (&context{}).frontendOptions(); got != wantLegacy { - t.Fatalf("frontendOptions() = %+v, want legacy options %+v", got, wantLegacy) - } - if got := (*context)(nil).frontendOptions(); got != wantLegacy { - t.Fatalf("nil frontendOptions() = %+v, want legacy options %+v", got, wantLegacy) - } - - wantExplicit := Options{Trace: true} - ctx := &context{options: wantExplicit, optionsSet: true} - if got := ctx.frontendOptions(); got != wantExplicit { - t.Fatalf("frontendOptions() = %+v, want explicit options %+v", got, wantExplicit) - } -} - func TestCompileDebugMetadata(t *testing.T) { const source = `package debugcompile diff --git a/cl/import.go b/cl/import.go index d097889a46..b5c406e3c9 100644 --- a/cl/import.go +++ b/cl/import.go @@ -152,7 +152,7 @@ func (p *context) importPkg(pkg *types.Package, i *pkgInfo) { } start: i.kind = kind - if p.frontendOptions().PreloadedSyntax { + if p.options.PreloadedSyntax { return } fset := p.fset @@ -186,7 +186,7 @@ start: } func (p *context) initFiles(pkgPath string, files []*ast.File, cPkg bool) { - preloaded := p.frontendOptions().PreloadedSyntax + preloaded := p.options.PreloadedSyntax for _, file := range files { for _, decl := range file.Decls { switch decl := decl.(type) { @@ -302,7 +302,7 @@ func (p *context) collectSkip(line string, prefix int) { // collectDeclarationDirectives caches source metadata needed after the syntax // pass. funcPos is token.NoPos for non-function declarations. func collectDeclarationDirectives(prog llssa.Program, fset *token.FileSet, doc *ast.CommentGroup, fullName, inPkgName string, funcPos token.Pos) { - _, _ = collectDeclarationDirectivesWithOptions(prog, fset, doc, fullName, inPkgName, funcPos, legacyOptions()) + _, _ = collectDeclarationDirectivesWithOptions(prog, fset, doc, fullName, inPkgName, funcPos, Options{}) } func collectDeclarationDirectivesWithOptions(prog llssa.Program, fset *token.FileSet, doc *ast.CommentGroup, fullName, inPkgName string, funcPos token.Pos, options Options) (bool, error) { @@ -348,7 +348,7 @@ func (p *context) processLinknameByDoc(doc *ast.CommentGroup, fullName, inPkgNam for n := len(doc.List) - 1; n >= 0; n-- { line := doc.List[n].Text ret := p.initLinkname(line, allowExport, func(name string, isExport bool) (_ string, _, ok bool) { - return fullName, isVar, name == inPkgName || (isExport && p.frontendOptions().ExportRename) + return fullName, isVar, name == inPkgName || (isExport && p.options.ExportRename) }) if ret != unknownDirective { return ret == hasLinkname @@ -422,7 +422,7 @@ func (p *context) initLink(line string, prefix int, export bool, f func(inPkgNam } } else { // Export with different names already processed by initLinknameByDoc - if export && p.frontendOptions().ExportRename { + if export && p.options.ExportRename { return } if export { @@ -804,9 +804,9 @@ func (p *context) initPyModule() { } // ParsePkgSyntax collects declaration directives in one syntax pass before SSA -// creation using the legacy frontend options. +// creation using default frontend options. func ParsePkgSyntax(prog llssa.Program, fset *token.FileSet, pkg *types.Package, files []*ast.File) error { - return ParsePkgSyntaxWithOptions(prog, fset, pkg, files, legacyOptions()) + return ParsePkgSyntaxWithOptions(prog, fset, pkg, files, Options{}) } // ParsePkgSyntaxWithOptions collects all Program-side declaration metadata. @@ -818,7 +818,7 @@ func ParsePkgSyntaxWithOptions(prog llssa.Program, fset *token.FileSet, pkg *typ if prog.PackageSyntaxParsed(pkg) { return nil } - ctx := &context{prog: prog, options: options, optionsSet: true} + ctx := &context{prog: prog, options: options} pkgPath := llssa.PathOf(pkg) for _, file := range files { for _, decl := range file.Decls { diff --git a/cl/instr.go b/cl/instr.go index aa6a95a9d6..3c9bd4bd12 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -25,6 +25,7 @@ import ( "log" "os" "regexp" + "sort" "strings" "golang.org/x/tools/go/ssa" @@ -931,11 +932,19 @@ func runtimeCallerFuncSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function panic("caller-tracking function set was not precomputed") } base := runtimeCallerBaseSet(c, pkg) + _, trackable := collectRuntimeCallerFunctions(pkg) + out := computeRuntimeCallerFuncSet(pkg, base, trackable, func(dep *ssa.Package) map[*ssa.Function]bool { + return runtimeCallerBaseSet(c, dep) + }) + c.extended[pkg] = out + return out +} + +func computeRuntimeCallerFuncSet(pkg *ssa.Package, base, trackable map[*ssa.Function]bool, baseSet func(*ssa.Package) map[*ssa.Function]bool) map[*ssa.Function]bool { out := make(map[*ssa.Function]bool, len(base)) for fn := range base { out[fn] = true } - _, trackable := collectRuntimeCallerFunctions(pkg) for fn := range trackable { if out[fn] { continue @@ -964,7 +973,7 @@ func runtimeCallerFuncSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function if !canTrackCallerFramesForPackage(callee.Pkg.Pkg.Path()) { return } - if runtimeCallerBaseSet(c, callee.Pkg)[callee] { + if baseSet(callee.Pkg)[callee] { out[fn] = true } }) @@ -972,7 +981,6 @@ func runtimeCallerFuncSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function if len(out) == 0 { out = nil } - c.extended[pkg] = out return out } @@ -1002,18 +1010,133 @@ func (c *CallerTracking) Precompute(pkgs []*ssa.Package) { if c == nil { return } + pkgs = uniqueCallerTrackingPackages(pkgs) + if c.precomputed { + for _, pkg := range pkgs { + if _, ok := c.base[pkg]; !ok { + panic("caller-tracking base set was not precomputed") + } + if _, ok := c.extended[pkg]; !ok { + panic("caller-tracking function set was not precomputed") + } + } + return + } + if len(pkgs) == 0 { + c.precomputed = true + return + } c.recoverAnalysis().precompute(pkgs) + + // RuntimeTypes and MethodValue are Program-wide and may lazily populate + // shared SSA state. Snapshot and materialize them once in stable package + // order before computing package sets. The remaining analysis is kept + // serial: interface target resolution may still consult MethodValue, and + // scheduling those calls concurrently makes later module layout depend on + // worker completion order. + runtimeTypes := callerTrackingRuntimeTypes(pkgs) + methods := callerTrackingMethods(pkgs, runtimeTypes) + analyses := make([]callerTrackingPackageAnalysis, len(pkgs)) + base := make([]map[*ssa.Function]bool, len(pkgs)) + extended := make([]map[*ssa.Function]bool, len(pkgs)) + index := make(map[*ssa.Package]int, len(pkgs)) + for i, pkg := range pkgs { + index[pkg] = i + } + + for i := range pkgs { + analyses[i] = analyzeCallerTrackingPackage(pkgs[i], methods[pkgs[i]]) + base[i] = analyses[i].base + } + for i := range pkgs { + extended[i] = computeRuntimeCallerFuncSet(pkgs[i], base[i], analyses[i].trackable, func(dep *ssa.Package) map[*ssa.Function]bool { + j, ok := index[dep] + if !ok { + panic("caller-tracking dependency was not precomputed") + } + return base[j] + }) + } + + for i, pkg := range pkgs { + c.base[pkg] = base[i] + } + for i, pkg := range pkgs { + c.extended[pkg] = extended[i] + } + c.precomputed = true +} + +type callerTrackingPackageAnalysis struct { + base map[*ssa.Function]bool + trackable map[*ssa.Function]bool +} + +func analyzeCallerTrackingPackage(pkg *ssa.Package, methods []*ssa.Function) callerTrackingPackageAnalysis { + funcs, trackable := collectRuntimeCallerFunctionsWithMethods(pkg, methods) + analysis := &runtimeCallerAnalysis{ + pkg: pkg, + funcs: funcs, + trackable: trackable, + callsites: collectRuntimeCallerCallsites(funcs), + memo: make(map[*ssa.Function]bool), + visiting: make(map[*ssa.Function]bool), + } + return callerTrackingPackageAnalysis{ + base: computeRuntimeCallerBaseSetFromAnalysis(analysis), + trackable: trackable, + } +} + +// callerTrackingMethods materializes method wrappers in stable package order +// before analysis workers start. Program.MethodValue is thread-safe but may +// lazily create wrappers; doing that in workers would make Program mutation +// order depend on scheduling and could change later module emission order. +func callerTrackingMethods(pkgs []*ssa.Package, runtimeTypes map[*ssa.Package][]types.Type) map[*ssa.Package][]*ssa.Function { + result := make(map[*ssa.Package][]*ssa.Function, len(pkgs)) + for _, pkg := range pkgs { + result[pkg] = collectRuntimeCallerMethods(pkg, runtimeTypes[pkg]) + } + return result +} + +// callerTrackingRuntimeTypes snapshots Program-wide runtime types once and +// partitions them by declaring package before workers start. RuntimeTypes +// computes derived types under Program and method-set locks; calling it from +// every package worker would serialize the supposedly parallel phase and would +// repeatedly scan the same whole-program data. +func callerTrackingRuntimeTypes(pkgs []*ssa.Package) map[*ssa.Package][]types.Type { + byProgram := make(map[*ssa.Program]map[*types.Package][]types.Type) + result := make(map[*ssa.Package][]types.Type, len(pkgs)) for _, pkg := range pkgs { - if pkg != nil { - runtimeCallerBaseSet(c, pkg) + if pkg == nil || pkg.Prog == nil || pkg.Pkg == nil { + continue + } + byPackage, ok := byProgram[pkg.Prog] + if !ok { + byPackage = make(map[*types.Package][]types.Type) + for _, typ := range pkg.Prog.RuntimeTypes() { + if owner := typeOwnerPackage(typ); owner != nil { + byPackage[owner] = append(byPackage[owner], typ) + } + } + byProgram[pkg.Prog] = byPackage } + result[pkg] = byPackage[pkg.Pkg] } + return result +} + +func uniqueCallerTrackingPackages(pkgs []*ssa.Package) []*ssa.Package { + unique := make([]*ssa.Package, 0, len(pkgs)) + seen := make(map[*ssa.Package]bool, len(pkgs)) for _, pkg := range pkgs { - if pkg != nil { - runtimeCallerFuncSet(c, pkg) + if pkg != nil && !seen[pkg] { + seen[pkg] = true + unique = append(unique, pkg) } } - c.precomputed = true + return unique } // NewCallerTracking creates the frontend-analysis caches for one compilation. @@ -1052,27 +1175,30 @@ func runtimeCallerBaseSet(c *CallerTracking, pkg *ssa.Package) map[*ssa.Function } func computeRuntimeCallerBaseSet(pkg *ssa.Package) map[*ssa.Function]bool { - 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), + var runtimeTypes []types.Type + if pkg != nil && pkg.Prog != nil { + for _, typ := range pkg.Prog.RuntimeTypes() { + if typeBelongsToPackage(typ, pkg.Pkg) { + runtimeTypes = append(runtimeTypes, typ) + } + } } + return analyzeCallerTrackingPackage(pkg, collectRuntimeCallerMethods(pkg, runtimeTypes)).base +} + +func computeRuntimeCallerBaseSetFromAnalysis(analysis *runtimeCallerAnalysis) map[*ssa.Function]bool { if !analysis.packageHasRuntimeCaller() { return nil } out := make(map[*ssa.Function]bool) for { - ntrack := len(trackable) - for fn := range trackable { + ntrack := len(analysis.trackable) + for fn := range analysis.trackable { if analysis.fnMayReachRuntimeCaller(fn) { out[fn] = true } } - if len(trackable) == ntrack { + if len(analysis.trackable) == ntrack { break } } @@ -1092,6 +1218,69 @@ type runtimeCallerAnalysis struct { } func collectRuntimeCallerFunctions(pkg *ssa.Package) (funcs, trackable map[*ssa.Function]bool) { + var runtimeTypes []types.Type + if pkg != nil && pkg.Prog != nil { + runtimeTypes = pkg.Prog.RuntimeTypes() + } + return collectRuntimeCallerFunctionsWithMethods(pkg, collectRuntimeCallerMethods(pkg, runtimeTypes)) +} + +func collectRuntimeCallerMethods(pkg *ssa.Package, runtimeTypes []types.Type) []*ssa.Function { + if pkg == nil || pkg.Prog == nil { + return nil + } + type methodType struct { + key string + typ types.Type + } + typesToVisit := make([]methodType, 0) + seenTypes := make(map[string]bool) + addType := func(typ types.Type) { + key := types.TypeString(typ, func(owner *types.Package) string { + if owner == nil { + return "" + } + return owner.Path() + }) + if !seenTypes[key] { + seenTypes[key] = true + typesToVisit = append(typesToVisit, methodType{key: key, typ: typ}) + } + } + for _, member := range pkg.Members { + if typ, ok := member.(*ssa.Type); ok { + addType(typ.Type()) + addType(types.NewPointer(typ.Type())) + } + } + for _, typ := range runtimeTypes { + if typeBelongsToPackage(typ, pkg.Pkg) { + addType(typ) + } + } + sort.Slice(typesToVisit, func(i, j int) bool { + return typesToVisit[i].key < typesToVisit[j].key + }) + + methods := make([]*ssa.Function, 0) + seen := make(map[*ssa.Function]bool) + addMethods := func(typ types.Type) { + methodSet := pkg.Prog.MethodSets.MethodSet(typ) + for i := 0; i < methodSet.Len(); i++ { + fn := pkg.Prog.MethodValue(methodSet.At(i)) + if fn != nil && !seen[fn] { + seen[fn] = true + methods = append(methods, fn) + } + } + } + for _, candidate := range typesToVisit { + addMethods(candidate.typ) + } + return methods +} + +func collectRuntimeCallerFunctionsWithMethods(pkg *ssa.Package, methods []*ssa.Function) (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 @@ -1119,34 +1308,11 @@ func collectRuntimeCallerFunctions(pkg *ssa.Package) (funcs, trackable map[*ssa. add(fn, true) } } - if pkg.Prog != nil { - // Methods are as trackable as package-level functions: one that - // (transitively) calls runtime.Caller needs frames and pcline - // labels of its own. - addMethods := func(typ types.Type) { - methods := pkg.Prog.MethodSets.MethodSet(typ) - for i := 0; i < methods.Len(); i++ { - add(pkg.Prog.MethodValue(methods.At(i)), true) - } - } - // Named-type methods are not package members and a type used only - // concretely never enters RuntimeTypes (slog.(*Logger).Info was - // missed exactly this way); collect both receiver forms from the - // package's own type declarations. - for _, member := range pkg.Members { - if t, ok := member.(*ssa.Type); ok { - addMethods(t.Type()) - addMethods(types.NewPointer(t.Type())) - } - } - if pkg.Pkg != nil { - for _, typ := range pkg.Prog.RuntimeTypes() { - if !typeBelongsToPackage(typ, pkg.Pkg) { - continue - } - addMethods(typ) - } - } + // Methods are as trackable as package-level functions: one that + // (transitively) calls runtime.Caller needs frames and pcline labels of its + // own. Method wrappers were materialized serially before workers started. + for _, method := range methods { + add(method, true) } for changed := true; changed; { changed = false @@ -1185,9 +1351,10 @@ func functionBelongsToPackage(pkg *ssa.Package, fn *ssa.Function) bool { } func typeBelongsToPackage(typ types.Type, pkg *types.Package) bool { - if pkg == nil { - return false - } + return pkg != nil && typeOwnerPackage(typ) == pkg +} + +func typeOwnerPackage(typ types.Type) *types.Package { for { if ptr, ok := types.Unalias(typ).(*types.Pointer); ok { typ = ptr.Elem() @@ -1196,7 +1363,10 @@ func typeBelongsToPackage(typ types.Type, pkg *types.Package) bool { break } named, ok := types.Unalias(typ).(*types.Named) - return ok && named.Obj() != nil && named.Obj().Pkg() == pkg + if !ok || named.Obj() == nil { + return nil + } + return named.Obj().Pkg() } func (a *runtimeCallerAnalysis) packageHasRuntimeCaller() bool { @@ -1516,7 +1686,7 @@ func (p *context) runtimeCallerFrameName() string { } func (p *context) pushCallerLocationFrame(b llssa.Builder, fn *ssa.Function) { - if !p.frontendOptions().ShadowStack { + if !p.options.ShadowStack { return } if fn == nil { @@ -1542,7 +1712,7 @@ func (p *context) recordPanicLocation(b llssa.Builder, pos token.Pos) { } func (p *context) recordRuntimeLocation(b llssa.Builder, pos token.Pos, fn string) { - if !p.frontendOptions().ShadowStack || !p.shouldTrackCallerFrames() { + if !p.options.ShadowStack || !p.shouldTrackCallerFrames() { return } position := p.fset.Position(pos) diff --git a/cl/locality_test.go b/cl/locality_test.go index fe25eed0ae..e170f6db9c 100644 --- a/cl/locality_test.go +++ b/cl/locality_test.go @@ -17,6 +17,10 @@ import ( ) func compileLocalitySource(t *testing.T, src string) (llssa.Program, string) { + return compileLocalitySourceWithOptions(t, src, Options{}) +} + +func compileLocalitySourceWithOptions(t *testing.T, src string, options Options) (llssa.Program, string) { t.Helper() fset := token.NewFileSet() file, err := parser.ParseFile(fset, "locality.go", src, parser.ParseComments) @@ -33,7 +37,7 @@ func compileLocalitySource(t *testing.T, src string) (llssa.Program, string) { prog := ssatest.NewProgramEx(t, nil, imp) prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH)) prog.SetRuntime(localityRuntimePackage()) - if err := ParsePkgSyntax(prog, fset, pkg, files); err != nil { + if err := ParsePkgSyntaxWithOptions(prog, fset, pkg, files, options); err != nil { t.Fatal(err) } if err := PrepareLocalVariables(prog, fset, pkg, info, files); err != nil { @@ -42,7 +46,9 @@ func compileLocalitySource(t *testing.T, src string) (llssa.Program, string) { goProg := ssa.NewProgram(fset, ssa.SanityCheckFunctions) ssaPkg := goProg.CreatePackage(pkg, files, info, true) ssaPkg.Build() - compiled, err := NewPackage(prog, ssaPkg, files) + compiled, _, err := NewPackageExWithEmbedMetaOptions( + prog, nil, nil, nil, ssaPkg, files, nil, false, options, + ) if err != nil { t.Fatal(err) } @@ -211,11 +217,7 @@ func values() (int, *int, int, *int) { } func TestLocalityDebugInfoOnlyUsesFixedGlobals(t *testing.T) { - EnableDebug(true) - EnableDbgSyms(true) - defer EnableDebug(false) - defer EnableDbgSyms(false) - _, ir := compileLocalitySource(t, `package locality + _, ir := compileLocalitySourceWithOptions(t, `package locality //llgo:tls var direct int @@ -224,7 +226,7 @@ var direct int var pointer *int func values() (int, *int) { return direct, pointer } -`) +`, Options{Debug: true, DebugSymbols: true}) direct := `@"example.com/locality.direct" = thread_local global i64` start := strings.Index(ir, direct)