diff --git a/cl/caller_frame_test.go b/cl/caller_frame_test.go index a5022e81f5..b4689e53ed 100644 --- a/cl/caller_frame_test.go +++ b/cl/caller_frame_test.go @@ -499,9 +499,7 @@ func TestRuntimeFrameNameNormalization(t *testing.T) { } func TestCompileRuntimeCallerFrameInstrumentation(t *testing.T) { - old := emitShadowStackInstrumentation - emitShadowStackInstrumentation = true - defer func() { emitShadowStackInstrumentation = old }() + t.Setenv("LLGO_SHADOW_STACK", "1") ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo import "runtime/debug" @@ -744,9 +742,7 @@ func top() { } func TestCompileRuntimeCallerFrameUsesGoNameForLinkname(t *testing.T) { - old := emitShadowStackInstrumentation - emitShadowStackInstrumentation = true - defer func() { emitShadowStackInstrumentation = old }() + t.Setenv("LLGO_SHADOW_STACK", "1") ssapkg, files := buildCallerFrameSSAPackage(t, "command-line-arguments", `package main import "runtime" @@ -825,9 +821,7 @@ func f() { _ = runtime.FuncForPC(0) } } func TestCompileRuntimeCallerLocationOnlyForRuntimePaths(t *testing.T) { - old := emitShadowStackInstrumentation - emitShadowStackInstrumentation = true - defer func() { emitShadowStackInstrumentation = old }() + t.Setenv("LLGO_SHADOW_STACK", "1") ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo import "runtime" diff --git a/cl/compile.go b/cl/compile.go index 1e8d1e07af..9032deb02b 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -65,6 +65,27 @@ var ( enableExportRename 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. +type Options struct { + Debug bool + DebugSymbols bool + Trace bool + ExportRename bool + ShadowStack 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 @@ -115,20 +136,27 @@ 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 } @@ -180,6 +208,8 @@ type context struct { debugAllocVars map[*ssa.Alloc]*types.Var runtimeCallerFuncs map[*ssa.Function]bool pcLineSeq uint64 + options Options + optionsSet bool patches Patches blkInfos []blocks.Info @@ -213,6 +243,13 @@ 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 @@ -601,8 +638,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 := enableDbg - dbgSymsEnabled := enableDbgSyms && (f == nil || f.Origin() == nil) + dbgEnabled := p.frontendOptions().Debug + dbgSymsEnabled := p.frontendOptions().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 @@ -850,11 +887,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 && enableCallTracing && !strings.HasPrefix(fn.Name(), "github.com/goplus/llgo/runtime/internal/runtime.Print") { + if block.Index == 0 && p.frontendOptions().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 enableDbgSyms && block.Parent().Origin() == nil && block.Index == 0 { + if p.frontendOptions().DebugSymbols && block.Parent().Origin() == nil && block.Index == 0 { p.debugParams(b, block.Parent()) } @@ -1640,7 +1677,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { if _, ok := p.staticInitInstrs[instr]; ok { return } - if enableDbg && instr.Parent().Origin() == nil { + if p.frontendOptions().Debug && instr.Parent().Origin() == nil { if _, isDebugRef := instr.(*ssa.DebugRef); !isDebugRef { scope := p.getDebugLocScope(instr.Parent(), instr.Pos()) if scope != nil { @@ -1757,7 +1794,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { p.recordPanicLocation(b, v.Pos()) b.Send(ch, x) case *ssa.DebugRef: - if enableDbgSyms && v.Parent().Origin() == nil { + if p.frontendOptions().DebugSymbols && v.Parent().Origin() == nil { p.debugRef(b, v) } default: @@ -1820,7 +1857,7 @@ func (p *context) compileValue(b llssa.Builder, v ssa.Value) llssa.Expr { if isCgoVar(varName) { p.cgoSymbols = append(p.cgoSymbols, val.Name()) } - if enableDbgSyms && p.localityAllowsGlobalDebug(v) { + if p.frontendOptions().DebugSymbols && p.localityAllowsGlobalDebug(v) { pos := p.fset.Position(v.Pos()) b.DIGlobal(val, v.Name(), pos) } @@ -2053,6 +2090,7 @@ type Patch struct { type Patches = map[string]Patch // NewPackage compiles a Go package to LLVM IR package. +// Deprecated: use NewPackageExWithEmbedMetaOptions with explicit Options. func NewPackage(prog llssa.Program, pkg *ssa.Package, files []*ast.File) (ret llssa.Package, err error) { ret, _, err = NewPackageEx(prog, nil, nil, pkg, files) return @@ -2073,8 +2111,9 @@ func NewPackage(prog llssa.Program, pkg *ssa.Package, files []*ast.File) (ret ll // // The rewrites map uses short variable names (without package qualifier) and // 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) + return newPackageEx(prog, nil, patches, rewrites, pkg, files, nil, false, legacyOptions()) } // NewPackageExWithEmbed compiles a package using pre-loaded go:embed metadata. @@ -2084,15 +2123,24 @@ func NewPackageEx(prog llssa.Program, patches Patches, rewrites map[string]strin // compiling multiple packages pass the same instance for every package // of one compilation (like patches). nil means one-shot: a fresh // 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) + return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, false, legacyOptions()) } +// 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) + return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, metaCollect, legacyOptions()) +} + +// NewPackageExWithEmbedMetaOptions is NewPackageExWithEmbedMeta with explicit +// per-package frontend options. +func NewPackageExWithEmbedMetaOptions(prog llssa.Program, ct *CallerTracking, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File, embedMap goembed.VarMap, metaCollect bool, options Options) (ret llssa.Package, externs []string, err error) { + return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, metaCollect, options) } -func newPackageEx(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) { +func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File, embedMap *goembed.VarMap, metaCollect bool, options Options) (ret llssa.Package, externs []string, err error) { pkgProg := pkg.Prog pkgTypes := pkg.Pkg oldTypes := pkgTypes @@ -2116,7 +2164,7 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri prog.SetRuntime(pkgTypes) } ret = prog.NewPackageEx(pkgName, pkgPath, metaCollect) - if enableDbg { + if options.Debug { ret.InitDebug(pkgName, pkgPath, pkgProg.Fset) defer ret.FinalizeDebug() } @@ -2132,6 +2180,8 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri goTyps: pkgTypes, 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 2f177005b2..1338ba715c 100644 --- a/cl/debug_compile_test.go +++ b/cl/debug_compile_test.go @@ -19,6 +19,45 @@ 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 @@ -55,21 +94,16 @@ var anonymous = func(seed int) int { t.Fatal(err) } - oldDebug, oldDebugSyms := enableDbg, enableDbgSyms - EnableDebug(true) - EnableDbgSyms(true) - defer func() { - EnableDebug(oldDebug) - EnableDbgSyms(oldDebugSyms) - }() - prog := newLLSSAProgForTarget(t, &llssa.Target{ GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, OptLevel: optlevel.O0, }) defer prog.Dispose() - pkg, err := NewPackage(prog, ssaPkg, []*ast.File{file}) + pkg, _, err := newPackageEx(prog, nil, nil, nil, ssaPkg, []*ast.File{file}, nil, false, Options{ + Debug: true, + DebugSymbols: true, + }) if err != nil { t.Fatal(err) } diff --git a/cl/import.go b/cl/import.go index a1d284f6c3..85e4cfc9fd 100644 --- a/cl/import.go +++ b/cl/import.go @@ -296,7 +296,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 && enableExportRename) + return fullName, isVar, name == inPkgName || (isExport && p.frontendOptions().ExportRename) }) if ret != unknownDirective { return ret == hasLinkname @@ -370,7 +370,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 && enableExportRename { + if export && p.frontendOptions().ExportRename { return } if export { diff --git a/cl/instr.go b/cl/instr.go index 678c4afc6d..7e091eee82 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -1481,16 +1481,8 @@ func (p *context) runtimeCallerFrameName() string { return "" } -// emitShadowStackInstrumentation gates the legacy shadow-stack calls -// (PushCallerLocationFrame / RecordCallerLocation / RecordPanicLocation). -// The FP-chain unwinder supersedes them: physical pcs resolve through the -// prebuilt ftab and pcline labels, so tracked functions keep only noinline, -// no-tail-call and the label records. The emitters stay for one release as -// an escape hatch (LLGO_SHADOW_STACK=1). -var emitShadowStackInstrumentation = os.Getenv("LLGO_SHADOW_STACK") == "1" - func (p *context) pushCallerLocationFrame(b llssa.Builder, fn *ssa.Function) { - if !emitShadowStackInstrumentation { + if !p.frontendOptions().ShadowStack { return } if fn == nil { @@ -1516,7 +1508,7 @@ func (p *context) recordPanicLocation(b llssa.Builder, pos token.Pos) { } func (p *context) recordRuntimeLocation(b llssa.Builder, pos token.Pos, fn string) { - if !emitShadowStackInstrumentation || !p.shouldTrackCallerFrames() { + if !p.frontendOptions().ShadowStack || !p.shouldTrackCallerFrames() { return } position := p.fset.Position(pos) diff --git a/internal/build/build.go b/internal/build/build.go index 0d45476efc..c53ce6b63f 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -354,6 +354,8 @@ const ( loadSyntax = loadTypes | packages.NeedSyntax | packages.NeedTypesInfo ) +var llssaInitOnce sync.Once + func Do(args []string, conf *Config) ([]Package, error) { return Build(Invocation{Args: args, Config: conf}) } @@ -391,11 +393,6 @@ func Build(inv Invocation) ([]Package, error) { if err := validateLinkOptions(conf, &export); err != nil { return nil, err } - // Enable different export names for TinyGo compatibility when using -target - if conf.Target != "" { - cl.EnableExportRename(true) - } - verbose := conf.Verbose patterns := slices.Clone(inv.Args) tags := defaultBuildTags(conf.Goarch, conf.Target) @@ -428,10 +425,16 @@ func Build(inv Invocation) ([]Package, error) { cfg.Mode |= packages.NeedForTest } emitDebugInfo := shouldEmitDebugInfo(conf, &export) - cl.EnableDebug(emitDebugInfo) - cl.EnableDbgSyms(emitDebugInfo) - cl.EnableTrace(IsTraceEnabled()) - llssa.Initialize(llssa.InitAll) + frontendOptions := cl.Options{ + Debug: emitDebugInfo, + DebugSymbols: emitDebugInfo, + Trace: IsTraceEnabled(), + ExportRename: conf.Target != "", + ShadowStack: isEnvOn(llgoShadowStack, false), + } + llssaInitOnce.Do(func() { + llssa.Initialize(llssa.InitAll) + }) target := &llssa.Target{ GOOS: conf.Goos, @@ -598,15 +601,16 @@ func Build(inv Invocation) ([]Package, error) { ctx := &context{conf: cfg, progSSA: progSSA, prog: prog, dedup: dedup, patches: patches, callerTracking: cl.NewCallerTracking(), built: make(map[string]none), initial: initial, mode: mode, - fingerprinting: make(map[string]bool), - pkgs: map[*packages.Package]Package{}, - pkgByID: map[string]Package{}, - output: output, - passOpt: passOpt, - buildConf: conf, - crossCompile: export, - commands: commands, - cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize), + fingerprinting: make(map[string]bool), + pkgs: map[*packages.Package]Package{}, + pkgByID: map[string]Package{}, + output: output, + passOpt: passOpt, + buildConf: conf, + crossCompile: export, + commands: commands, + frontendOptions: frontendOptions, + cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize), } defer ctx.closePackageMetas() @@ -861,9 +865,10 @@ type context struct { output bool passOpt bool - buildConf *Config - crossCompile crosscompile.Export - commands commandEnv + buildConf *Config + crossCompile crosscompile.Export + commands commandEnv + frontendOptions cl.Options cTransformer *cabi.Transformer @@ -1747,22 +1752,17 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error { syntax = append(syntax, altPkg.Syntax...) } showDetail := verbose && pkgExists(ctx.initial, pkg) + needMeta := !aPkg.CacheHit && ctx.buildConf.packageMetaEnabled() if showDetail { - llssa.SetDebug(llssa.DbgFlagAll) - cl.SetDebug(cl.DbgFlagAll) - defer func() { - llssa.SetDebug(0) - cl.SetDebug(0) - }() + fmt.Fprintf(os.Stderr, "==> Compile %s\n", pkgPath) } - embedMap, err := goembed.LoadDirectives(ctx.conf.Fset, syntax) if err != nil { return fmt.Errorf("load go:embed directives for %s failed: %w", pkgPath, err) } - - needMeta := !aPkg.CacheHit && ctx.buildConf.packageMetaEnabled() - ret, externs, err := cl.NewPackageExWithEmbedMeta(ctx.prog, ctx.callerTracking, ctx.patches, aPkg.rewriteVars, aPkg.SSA, syntax, embedMap, needMeta) + ret, externs, err := cl.NewPackageExWithEmbedMetaOptions( + ctx.prog, ctx.callerTracking, ctx.patches, aPkg.rewriteVars, + aPkg.SSA, syntax, embedMap, needMeta, ctx.frontendOptions) check(err) aPkg.LPkg = ret @@ -2362,6 +2362,7 @@ const llgoWasiThreads = "LLGO_WASI_THREADS" const llgoStdioNobuf = "LLGO_STDIO_NOBUF" const llgoFullRpath = "LLGO_FULL_RPATH" const llgoBuildCache = "LLGO_BUILD_CACHE" +const llgoShadowStack = "LLGO_SHADOW_STACK" // for Plan9 asm translation debug const llgoPlan9ASMPkgs = "LLGO_PLAN9ASM_PKGS" diff --git a/internal/build/build_test.go b/internal/build/build_test.go index e96bfc870d..5d67a571b2 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -168,6 +168,54 @@ func TestInvocationUsesExplicitWorkingDirectory(t *testing.T) { pkgs[0].LPkg.Prog.Dispose() } +func TestConcurrentInvocationsIsolateFrontendOptions(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/frontend\n\ngo 1.24\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "frontend.go"), []byte("package frontend\n\nfunc F(v int) int { return v + 1 }\n"), 0o644); err != nil { + t.Fatal(err) + } + t.Setenv(llgoBuildCache, "0") + + type result struct { + debug bool + pkgs []Package + err error + } + results := make(chan result, 2) + for _, debug := range []bool{false, true} { + conf := NewDefaultConf(ModeGen) + conf.LinkOptions.DWARF = DWARFOmit + if debug { + conf.LinkOptions.DWARF = DWARFPreserve + conf.Verbose = true + } + go func() { + pkgs, err := Build(Invocation{ + Args: []string{"."}, + Config: conf, + Dir: dir, + }) + results <- result{debug: debug, pkgs: pkgs, err: err} + }() + } + for range 2 { + got := <-results + if got.err != nil { + t.Fatal(got.err) + } + if len(got.pkgs) != 1 || got.pkgs[0].LPkg == nil { + t.Fatalf("Build returned packages = %+v, want one compiled package", got.pkgs) + } + t.Cleanup(got.pkgs[0].LPkg.Prog.Dispose) + hasDebugInfo := strings.Contains(got.pkgs[0].LPkg.String(), "!llvm.dbg.cu") + if hasDebugInfo != got.debug { + t.Fatalf("debug=%v produced hasDebugInfo=%v", got.debug, hasDebugInfo) + } + } +} + func TestResolveOutputsUsesInvocationDirectory(t *testing.T) { dir := t.TempDir() out := &OutFmtDetails{