From 3e0c6459a8b0e10024a7b2abb2599ac52b5c5679 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 6 Jul 2026 22:19:58 +0800 Subject: [PATCH 1/9] cl,ssa,runtime: stack-object finalizer liveness Re-expresses #1906 on the #2023 base (its remaining ~11k diff lines were the pre-#2012 funcinfo draft, superseded by the stage-5 chain): - cl gains a liveness analysis for stack-allocated objects: allocas whose last use has passed are cleared so bdwgc's conservative stack scan stops keeping dead stack objects (and what they point to) alive; pointer registers are clobbered around the trigger points (llgo_clobber_pointer_regs) and dead stack slots holding the target are zeroed (llgo_clear_stack_ptr, pthread stack-bounds walk). - runtime.SetFinalizer paths (mfinal, runtime_gc, bdwgc binding) hook the cleared-slot machinery so finalizers for dead stack objects run. - xfail: retire deferfin.go, stackobj.go, stackobj3.go, validated on darwin/arm64 go1.24 + go1.26 (stackobj2 already passed). Carries the #2035 shared-GOCACHE commit temporarily (same patch-id, auto-dedups when the chain rebases after #2035 merges). Supersedes #1906. --- cl/compile.go | 661 ++++++++++++++++ cl/instr.go | 6 + cl/liveness_internal_test.go | 860 +++++++++++++++++++++ runtime/internal/clite/bdwgc/bdwgc.go | 3 + runtime/internal/lib/runtime/mfinal.go | 6 +- runtime/internal/lib/runtime/runtime_gc.go | 2 + ssa/memory.go | 28 + ssa/ssa_test.go | 65 ++ test/go/finalizer_test.go | 112 +++ test/goroot/xfail.yaml | 57 -- 10 files changed, 1740 insertions(+), 60 deletions(-) create mode 100644 cl/liveness_internal_test.go diff --git a/cl/compile.go b/cl/compile.go index 1e8d1e07af..fcea57b37a 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -178,6 +178,12 @@ type context struct { anonDefers map[*ssa.Function]bool debugDIVars map[*types.Var]llssa.DIVar debugAllocVars map[*ssa.Alloc]*types.Var + stackClears map[ssa.Instruction][]*ssa.Alloc + entryClears map[*ssa.BasicBlock][]*ssa.Alloc + loadClears map[ssa.Instruction]bool + callClobbers map[ssa.Instruction]bool + paramClobbers map[ssa.Instruction]bool + paramScans map[ssa.Instruction][]*ssa.Parameter runtimeCallerFuncs map[*ssa.Function]bool pcLineSeq uint64 @@ -634,6 +640,21 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun p.prepareExportedLocalContext(f) p.bvals = make(map[ssa.Value]llssa.Expr) p.methodNilDerefChecks = collectMethodNilDerefChecks(f) + if p.enableConservativeLivenessClears(f) { + p.stackClears = p.collectStackClearPlans(f) + p.entryClears = p.collectEntryClearPlans(f) + p.loadClears = make(map[ssa.Instruction]bool) + p.callClobbers = p.collectCallClobberPlans(f) + p.paramClobbers = p.collectParamClobberPlans(f) + p.paramScans = p.collectParamScanPlans(f) + } else { + p.stackClears = nil + p.entryClears = nil + p.loadClears = nil + p.callClobbers = nil + p.paramClobbers = nil + p.paramScans = nil + } off := make([]int, len(f.Blocks)) if isCgo { p.cgoArgs = make([]llssa.Expr, len(f.Params)) @@ -847,6 +868,7 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do if block.Index == 0 { p.enterExportedLocalContext(b) } + p.clearEntryAllocs(b, block) if block.Index == 0 && p.shouldTrackCallerFrames() { p.pushCallerLocationFrame(b, block.Parent()) } @@ -885,6 +907,9 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do fnOld := pkg.NewFunc(initFnNameOld, llssa.NoArgsNoRet, llssa.InC) b.Call(fnOld.Expr) } + if !(isCgoCfunc || isCgoC2 || isCgoCmacro) && p.shouldSkipLateSetFinalizerValue(instr) { + continue + } if isCgoCfunc || isCgoC2 || isCgoCmacro { switch instr := instr.(type) { case *ssa.Alloc: @@ -923,6 +948,17 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do } else { p.compileInstr(b, instr) } + if isTerminatingInstruction(instr) { + continue + } + p.clearDeadAllocs(b, instr) + if p.callClobbers[instr] { + p.clobberPointerRegs(b) + } + p.scanParamPointers(b, instr) + if p.paramClobbers[instr] { + p.clobberPointerRegs(b) + } } // is cgo cfunc but not return yet, some funcs has multiple blocks if (isCgoCfunc || isCgoC2 || isCgoCmacro) && !cgoReturned { @@ -1155,6 +1191,623 @@ func isAllocVargs(ctx *context, v *ssa.Alloc) bool { return false } +func (p *context) enableConservativeLivenessClears(fn *ssa.Function) bool { + if fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + return false + } + path := fn.Pkg.Pkg.Path() + if path == "command-line-arguments" { + return p.packageUsesRuntimeSetFinalizer(fn.Pkg) + } + return false +} + +func (p *context) packageUsesRuntimeSetFinalizer(pkg *ssa.Package) bool { + for _, member := range pkg.Members { + fn, ok := member.(*ssa.Function) + if ok && p.functionUsesRuntimeSetFinalizer(fn, map[*ssa.Function]bool{}) { + return true + } + } + return false +} + +func (p *context) functionUsesRuntimeSetFinalizer(fn *ssa.Function, seen map[*ssa.Function]bool) bool { + if fn == nil || seen[fn] { + return false + } + seen[fn] = true + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + switch instr := instr.(type) { + case *ssa.Call: + if p.isRuntimeSetFinalizerCall(&instr.Call) { + return true + } + case *ssa.Defer: + if p.isRuntimeSetFinalizerCall(&instr.Call) { + return true + } + case *ssa.Go: + if p.isRuntimeSetFinalizerCall(&instr.Call) { + return true + } + } + } + } + for _, anon := range fn.AnonFuncs { + if p.functionUsesRuntimeSetFinalizer(anon, seen) { + return true + } + } + return false +} + +func hasConservativeGCPointers(t types.Type, seen map[types.Type]bool) bool { + if t == nil { + return false + } + t = types.Unalias(t) + if seen[t] { + return false + } + seen[t] = true + switch t := t.Underlying().(type) { + case *types.Pointer, *types.Slice, *types.Map, *types.Chan, *types.Signature, *types.Interface: + return true + case *types.Basic: + return t.Kind() == types.String || t.Kind() == types.UnsafePointer + case *types.Array: + return hasConservativeGCPointers(t.Elem(), seen) + case *types.Struct: + for i := 0; i < t.NumFields(); i++ { + if hasConservativeGCPointers(t.Field(i).Type(), seen) { + return true + } + } + } + return false +} + +func (p *context) shouldClearAlloc(v *ssa.Alloc) bool { + if v == nil || v.Comment == "varargs" || v.Comment == "makeslice" { + return false + } + ptr, ok := v.Type().Underlying().(*types.Pointer) + return ok && hasConservativeGCPointers(ptr.Elem(), map[types.Type]bool{}) +} + +func blockCanReach(from, to *ssa.BasicBlock, seen map[*ssa.BasicBlock]bool) bool { + if from == nil || to == nil { + return false + } + if from == to { + return true + } + if seen[from] { + return false + } + seen[from] = true + for _, succ := range from.Succs { + if blockCanReach(succ, to, seen) { + return true + } + } + return false +} + +func refBlock(ref ssa.Instruction) *ssa.BasicBlock { + if ref == nil { + return nil + } + return ref.Block() +} + +func instructionUsesValue(instr ssa.Instruction, v ssa.Value) bool { + if instr == nil || v == nil { + return false + } + for _, operand := range instr.Operands(nil) { + if operand != nil && *operand == v { + return true + } + } + return false +} + +func isCallLikeInstruction(instr ssa.Instruction) bool { + switch instr.(type) { + case *ssa.Call, *ssa.Defer, *ssa.Go: + return true + } + return false +} + +func isTerminatingInstruction(instr ssa.Instruction) bool { + switch instr.(type) { + case *ssa.Jump, *ssa.Return, *ssa.If, *ssa.Panic: + return true + } + return false +} + +func (p *context) isRuntimeSetFinalizerCall(call *ssa.CallCommon) bool { + if call == nil { + return false + } + fn, ok := call.Value.(*ssa.Function) + if !ok || fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + return false + } + return fn.Name() == "SetFinalizer" && + fn.Pkg.Pkg.Path() == "github.com/goplus/llgo/runtime/internal/lib/runtime" +} + +func (p *context) isOnlyRuntimeSetFinalizerArg(v ssa.Value) bool { + refs := v.Referrers() + if refs == nil || len(*refs) != 1 { + return false + } + call, ok := (*refs)[0].(*ssa.Call) + return ok && p.isRuntimeSetFinalizerCall(&call.Call) +} + +func (p *context) shouldSkipLateSetFinalizerValue(instr ssa.Instruction) bool { + switch instr := instr.(type) { + case *ssa.MakeInterface: + return p.isOnlyRuntimeSetFinalizerArg(instr) + case *ssa.UnOp: + if instr.Op != token.MUL { + return false + } + refs := instr.Referrers() + if refs == nil || len(*refs) != 1 { + return false + } + mi, ok := (*refs)[0].(*ssa.MakeInterface) + return ok && p.isOnlyRuntimeSetFinalizerArg(mi) + } + return false +} + +func (p *context) collectValueUseBlocks(v ssa.Value, blocks map[*ssa.BasicBlock]bool, seen map[ssa.Value]bool, followPhi bool) bool { + if v == nil || seen[v] { + return true + } + seen[v] = true + refs := v.Referrers() + if refs == nil { + return true + } + for _, ref := range *refs { + switch ref := ref.(type) { + case *ssa.DebugRef: + continue + case *ssa.FieldAddr, *ssa.IndexAddr, *ssa.ChangeType, *ssa.Convert, *ssa.MakeInterface: + refVal, ok := ref.(ssa.Value) + if !ok { + return false + } + if !p.collectValueUseBlocks(refVal, blocks, seen, followPhi) { + return false + } + case *ssa.UnOp: + if ref.Op != token.MUL || ref.X != v { + blk := refBlock(ref) + if blk == nil { + return false + } + blocks[blk] = true + continue + } + blk := refBlock(ref) + if blk == nil { + return false + } + blocks[blk] = true + if !p.collectValueUseBlocks(ref, blocks, seen, followPhi) { + return false + } + case *ssa.Phi: + if followPhi { + if !p.collectValueUseBlocks(ref, blocks, seen, followPhi) { + return false + } + continue + } + for i, edge := range ref.Edges { + if edge == v && i < len(ref.Block().Preds) { + blocks[ref.Block().Preds[i]] = true + } + } + default: + instr, ok := ref.(ssa.Instruction) + if !ok || !instructionUsesValue(instr, v) { + return false + } + blk := refBlock(instr) + if blk == nil { + return false + } + blocks[blk] = true + } + } + return true +} + +func (p *context) valueLastUseBlock(v ssa.Value) (*ssa.BasicBlock, bool) { + blocks := make(map[*ssa.BasicBlock]bool) + if !p.collectValueUseBlocks(v, blocks, map[ssa.Value]bool{}, true) { + return nil, false + } + if len(blocks) == 0 { + return nil, true + } + for candidate := range blocks { + ok := true + for blk := range blocks { + if blk != candidate && !blockCanReach(blk, candidate, map[*ssa.BasicBlock]bool{}) { + ok = false + break + } + } + if ok { + return candidate, true + } + } + return nil, false +} + +func (p *context) lastUseInBlock(v ssa.Value, blk *ssa.BasicBlock, order map[ssa.Instruction]int, seen map[ssa.Value]bool) (ssa.Instruction, bool) { + if v == nil || seen[v] { + return nil, true + } + seen[v] = true + refs := v.Referrers() + if refs == nil { + return nil, true + } + var last ssa.Instruction + updateLast := func(instr ssa.Instruction) { + if instr == nil { + return + } + if last == nil || order[instr] > order[last] { + last = instr + } + } + refBeforeBlock := func(refBlk *ssa.BasicBlock) bool { + return refBlk != nil && blk != nil && refBlk != blk && blockCanReach(refBlk, blk, map[*ssa.BasicBlock]bool{}) + } + for _, ref := range *refs { + switch ref := ref.(type) { + case *ssa.DebugRef: + continue + case *ssa.FieldAddr, *ssa.IndexAddr, *ssa.ChangeType, *ssa.Convert, *ssa.MakeInterface: + refVal := ref.(ssa.Value) + refInstr := ref.(ssa.Instruction) + if refInstr.Block() != blk { + if refBeforeBlock(refInstr.Block()) { + continue + } + return nil, false + } + use, ok := p.lastUseInBlock(refVal, blk, order, seen) + if !ok { + return nil, false + } + updateLast(use) + case *ssa.UnOp: + if ref.Op != token.MUL || ref.X != v { + if ref.Block() != blk { + if refBeforeBlock(ref.Block()) { + continue + } + return nil, false + } + updateLast(ref) + continue + } + if ref.Block() != blk { + if refBeforeBlock(ref.Block()) { + continue + } + return nil, false + } + use, ok := p.lastUseInBlock(ref, blk, order, seen) + if !ok { + return nil, false + } + if use != nil { + if isCallLikeInstruction(use) { + updateLast(ref) + continue + } + updateLast(use) + } else { + updateLast(ref) + } + case *ssa.Phi: + use, ok := p.lastUseInBlock(ref, blk, order, seen) + if !ok { + return nil, false + } + updateLast(use) + default: + instr, ok := ref.(ssa.Instruction) + if !ok || !instructionUsesValue(instr, v) { + return nil, false + } + if instr.Block() != blk { + if refBeforeBlock(instr.Block()) { + continue + } + return nil, false + } + updateLast(instr) + } + } + return last, true +} + +func (p *context) collectStackClearPlans(fn *ssa.Function) map[ssa.Instruction][]*ssa.Alloc { + plans := make(map[ssa.Instruction][]*ssa.Alloc) + for _, blk := range fn.Blocks { + for _, instr := range blk.Instrs { + alloc, ok := instr.(*ssa.Alloc) + if !ok || !p.shouldClearAlloc(alloc) { + continue + } + useBlk, ok := p.valueLastUseBlock(alloc) + if !ok || useBlk == nil { + continue + } + if useBlk != alloc.Block() && alloc.Block().Index != 0 { + continue + } + order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) + for i, useInstr := range useBlk.Instrs { + order[useInstr] = i + } + last, ok := p.lastUseInBlock(alloc, useBlk, order, map[ssa.Value]bool{}) + if ok && last != nil { + plans[last] = append(plans[last], alloc) + } + } + } + return plans +} + +func (p *context) collectEntryClearPlans(fn *ssa.Function) map[*ssa.BasicBlock][]*ssa.Alloc { + plans := make(map[*ssa.BasicBlock][]*ssa.Alloc) + for _, blk := range fn.Blocks { + if blk == nil || len(blk.Succs) < 2 { + continue + } + for _, instr := range blk.Instrs { + alloc, ok := instr.(*ssa.Alloc) + if !ok || !p.shouldClearAlloc(alloc) { + continue + } + useBlocks := make(map[*ssa.BasicBlock]bool) + if !p.collectValueUseBlocks(alloc, useBlocks, map[ssa.Value]bool{}, false) { + continue + } + liveSucc := make(map[*ssa.BasicBlock]bool, len(blk.Succs)) + for _, succ := range blk.Succs { + for useBlk := range useBlocks { + if useBlk == nil { + continue + } + if succ == useBlk || blockCanReach(succ, useBlk, map[*ssa.BasicBlock]bool{}) { + liveSucc[succ] = true + break + } + } + } + if len(liveSucc) == 0 || len(liveSucc) == len(blk.Succs) { + continue + } + for _, succ := range blk.Succs { + if !liveSucc[succ] && len(succ.Preds) == 1 { + plans[succ] = append(plans[succ], alloc) + } + } + } + } + return plans +} + +func (p *context) collectParamClobberPlans(fn *ssa.Function) map[ssa.Instruction]bool { + plans := make(map[ssa.Instruction]bool) + for _, param := range fn.Params { + if !hasConservativeGCPointers(param.Type(), map[types.Type]bool{}) { + continue + } + useBlk, ok := p.valueLastUseBlock(param) + if !ok || useBlk == nil { + continue + } + order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) + for i, useInstr := range useBlk.Instrs { + order[useInstr] = i + } + last, ok := p.lastUseInBlock(param, useBlk, order, map[ssa.Value]bool{}) + if ok && last != nil { + plans[last] = true + } + } + return plans +} + +func (p *context) collectParamScanPlans(fn *ssa.Function) map[ssa.Instruction][]*ssa.Parameter { + plans := make(map[ssa.Instruction][]*ssa.Parameter) + for _, param := range fn.Params { + if !hasConservativeGCPointers(param.Type(), map[types.Type]bool{}) { + continue + } + useBlk, ok := p.valueLastUseBlock(param) + if !ok || useBlk == nil { + continue + } + order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) + for i, useInstr := range useBlk.Instrs { + order[useInstr] = i + } + last, ok := p.lastUseInBlock(param, useBlk, order, map[ssa.Value]bool{}) + if ok && last != nil { + plans[last] = append(plans[last], param) + } + } + return plans +} + +func (p *context) collectCallClobberPlans(fn *ssa.Function) map[ssa.Instruction]bool { + plans := make(map[ssa.Instruction]bool) + for _, blk := range fn.Blocks { + for _, instr := range blk.Instrs { + call, ok := instr.(*ssa.Call) + if !ok { + continue + } + for _, arg := range call.Common().Args { + if hasConservativeGCPointers(arg.Type(), map[types.Type]bool{}) { + plans[instr] = true + break + } + } + } + } + return plans +} + +func (p *context) compileLateValue(b llssa.Builder, v ssa.Value) llssa.Expr { + switch v := v.(type) { + case *ssa.MakeInterface: + t := p.type_(v.Type(), llssa.InGo) + x := p.compileLateValue(b, v.X) + return b.MakeInterface(t, x) + case *ssa.UnOp: + if v.Op != token.MUL { + return p.compileValue(b, v) + } + x := p.compileLateValue(b, v.X) + return b.UnOp(v.Op, x) + case *ssa.FieldAddr: + x := p.compileLateValue(b, v.X) + return b.FieldAddr(x, v.Field) + case *ssa.IndexAddr: + x := p.compileLateValue(b, v.X) + idx := p.compileLateValue(b, v.Index) + return b.IndexAddr(x, idx) + case *ssa.ChangeType: + t := p.type_(v.Type(), llssa.InGo) + x := p.compileLateValue(b, v.X) + return b.ChangeType(t, x) + case *ssa.Convert: + t := p.type_(v.Type(), llssa.InGo) + x := p.compileLateValue(b, v.X) + return b.Convert(t, x) + } + return p.compileValue(b, v) +} + +func (p *context) scanStackPointer(b llssa.Builder, val llssa.Expr) { + b.Pkg.NeedRuntime = true + t := p.type_(types.Typ[types.Uintptr], llssa.InGo) + if !types.Identical(val.RawType(), t.RawType()) { + val = b.Convert(t, val) + } + fn := b.Pkg.NewFunc("llgo_clear_stack_ptr", + types.NewSignatureType(nil, nil, nil, types.NewTuple(types.NewParam(token.NoPos, nil, "target", types.Typ[types.Uintptr])), nil, false), llssa.InC) + b.Call(fn.Expr, val) +} + +func (p *context) scanPointerExpr(b llssa.Builder, val llssa.Expr) { + switch t := types.Unalias(val.RawType()).Underlying().(type) { + case *types.Pointer: + p.scanStackPointer(b, val) + case *types.Struct: + if t.NumFields() == 1 { + if _, ok := types.Unalias(t.Field(0).Type()).Underlying().(*types.Pointer); ok { + p.scanStackPointer(b, b.Field(val, 0)) + } + } + } +} + +func (p *context) scanAllocPointer(b llssa.Builder, ptr llssa.Expr) { + elem := b.Prog.Elem(ptr.Type) + switch t := types.Unalias(elem.RawType()).Underlying().(type) { + case *types.Pointer: + p.scanStackPointer(b, b.Load(ptr)) + case *types.Struct: + if t.NumFields() == 1 { + if _, ok := types.Unalias(t.Field(0).Type()).Underlying().(*types.Pointer); ok { + p.scanStackPointer(b, b.Load(b.FieldAddr(ptr, 0))) + } + } + } +} + +func (p *context) scanParamPointers(b llssa.Builder, instr ssa.Instruction) { + params := p.paramScans[instr] + for _, param := range params { + p.scanPointerExpr(b, p.compileValue(b, param)) + } +} + +func (p *context) clearAlloc(b llssa.Builder, alloc *ssa.Alloc) { + ptr := p.compileValue(b, alloc) + b.IfThen(b.BinOp(token.NEQ, ptr, p.prog.Zero(ptr.Type)), func() { + p.scanAllocPointer(b, ptr) + elem := b.Prog.Elem(ptr.Type) + b.Store(ptr, p.prog.Zero(elem)) + }) +} + +func (p *context) clearDeadAllocs(b llssa.Builder, instr ssa.Instruction) { + if p.loadClears[instr] { + return + } + allocs := p.stackClears[instr] + if len(allocs) == 0 { + return + } + for _, alloc := range allocs { + p.clearAlloc(b, alloc) + } + if unop, ok := instr.(*ssa.UnOp); ok && unop.Op == token.MUL { + return + } + p.clobberPointerRegs(b) +} + +func (p *context) clearEntryAllocs(b llssa.Builder, block *ssa.BasicBlock) { + allocs := p.entryClears[block] + if len(allocs) == 0 { + return + } + for _, alloc := range allocs { + p.clearAlloc(b, alloc) + } + p.clobberPointerRegs(b) +} + +func (p *context) clobberPointerRegs(b llssa.Builder) { + b.Pkg.NeedRuntime = true + uintptrParam := func(name string) *types.Var { + return types.NewParam(token.NoPos, nil, name, types.Typ[types.Uintptr]) + } + fn := b.Pkg.NewFunc("llgo_clobber_pointer_regs", + types.NewSignatureType(nil, nil, nil, types.NewTuple( + uintptrParam("a0"), uintptrParam("a1"), uintptrParam("a2"), uintptrParam("a3"), + uintptrParam("a4"), uintptrParam("a5"), uintptrParam("a6"), uintptrParam("a7"), + ), nil, false), llssa.InC) + zero := b.Prog.IntVal(0, b.Prog.Uintptr()) + b.Call(fn.Expr, zero, zero, zero, zero, zero, zero, zero, zero) +} + func isPhi(i ssa.Instruction) bool { _, ok := i.(*ssa.Phi) return ok @@ -1290,6 +1943,14 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue return ret } } + if len(p.stackClears[v]) > 0 { + x := p.compileValue(b, v.X) + if ret, ok := b.LoadAndClearSinglePointer(x); ok { + p.loadClears[v] = true + p.bvals[iv] = ret + return ret + } + } } x := p.compileValue(b, v.X) if v.Op != token.ARROW { diff --git a/cl/instr.go b/cl/instr.go index 678c4afc6d..ec14a11642 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -1998,6 +1998,12 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm ret = p.emitDo(b, act, ds, llssa.Builtin(fn), llssa.Builder.Call, args...) case *ssa.Function: aFn, pyFn, ftype := p.compileFunction(cv) + if p.isRuntimeSetFinalizerCall(call) && len(args) == 2 && act == llssa.Call && ds == nil { + finalizer := p.compileLateValue(b, args[1]) + obj := p.compileLateValue(b, args[0]) + ret = p.emitDo(b, act, nil, aFn.Expr, llssa.Builder.Call, obj, finalizer) + return + } // TODO(xsw): check ca != llssa.Call switch ftype { case cFunc: diff --git a/cl/liveness_internal_test.go b/cl/liveness_internal_test.go new file mode 100644 index 0000000000..9e142e0d25 --- /dev/null +++ b/cl/liveness_internal_test.go @@ -0,0 +1,860 @@ +//go:build !llgo +// +build !llgo + +package cl + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/gogen/packages" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +func buildSSAPackageWithPath(t *testing.T, pkgPath, pkgName, src string) *ssa.Package { + t.Helper() + ssapkg, _ := buildSSAPackageWithPathAndFiles(t, pkgPath, pkgName, src) + return ssapkg +} + +func buildSSAPackageWithPathAndFiles(t *testing.T, pkgPath, pkgName, src string) (*ssa.Package, []*ast.File) { + t.Helper() + return buildSSAPackageWithPathAndFilesMode(t, pkgPath, pkgName, src, ssa.SanityCheckFunctions|ssa.InstantiateGenerics) +} + +func buildSSAPackageWithPathAndFilesMode(t *testing.T, pkgPath, pkgName, src string, mode ssa.BuilderMode) (*ssa.Package, []*ast.File) { + t.Helper() + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p.go", src, 0) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{f} + pkg := types.NewPackage(pkgPath, pkgName) + imp := packages.NewImporter(fset) + ssapkg, _, err := ssautil.BuildPackage(&types.Config{Importer: imp}, fset, pkg, files, mode) + if err != nil { + t.Fatal(err) + } + return ssapkg, files +} + +func TestConservativeGCPointerTypeAnalysis(t *testing.T) { + if hasConservativeGCPointers(nil, map[types.Type]bool{}) { + t.Fatal("nil type should not report conservative pointers") + } + if hasConservativeGCPointers(types.Typ[types.Int], map[types.Type]bool{}) { + t.Fatal("int should not report conservative pointers") + } + if hasConservativeGCPointers(types.Typ[types.String], map[types.Type]bool{types.Typ[types.String]: true}) { + t.Fatal("seen type should short-circuit") + } + for _, typ := range []types.Type{ + types.Typ[types.String], + types.Typ[types.UnsafePointer], + types.NewPointer(types.Typ[types.Int]), + types.NewSlice(types.Typ[types.Int]), + types.NewMap(types.Typ[types.String], types.Typ[types.Int]), + types.NewChan(types.SendRecv, types.Typ[types.Int]), + types.NewSignatureType(nil, nil, nil, nil, nil, false), + types.NewInterfaceType(nil, nil), + types.NewArray(types.NewPointer(types.Typ[types.Int]), 2), + types.NewStruct([]*types.Var{types.NewField(token.NoPos, nil, "p", types.NewPointer(types.Typ[types.Int]), false)}, nil), + } { + if !hasConservativeGCPointers(typ, map[types.Type]bool{}) { + t.Fatalf("%v should report conservative pointers", typ) + } + } + if hasConservativeGCPointers(types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, nil, "i", types.Typ[types.Int], false), + }, nil), map[types.Type]bool{}) { + t.Fatal("struct without pointer fields should not report conservative pointers") + } + if hasConservativeGCPointers(types.NewArray(types.Typ[types.Int], 2), map[types.Type]bool{}) { + t.Fatal("array without pointer elements should not report conservative pointers") + } + if !hasConservativeGCPointers(types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, nil, "i", types.Typ[types.Int], false), + types.NewField(token.NoPos, nil, "p", types.NewPointer(types.Typ[types.Int]), false), + }, nil), map[types.Type]bool{}) { + t.Fatal("struct with later pointer field should report conservative pointers") + } +} + +func TestShouldClearAlloc(t *testing.T) { + ssapkg := buildSSAPackageWithPath(t, "example.com/live", "live", `package live + +type Box struct{ p *int } + +var Sink any + +func allocs(p *int) { + var box Box + var i int + box.p = p + Sink = &box + Sink = &i +} + `) + fn := ssapkg.Func("allocs") + ctx := &context{} + if ctx.shouldClearAlloc(nil) { + t.Fatal("nil alloc should not be cleared") + } + + var boxAlloc, intAlloc *ssa.Alloc + for _, local := range functionAllocs(fn) { + ptr := local.Type().Underlying().(*types.Pointer) + if _, ok := ptr.Elem().Underlying().(*types.Struct); ok { + boxAlloc = local + } + if ptr.Elem() == types.Typ[types.Int] { + intAlloc = local + } + } + if boxAlloc == nil || intAlloc == nil { + var dump strings.Builder + fn.WriteTo(&dump) + t.Fatalf("missing expected allocs: %v\n%s", functionAllocs(fn), dump.String()) + } + if !ctx.shouldClearAlloc(boxAlloc) { + t.Fatal("struct containing a pointer should be cleared") + } + if ctx.shouldClearAlloc(intAlloc) { + t.Fatal("int alloc should not be cleared") + } + + boxAlloc.Comment = "varargs" + if ctx.shouldClearAlloc(boxAlloc) { + t.Fatal("varargs alloc should not be cleared") + } + boxAlloc.Comment = "makeslice" + if ctx.shouldClearAlloc(boxAlloc) { + t.Fatal("synthetic makeslice alloc should not be cleared") + } +} + +func functionAllocs(fn *ssa.Function) []*ssa.Alloc { + seen := make(map[*ssa.Alloc]bool) + var allocs []*ssa.Alloc + add := func(alloc *ssa.Alloc) { + if alloc != nil && !seen[alloc] { + seen[alloc] = true + allocs = append(allocs, alloc) + } + } + for _, local := range fn.Locals { + add(local) + } + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + if alloc, ok := instr.(*ssa.Alloc); ok { + add(alloc) + } + } + } + return allocs +} + +func TestRuntimeSetFinalizerDetection(t *testing.T) { + ssapkg := buildSSAPackageWithPath(t, "github.com/goplus/llgo/runtime/livetest", "livetest", `package livetest + +import rt "github.com/goplus/llgo/runtime/internal/lib/runtime" + +func direct(p *int) { + rt.SetFinalizer(p, func(*int) {}) +} + +func deferred(p *int) { + defer rt.SetFinalizer(p, nil) +} + +func goroutine(p *int) { + go rt.SetFinalizer(p, nil) +} + +func nested(p *int) { + func() { + rt.SetFinalizer(p, nil) + }() +} + +func none(p *int) {} +`) + ctx := &context{} + if ctx.enableConservativeLivenessClears(nil) { + t.Fatal("nil function should not enable conservative clears") + } + for _, name := range []string{"direct", "deferred", "goroutine", "nested"} { + if !ctx.functionUsesRuntimeSetFinalizer(ssapkg.Func(name), map[*ssa.Function]bool{}) { + t.Fatalf("%s should be detected as SetFinalizer user", name) + } + } + if ctx.functionUsesRuntimeSetFinalizer(nil, map[*ssa.Function]bool{}) { + t.Fatal("nil function should not use SetFinalizer") + } + direct := ssapkg.Func("direct") + if ctx.functionUsesRuntimeSetFinalizer(direct, map[*ssa.Function]bool{direct: true}) { + t.Fatal("seen function should short-circuit") + } + if ctx.functionUsesRuntimeSetFinalizer(ssapkg.Func("none"), map[*ssa.Function]bool{}) { + t.Fatal("none should not use SetFinalizer") + } + if ctx.packageUsesRuntimeSetFinalizer(&ssa.Package{Members: map[string]ssa.Member{"none": ssapkg.Func("none")}}) { + t.Fatal("package without SetFinalizer should not report use") + } + if !ctx.packageUsesRuntimeSetFinalizer(ssapkg) { + t.Fatal("package should report SetFinalizer use") + } + if ctx.enableConservativeLivenessClears(direct) { + t.Fatal("non command-line-arguments package should not enable conservative clears") + } + ssapkg.Pkg = types.NewPackage("command-line-arguments", "main") + if !ctx.enableConservativeLivenessClears(direct) { + t.Fatal("command-line-arguments package with SetFinalizer should enable conservative clears") + } +} + +func TestRuntimeSetFinalizerLateValueSkips(t *testing.T) { + ssapkg := buildSSAPackageWithPath(t, "github.com/goplus/llgo/runtime/livetest", "livetest", `package livetest + +import rt "github.com/goplus/llgo/runtime/internal/lib/runtime" + +func direct(p *int) { + rt.SetFinalizer(p, func(*int) {}) +} +`) + ctx := &context{} + fn := ssapkg.Func("direct") + var makeIface *ssa.MakeInterface + var deref *ssa.UnOp + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + switch instr := instr.(type) { + case *ssa.MakeInterface: + makeIface = instr + case *ssa.UnOp: + if instr.Op == token.MUL { + deref = instr + } + } + } + } + if makeIface == nil { + t.Fatal("missing MakeInterface for SetFinalizer argument") + } + if ctx.isRuntimeSetFinalizerCall(nil) { + t.Fatal("nil call should not be SetFinalizer") + } + if !ctx.shouldSkipLateSetFinalizerValue(makeIface) { + t.Fatal("SetFinalizer-only MakeInterface should be skipped") + } + if deref != nil && !ctx.shouldSkipLateSetFinalizerValue(deref) { + t.Fatal("SetFinalizer-only deref should be skipped") + } + if ctx.shouldSkipLateSetFinalizerValue(&ssa.Return{}) { + t.Fatal("unrelated instruction should not be skipped") + } + if ctx.shouldSkipLateSetFinalizerValue(&ssa.UnOp{Op: token.SUB}) { + t.Fatal("non-deref unary op should not be skipped") + } +} + +func TestConservativeLivenessPlanCollectors(t *testing.T) { + ssapkg := buildSSAPackageWithPath(t, "example.com/live", "live", `package live + +type Box struct{ p *int } + +var Sink any + +func linear(p *int) { + var box Box + box.p = p + Sink = box.p + Sink = 1 +} + +func branch(p *int, cond bool) { + var box Box + box.p = p + if cond { + Sink = box.p + } else { + Sink = 0 + } + Sink = 1 +} + +func branchBoth(p *int, cond bool) { + var box Box + box.p = p + if cond { + Sink = box.p + } else { + Sink = box.p + } + Sink = 1 +} + +func paramUse(p *int) { + Sink = p + Sink = 1 +} + +func splitParam(p *int, cond bool) { + if cond { + Sink = p + } else { + Sink = p + } + Sink = 1 +} + +func takes(*int) {} + +func callWithPointer(p *int) { + takes(p) + Sink = 1 +} + +func callWithInt(i int) { + Sink = i +} +`) + ctx := &context{} + linear := ssapkg.Func("linear") + stackPlans := ctx.collectStackClearPlans(linear) + if len(stackPlans) == 0 { + t.Fatal("linear should produce stack clear plans") + } + for instr := range stackPlans { + if isTerminatingInstruction(instr) { + t.Fatalf("stack clear should not be scheduled after terminator %T", instr) + } + } + + entryPlans := ctx.collectEntryClearPlans(ssapkg.Func("branch")) + if len(entryPlans) == 0 { + t.Fatal("branch should produce entry clear plans for dead successor") + } + if got := ctx.collectEntryClearPlans(ssapkg.Func("branchBoth")); len(got) != 0 { + t.Fatalf("branchBoth should not clear values live in both successors: %v", got) + } + + paramFn := ssapkg.Func("paramUse") + if len(ctx.collectParamClobberPlans(paramFn)) == 0 { + t.Fatal("paramUse should produce param clobber plans") + } + if len(ctx.collectParamScanPlans(paramFn)) == 0 { + t.Fatal("paramUse should produce param scan plans") + } + splitParam := ssapkg.Func("splitParam") + if got := ctx.collectParamClobberPlans(splitParam); len(got) != 0 { + t.Fatalf("splitParam has no single last-use block, got clobbers: %v", got) + } + if got := ctx.collectParamScanPlans(splitParam); len(got) != 0 { + t.Fatalf("splitParam has no single last-use block, got scans: %v", got) + } + if len(ctx.collectCallClobberPlans(ssapkg.Func("callWithPointer"))) == 0 { + t.Fatal("pointer call should clobber pointer regs") + } + if len(ctx.collectCallClobberPlans(ssapkg.Func("callWithInt"))) != 0 { + t.Fatal("int-only call should not clobber pointer regs") + } +} + +func TestConservativeLivenessGraphHelpers(t *testing.T) { + ssapkg := buildSSAPackageWithPath(t, "example.com/live", "live", `package live + +import "unsafe" + +var Sink any + +type Box struct{ p *int } + +func flow(p *int, cond bool) { + if cond { + Sink = p + } else { + Sink = 0 + } +} + +func split(p *int, cond bool) { + if cond { + Sink = p + } else { + Sink = p + } +} + +func target(*int) {} + +func withCall(p *int) { + target(p) +} + +func refs(p *int, arr *[2]*int, box *Box, cond bool) *int { + var q *int + if cond { + q = p + } else { + q = box.p + } + Sink = arr[0] + Sink = q + return q +} + +func converted(p *int) unsafe.Pointer { + return unsafe.Pointer(p) +} + `) + fn := ssapkg.Func("flow") + if blockCanReach(nil, fn.Blocks[0], map[*ssa.BasicBlock]bool{}) { + t.Fatal("nil block should not reach anything") + } + if !blockCanReach(fn.Blocks[0], fn.Blocks[0], map[*ssa.BasicBlock]bool{}) { + t.Fatal("block should reach itself") + } + if instructionUsesValue(nil, fn.Params[0]) { + t.Fatal("nil instruction should not use values") + } + if instructionUsesValue(fn.Blocks[0].Instrs[0], nil) { + t.Fatal("nil value should not be used") + } + if isCallLikeInstruction(fn.Blocks[0].Instrs[0]) { + t.Fatal("if instruction should not be call-like") + } + if !isTerminatingInstruction(fn.Blocks[0].Instrs[len(fn.Blocks[0].Instrs)-1]) { + t.Fatal("entry block should end with a terminator") + } + + ctx := &context{} + if blk := refBlock(nil); blk != nil { + t.Fatalf("refBlock(nil) = %v", blk) + } + blocks := make(map[*ssa.BasicBlock]bool) + if !ctx.collectValueUseBlocks(nil, blocks, map[ssa.Value]bool{}, false) { + t.Fatal("nil collectValueUseBlocks should succeed") + } + if !ctx.collectValueUseBlocks(fn.Params[0], blocks, map[ssa.Value]bool{fn.Params[0]: true}, false) { + t.Fatal("seen collectValueUseBlocks should succeed") + } + if !ctx.collectValueUseBlocks(fn.Params[0], blocks, map[ssa.Value]bool{}, false) { + t.Fatal("collectValueUseBlocks failed") + } + if len(blocks) == 0 { + t.Fatal("expected use blocks for parameter") + } + if blk, ok := ctx.valueLastUseBlock(fn.Params[0]); !ok || blk == nil { + t.Fatalf("valueLastUseBlock = %v, %v", blk, ok) + } + if blk, ok := ctx.valueLastUseBlock(nil); !ok || blk != nil { + t.Fatalf("valueLastUseBlock(nil) = %v, %v", blk, ok) + } + if last, ok := ctx.lastUseInBlock(nil, fn.Blocks[0], map[ssa.Instruction]int{}, map[ssa.Value]bool{}); !ok || last != nil { + t.Fatalf("lastUseInBlock(nil) = %v, %v", last, ok) + } + split := ssapkg.Func("split") + if blk, ok := ctx.valueLastUseBlock(split.Params[0]); ok || blk != nil { + t.Fatalf("valueLastUseBlock(split param) = %v, %v; want no single block", blk, ok) + } + entryOrder := make(map[ssa.Instruction]int, len(split.Blocks[0].Instrs)) + for i, instr := range split.Blocks[0].Instrs { + entryOrder[instr] = i + } + if last, ok := ctx.lastUseInBlock(split.Params[0], split.Blocks[0], entryOrder, map[ssa.Value]bool{}); ok || last != nil { + t.Fatalf("lastUseInBlock(split param in entry) = %v, %v; want failure outside block", last, ok) + } + + var callLike int + for _, block := range ssapkg.Func("withCall").Blocks { + for _, instr := range block.Instrs { + if isCallLikeInstruction(instr) { + callLike++ + } + } + } + if callLike == 0 { + t.Fatal("flow should include at least one call-like instruction") + } + + refs := ssapkg.Func("refs") + var lastUseCount int + for _, param := range refs.Params { + blocks := make(map[*ssa.BasicBlock]bool) + if !ctx.collectValueUseBlocks(param, blocks, map[ssa.Value]bool{}, true) { + t.Fatalf("collectValueUseBlocks failed for %s", param.Name()) + } + if len(blocks) == 0 { + t.Fatalf("expected use blocks for %s", param.Name()) + } + blk, ok := ctx.valueLastUseBlock(param) + if !ok || blk == nil { + t.Fatalf("valueLastUseBlock(%s) = %v, %v", param.Name(), blk, ok) + } + order := make(map[ssa.Instruction]int, len(blk.Instrs)) + for i, instr := range blk.Instrs { + order[instr] = i + } + if last, ok := ctx.lastUseInBlock(param, blk, order, map[ssa.Value]bool{}); !ok { + t.Fatalf("lastUseInBlock(%s) = %v, %v", param.Name(), last, ok) + } else if last != nil { + lastUseCount++ + } + } + if lastUseCount == 0 { + t.Fatal("expected at least one parameter with a concrete last use") + } + phiBlocks := make(map[*ssa.BasicBlock]bool) + if !ctx.collectValueUseBlocks(refs.Params[0], phiBlocks, map[ssa.Value]bool{}, false) { + t.Fatal("non-following phi use collection failed") + } + if len(phiBlocks) == 0 { + t.Fatal("non-following phi use collection should record predecessor blocks") + } + converted := ssapkg.Func("converted") + if !ctx.collectValueUseBlocks(converted.Params[0], make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, true) { + t.Fatal("conversion use collection failed") + } +} + +func TestConservativeLivenessHelperFallbacks(t *testing.T) { + ssapkg := buildSSAPackageWithPath(t, "example.com/live", "live", `package live + +var Sink any + +func branch(cond bool) { + if cond { + Sink = 1 + } else { + Sink = 2 + } +} + +func useOne(p, q *int) { + Sink = p +} + +func neg(i int) int { + return -i +} + +func callDeref(f *func()) { + (*f)() +} + +func derefOnly(p **int) { + _ = *p +} + `) + ctx := &context{} + + branch := ssapkg.Func("branch") + if len(branch.Blocks) < 2 { + t.Fatalf("branch should have successors:\n%s", branch.String()) + } + if blockCanReach(branch.Blocks[0], branch.Blocks[1], map[*ssa.BasicBlock]bool{branch.Blocks[0]: true}) { + t.Fatal("seen entry block should stop reachability recursion") + } + + useOne := ssapkg.Func("useOne") + var useP ssa.Instruction + for _, block := range useOne.Blocks { + for _, instr := range block.Instrs { + if instructionUsesValue(instr, useOne.Params[0]) { + useP = instr + break + } + } + if useP != nil { + break + } + } + if useP == nil { + t.Fatalf("missing instruction that uses p:\n%s", useOne.String()) + } + if instructionUsesValue(useP, useOne.Params[1]) { + t.Fatal("instruction using p should not report use of q") + } + if ctx.isOnlyRuntimeSetFinalizerArg(useOne.Params[1]) { + t.Fatal("unused parameter should not be treated as a SetFinalizer-only argument") + } + if ctx.shouldSkipLateSetFinalizerValue(&ssa.UnOp{Op: token.MUL}) { + t.Fatal("deref without a single MakeInterface referrer should not be skipped") + } + + global := ssapkg.Members["Sink"].(*ssa.Global) + if !ctx.collectValueUseBlocks(global, make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, false) { + t.Fatal("global without referrers should be a valid value-use query") + } + if last, ok := ctx.lastUseInBlock(global, useOne.Blocks[0], map[ssa.Instruction]int{}, map[ssa.Value]bool{}); !ok || last != nil { + t.Fatalf("lastUseInBlock(global) = %v, %v", last, ok) + } + + neg := ssapkg.Func("neg") + var negInstr *ssa.UnOp + for _, block := range neg.Blocks { + for _, instr := range block.Instrs { + if unop, ok := instr.(*ssa.UnOp); ok && unop.Op == token.SUB { + negInstr = unop + break + } + } + if negInstr != nil { + break + } + } + if negInstr == nil { + t.Fatalf("missing unary negation:\n%s", neg.String()) + } + blocks := make(map[*ssa.BasicBlock]bool) + if !ctx.collectValueUseBlocks(neg.Params[0], blocks, map[ssa.Value]bool{}, false) { + t.Fatal("non-deref unary use collection failed") + } + if !blocks[negInstr.Block()] { + t.Fatal("non-deref unary use should record its block") + } + order := make(map[ssa.Instruction]int, len(negInstr.Block().Instrs)) + for i, instr := range negInstr.Block().Instrs { + order[instr] = i + } + if last, ok := ctx.lastUseInBlock(neg.Params[0], negInstr.Block(), order, map[ssa.Value]bool{}); !ok || last != negInstr { + t.Fatalf("lastUseInBlock(neg param) = %v, %v; want unary op", last, ok) + } + + callDeref := ssapkg.Func("callDeref") + var deref *ssa.UnOp + for _, block := range callDeref.Blocks { + for _, instr := range block.Instrs { + if unop, ok := instr.(*ssa.UnOp); ok && unop.Op == token.MUL { + deref = unop + break + } + } + if deref != nil { + break + } + } + if deref == nil { + t.Fatalf("missing call dereference:\n%s", callDeref.String()) + } + order = make(map[ssa.Instruction]int, len(deref.Block().Instrs)) + for i, instr := range deref.Block().Instrs { + order[instr] = i + } + if last, ok := ctx.lastUseInBlock(callDeref.Params[0], deref.Block(), order, map[ssa.Value]bool{}); !ok || last != deref { + t.Fatalf("lastUseInBlock(call deref param) = %v, %v; want deref", last, ok) + } + + derefOnly := ssapkg.Func("derefOnly") + var loneDeref *ssa.UnOp + for _, block := range derefOnly.Blocks { + for _, instr := range block.Instrs { + if unop, ok := instr.(*ssa.UnOp); ok && unop.Op == token.MUL { + loneDeref = unop + break + } + } + if loneDeref != nil { + break + } + } + if loneDeref == nil { + t.Fatalf("missing lone dereference:\n%s", derefOnly.String()) + } + if !ctx.collectValueUseBlocks(derefOnly.Params[0], make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, false) { + t.Fatal("lone deref use collection failed") + } + order = make(map[ssa.Instruction]int, len(loneDeref.Block().Instrs)) + for i, instr := range loneDeref.Block().Instrs { + order[instr] = i + } + if last, ok := ctx.lastUseInBlock(derefOnly.Params[0], loneDeref.Block(), order, map[ssa.Value]bool{}); !ok || last != loneDeref { + t.Fatalf("lastUseInBlock(lone deref param) = %v, %v; want deref", last, ok) + } +} + +func TestConservativeLivenessDebugRefs(t *testing.T) { + ssapkg, _ := buildSSAPackageWithPathAndFilesMode(t, "example.com/live", "live", `package live + +var Sink any + +func use(p *int) { + Sink = p +} + `, ssa.SanityCheckFunctions|ssa.InstantiateGenerics|ssa.GlobalDebug) + + fn := ssapkg.Func("use") + var debugRefs int + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + if _, ok := instr.(*ssa.DebugRef); ok { + debugRefs++ + } + } + } + if debugRefs == 0 { + t.Fatalf("debug SSA package did not contain DebugRef instructions:\n%s", fn.String()) + } + + ctx := &context{} + if !ctx.collectValueUseBlocks(fn.Params[0], make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, false) { + t.Fatal("DebugRef should be ignored while collecting use blocks") + } + order := make(map[ssa.Instruction]int, len(fn.Blocks[0].Instrs)) + for i, instr := range fn.Blocks[0].Instrs { + order[instr] = i + } + if last, ok := ctx.lastUseInBlock(fn.Params[0], fn.Blocks[0], order, map[ssa.Value]bool{}); !ok || last == nil { + t.Fatalf("lastUseInBlock with DebugRef = %v, %v", last, ok) + } +} + +func TestConservativeLivenessScanAllocPointerSlot(t *testing.T) { + prog := newLLSSAProg(t) + pkg := prog.NewPackage("live", "live") + ptrToInt := types.NewPointer(types.Typ[types.Int]) + slotType := types.NewPointer(ptrToInt) + sig := types.NewSignatureType(nil, nil, nil, + types.NewTuple(types.NewParam(token.NoPos, nil, "slot", slotType)), nil, false) + fn := pkg.NewFunc("scanPointerSlot", sig, llssa.InGo) + b := fn.MakeBody(1) + (&context{prog: prog}).scanAllocPointer(b, fn.Param(0)) + b.Return() + b.EndBuild() + + ir := pkg.String() + if !strings.Contains(ir, "llgo_clear_stack_ptr") { + t.Fatalf("pointer slot scan should emit stack clear helper:\n%s", ir) + } +} + +func TestCompileWithoutConservativeLivenessClears(t *testing.T) { + ssapkg, files := buildSSAPackageWithPathAndFiles(t, "command-line-arguments", "main", `package main + +func main() { + x := 1 + _ = &x +} +`) + + prog := newLLSSAProg(t) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + if strings.Contains(pkg.String(), "llgo_clear_stack_ptr") { + t.Fatalf("package without SetFinalizer should not emit liveness clear helpers:\n%s", pkg.String()) + } +} + +func TestCompileConservativeLivenessClears(t *testing.T) { + ssapkg, files := buildSSAPackageWithPathAndFiles(t, "github.com/goplus/llgo/runtime/livetest", "main", `package main + +import rt "github.com/goplus/llgo/runtime/internal/lib/runtime" + +type Box struct{ p *int } + +var Sink any + +func main() { + x := 1 + var box Box + box.p = &x + Sink = box.p + rt.SetFinalizer(&box, func(*Box) {}) + Sink = 1 +} +`) + ssapkg.Pkg = types.NewPackage("command-line-arguments", "main") + + prog := newLLSSAProg(t) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.String() + for _, want := range []string{"llgo_clear_stack_ptr", "llgo_clobber_pointer_regs"} { + if !strings.Contains(ir, want) { + t.Fatalf("compiled liveness module missing %s:\n%s", want, ir) + } + } + if !pkg.NeedRuntime { + t.Fatal("liveness clear helpers should mark runtime as needed") + } +} + +func TestCompileConservativeLivenessStructParamScans(t *testing.T) { + ssapkg, files := buildSSAPackageWithPathAndFiles(t, "github.com/goplus/llgo/runtime/livetest", "main", `package main + +import rt "github.com/goplus/llgo/runtime/internal/lib/runtime" +import "unsafe" + +type Cell struct{ p *int } +type Ptr *int + +var Sink any + +func consume(cell Cell) { + Sink = cell.p + Sink = 1 +} + +func consumePtr(p *int) { + Sink = p + Sink = 1 +} + +func branch(cell Cell, cond bool) { + if cond { + Sink = cell.p + } else { + Sink = 0 + } + Sink = 1 +} + +func main() { + x := 1 + y := 2 + arr := [2]*int{&x, &y} + cell := Cell{p: &x} + p := &x + pp := &p + ptr := Ptr(&x) + rt.SetFinalizer(&cell, func(*Cell) {}) + rt.SetFinalizer(&p, func(**int) {}) + rt.SetFinalizer(*pp, nil) + rt.SetFinalizer(&cell.p, func(**int) {}) + rt.SetFinalizer(&arr[0], func(**int) {}) + rt.SetFinalizer(unsafe.Pointer(&x), nil) + rt.SetFinalizer(ptr, nil) + consume(cell) + consumePtr(p) + branch(cell, x == y) +} + `) + ssapkg.Pkg = types.NewPackage("command-line-arguments", "main") + + prog := newLLSSAProg(t) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.String() + if strings.Count(ir, "llgo_clear_stack_ptr") < 2 { + t.Fatalf("expected stack pointer scans for struct param and local:\n%s", ir) + } + if !strings.Contains(ir, "llgo_clobber_pointer_regs") { + t.Fatalf("compiled liveness module missing clobber helper:\n%s", ir) + } +} diff --git a/runtime/internal/clite/bdwgc/bdwgc.go b/runtime/internal/clite/bdwgc/bdwgc.go index 9f0bec38e6..4a07b2d44b 100644 --- a/runtime/internal/clite/bdwgc/bdwgc.go +++ b/runtime/internal/clite/bdwgc/bdwgc.go @@ -108,6 +108,9 @@ func GetGCNo() uintptr //go:linkname GetHeapUsageSafe C.GC_get_heap_usage_safe func GetHeapUsageSafe(heapSize, freeBytes, unmappedBytes, bytesSinceGC, totalBytes *uintptr) +//go:linkname ClearStack C.GC_clear_stack +func ClearStack(arg c.Pointer) c.Pointer + //go:linkname GetMemoryUse C.GC_get_memory_use func GetMemoryUse() uintptr diff --git a/runtime/internal/lib/runtime/mfinal.go b/runtime/internal/lib/runtime/mfinal.go index a62305c8a2..71afef7b20 100644 --- a/runtime/internal/lib/runtime/mfinal.go +++ b/runtime/internal/lib/runtime/mfinal.go @@ -46,14 +46,14 @@ func initFinalizerState() { } func SetFinalizer(obj any, finalizer any) { - objFace := (*eface)(unsafe.Pointer(&obj)) + objFace := *(*eface)(unsafe.Pointer(&obj)) if objFace._type == nil { throw("runtime.SetFinalizer: first argument is nil") } if objFace._type.Kind() != abi.Pointer { throw("runtime.SetFinalizer: first argument is " + objFace._type.String() + ", not pointer") } - objPtr := ifacePointerData(objFace) + objPtr := ifacePointerData(&objFace) if objPtr == nil { throw("runtime.SetFinalizer: first argument is nil") } @@ -69,7 +69,7 @@ func SetFinalizer(obj any, finalizer any) { } finalizerState.mu.Unlock() - finalizerFace := (*eface)(unsafe.Pointer(&finalizer)) + finalizerFace := *(*eface)(unsafe.Pointer(&finalizer)) if finalizerFace._type == nil { return } diff --git a/runtime/internal/lib/runtime/runtime_gc.go b/runtime/internal/lib/runtime/runtime_gc.go index d8656f93a4..810d076ebd 100644 --- a/runtime/internal/lib/runtime/runtime_gc.go +++ b/runtime/internal/lib/runtime/runtime_gc.go @@ -36,11 +36,13 @@ func ReadMemStats(m *runtime.MemStats) { } func GC() { + bdwgc.ClearStack(nil) bdwgc.Gcollect() runFinalizers() // BDW finalizers are observed on a subsequent collection cycle. // Run one extra cycle so weak-pointer cleanup hooks (unique/weak) see // finalized state before we trigger map cleanup callbacks. + bdwgc.ClearStack(nil) bdwgc.Gcollect() runFinalizers() unique_runtime_notifyMapCleanup() diff --git a/ssa/memory.go b/ssa/memory.go index f88b4826d5..5e6646f87b 100644 --- a/ssa/memory.go +++ b/ssa/memory.go @@ -64,6 +64,34 @@ func (b Builder) aggregateValue(t Type, flds ...llvm.Value) Expr { return Expr{aggregateValue(b.impl, t.ll, flds...), t} } +// LoadAndClearSinglePointer atomically copies a pointer-sized value out of ptr +// and clears the source slot. It handles either *P or *struct{P}. +func (b Builder) LoadAndClearSinglePointer(ptr Expr) (Expr, bool) { + elem := b.Prog.Elem(ptr.Type) + if elem.ll.TypeKind() == llvm.PointerTypeKind { + old := b.loadAndClearPointerWord(ptr.impl, elem.ll) + return Expr{old, elem}, true + } + + st, ok := types.Unalias(elem.RawType()).Underlying().(*types.Struct) + if !ok || st.NumFields() != 1 { + return Nil, false + } + field := b.Prog.rawType(st.Field(0).Type()) + if field.ll.TypeKind() != llvm.PointerTypeKind { + return Nil, false + } + fieldPtr := llvm.CreateStructGEP(b.impl, elem.ll, ptr.impl, 0) + old := b.loadAndClearPointerWord(fieldPtr, field.ll) + return b.aggregateValue(elem, old), true +} + +func (b Builder) loadAndClearPointerWord(ptr llvm.Value, typ llvm.Type) llvm.Value { + old := llvm.CreateLoad(b.impl, typ, ptr) + b.impl.CreateStore(llvm.ConstNull(typ), ptr) + return old +} + func aggregateValue(b llvm.Builder, tll llvm.Type, flds ...llvm.Value) llvm.Value { agg := llvm.Undef(tll) for i, fld := range flds { diff --git a/ssa/ssa_test.go b/ssa/ssa_test.go index 38085a627b..0efa654561 100644 --- a/ssa/ssa_test.go +++ b/ssa/ssa_test.go @@ -1858,6 +1858,71 @@ func TestZeroSizedLoadEmitsNilDerefGuard(t *testing.T) { } } +func TestLoadAndClearSinglePointer(t *testing.T) { + prog := NewProgram(nil) + prog.sizes = types.SizesFor("gc", runtime.GOARCH) + pkg := prog.NewPackage("bar", "foo/bar") + + ptrToInt := types.NewPointer(types.Typ[types.Int]) + wrapStruct := types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, nil, "p", ptrToInt, false), + }, nil) + + params := types.NewTuple( + types.NewVar(0, nil, "p", types.NewPointer(ptrToInt)), + types.NewVar(0, nil, "s", types.NewPointer(wrapStruct)), + ) + results := types.NewTuple( + types.NewVar(0, nil, "", ptrToInt), + types.NewVar(0, nil, "", wrapStruct), + ) + sig := types.NewSignatureType(nil, nil, nil, params, results, false) + fn := pkg.NewFunc("loadAndClear", sig, InGo) + b := fn.MakeBody(1) + pv, ok := b.LoadAndClearSinglePointer(fn.Param(0)) + if !ok { + t.Fatal("pointer slot should be load-and-clearable") + } + sv, ok := b.LoadAndClearSinglePointer(fn.Param(1)) + if !ok { + t.Fatal("single-pointer struct slot should be load-and-clearable") + } + if got, want := sv.impl.Type().String(), sv.Type.ll.String(); got != want { + t.Fatalf("single-pointer struct load-and-clear type = %s, want %s", got, want) + } + b.Return(pv, sv) + b.EndBuild() + + ir := fn.impl.String() + if got := strings.Count(ir, "store ptr null"); got != 2 { + t.Fatalf("LoadAndClearSinglePointer should clear both pointer slots, got %d stores:\n%s", got, ir) + } + if got := strings.Count(ir, "load ptr"); got < 2 { + t.Fatalf("LoadAndClearSinglePointer should load both pointer slots, got %d loads:\n%s", got, ir) + } + + noPtrStruct := types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, nil, "i", types.Typ[types.Int], false), + }, nil) + multiStruct := types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, nil, "p", ptrToInt, false), + types.NewField(token.NoPos, nil, "q", ptrToInt, false), + }, nil) + falseCases := []types.Type{ + types.NewPointer(types.Typ[types.Int]), + types.NewPointer(noPtrStruct), + types.NewPointer(multiStruct), + } + for i, typ := range falseCases { + fn := pkg.NewFunc(fmt.Sprintf("rejectLoadAndClear%d", i), types.NewSignatureType(nil, nil, nil, + types.NewTuple(types.NewVar(0, nil, "p", typ)), nil, false), InGo) + b := fn.MakeBody(1) + if _, ok := b.LoadAndClearSinglePointer(fn.Param(0)); ok { + t.Fatalf("LoadAndClearSinglePointer accepted %v", typ) + } + } +} + func TestTypeAssertSingleElemArrayUsesInsertValue(t *testing.T) { prog := NewProgram(nil) prog.sizes = types.SizesFor("gc", runtime.GOARCH) diff --git a/test/go/finalizer_test.go b/test/go/finalizer_test.go index ec986129d9..bafbc39f8a 100644 --- a/test/go/finalizer_test.go +++ b/test/go/finalizer_test.go @@ -17,6 +17,8 @@ package gotest import ( + "os" + "path/filepath" "runtime" "testing" "time" @@ -94,6 +96,116 @@ func TestRuntimeSetFinalizerCancel(t *testing.T) { } } +const finalizerStackLivenessProbe = `package main + +import ( + "fmt" + "runtime" +) + +type HeapObj [8]int64 + +type StkObj struct { + h *HeapObj +} + +var n int +var c int = -1 +var null StkObj +var sink *HeapObj + +func gc() { + runtime.GC() + runtime.GC() + runtime.GC() + n++ +} + +func keepAliveCase() { + c = -1 + n = 0 + f() + gc() + if c != 1 { + panic(fmt.Sprintf("keepalive collection phase = %d, want 1", c)) + } +} + +func f() { + var s StkObj + s.h = new(HeapObj) + runtime.SetFinalizer(s.h, func(h *HeapObj) { + c = n + }) + g(&s) + gc() +} + +func g(s *StkObj) { + gc() + runtime.KeepAlive(s) + gc() +} + +//go:noinline +func use(p *StkObj) { +} + +//go:noinline +func ambiguousArgCase(s StkObj, b bool) { + var p *StkObj + if b { + p = &s + } else { + p = &null + } + use(p) + gc() + sink = p.h + gc() + sink = nil + gc() +} + +func runAmbiguousArgCase(b bool, want int) { + var s StkObj + s.h = new(HeapObj) + c = -1 + n = 0 + runtime.SetFinalizer(s.h, func(h *HeapObj) { + c = n + }) + ambiguousArgCase(s, b) + if c != want { + panic(fmt.Sprintf("ambiguous arg b=%v collection phase = %d, want %d", b, c, want)) + } +} + +func main() { + keepAliveCase() + runAmbiguousArgCase(true, 2) + runAmbiguousArgCase(false, 0) +} +` + +func TestRuntimeSetFinalizerStackObjectLiveness(t *testing.T) { + dir, err := os.MkdirTemp("", "llgo-finalizer-stack-*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + mainFile := filepath.Join(dir, "main.go") + if err := os.WriteFile(mainFile, []byte(finalizerStackLivenessProbe), 0644); err != nil { + t.Fatal(err) + } + + runGoCmd(t, dir, "run", mainFile) + + root := findLLGoRoot(t) + t.Setenv("LLGO_ROOT", root) + runGoCmd(t, root, "run", "./cmd/llgo", "run", mainFile) +} + func runGCWithTimeout(t *testing.T) { t.Helper() done := make(chan struct{}) diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index cee4671e72..1c7ebb9ce3 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2795,10 +2795,6 @@ xfails: reason: latest main goroot run failure on darwin/arm64 - version: go1.25 platform: darwin/arm64 - directive: run - case: deferfin.go - reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 directive: run case: heapsampling.go reason: latest main goroot run failure on darwin/arm64 @@ -2818,14 +2814,6 @@ xfails: directive: run case: recover4.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: stackobj.go - reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: stackobj3.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/bug347.go @@ -2884,11 +2872,6 @@ xfails: directive: run case: fixedbugs/issue5963.go reason: latest main goroot run failure on darwin/arm64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: deferfin.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -3004,27 +2987,12 @@ xfails: directive: run case: recover4.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: stackobj.go - reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: stackobj3.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run case: tinyfin.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: deferfin.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3085,16 +3053,6 @@ xfails: directive: run case: recover1.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: stackobj.go - reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: stackobj3.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3165,11 +3123,6 @@ xfails: directive: run case: maymorestack.go reason: go1.26 goroot ci-mode run failure on darwin/arm64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: deferfin.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -3185,16 +3138,6 @@ xfails: directive: run case: recover4.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: stackobj.go - reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: stackobj3.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run From f698915acb3abbc79cf2218bd4c82440f2b8d3ee Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 27 Jul 2026 09:28:36 +0800 Subject: [PATCH 2/9] test(cl): cover malformed liveness referrers --- cl/liveness_internal_test.go | 80 ++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/cl/liveness_internal_test.go b/cl/liveness_internal_test.go index 9e142e0d25..c20df9d7b0 100644 --- a/cl/liveness_internal_test.go +++ b/cl/liveness_internal_test.go @@ -683,6 +683,86 @@ func derefOnly(p **int) { } } +func TestConservativeLivenessMalformedReferrersFailClosed(t *testing.T) { + ssapkg := buildSSAPackageWithPath(t, "example.com/live", "live", `package live + +var Sink any + +func use(p *int) { + Sink = p +} +`) + fn := ssapkg.Func("use") + param := fn.Params[0] + ctx := &context{} + + check := func(t *testing.T, ref ssa.Instruction) { + t.Helper() + refs := param.Referrers() + original := append([]ssa.Instruction(nil), (*refs)...) + *refs = []ssa.Instruction{ref} + defer func() { + *refs = original + }() + + if ctx.collectValueUseBlocks(param, make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, true) { + t.Fatal("malformed referrer graph should disable liveness clearing") + } + if block, ok := ctx.valueLastUseBlock(param); ok || block != nil { + t.Fatalf("valueLastUseBlock with malformed referrer = %v, %v; want failure", block, ok) + } + order := make(map[ssa.Instruction]int, len(fn.Blocks[0].Instrs)) + for i, instr := range fn.Blocks[0].Instrs { + order[instr] = i + } + if last, ok := ctx.lastUseInBlock(param, fn.Blocks[0], order, map[ssa.Value]bool{}); ok || last != nil { + t.Fatalf("lastUseInBlock with malformed referrer = %v, %v; want failure", last, ok) + } + } + + t.Run("unary", func(t *testing.T) { + check(t, &ssa.UnOp{Op: token.SUB, X: param}) + }) + t.Run("dereference", func(t *testing.T) { + check(t, &ssa.UnOp{Op: token.MUL, X: param}) + }) + t.Run("return", func(t *testing.T) { + check(t, &ssa.Return{Results: []ssa.Value{param}}) + }) + t.Run("derived-value", func(t *testing.T) { + var derived ssa.Value + for _, ref := range *param.Referrers() { + if value, ok := ref.(*ssa.MakeInterface); ok { + derived = value + break + } + } + if derived == nil { + t.Fatal("missing MakeInterface derived from parameter") + } + refs := derived.Referrers() + original := append([]ssa.Instruction(nil), (*refs)...) + *refs = []ssa.Instruction{&ssa.Return{Results: []ssa.Value{derived}}} + defer func() { + *refs = original + }() + + if ctx.collectValueUseBlocks(param, make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, true) { + t.Fatal("malformed derived referrer graph should disable liveness clearing") + } + if block, ok := ctx.valueLastUseBlock(param); ok || block != nil { + t.Fatalf("valueLastUseBlock with malformed derived referrer = %v, %v; want failure", block, ok) + } + order := make(map[ssa.Instruction]int, len(fn.Blocks[0].Instrs)) + for i, instr := range fn.Blocks[0].Instrs { + order[instr] = i + } + if last, ok := ctx.lastUseInBlock(param, fn.Blocks[0], order, map[ssa.Value]bool{}); ok || last != nil { + t.Fatalf("lastUseInBlock with malformed derived referrer = %v, %v; want failure", last, ok) + } + }) +} + func TestConservativeLivenessDebugRefs(t *testing.T) { ssapkg, _ := buildSSAPackageWithPathAndFilesMode(t, "example.com/live", "live", `package live From 26072550f82fd27ffcc55c0d9cbedcc6514add69 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 29 Jul 2026 12:37:42 +0800 Subject: [PATCH 3/9] cl,runtime: make finalizer liveness clearing fail closed --- cl/compile.go | 547 ++++-------------- cl/instr.go | 6 - cl/liveness_internal_test.go | 491 ++++++++-------- runtime/internal/lib/runtime/mfinal.go | 6 +- ssa/memory.go | 37 +- ssa/ssa_test.go | 83 +-- test/go/finalizer_liveness_regression_test.go | 259 +++++++++ test/go/finalizer_test.go | 112 ---- test/goroot/xfail.yaml | 61 +- 9 files changed, 672 insertions(+), 930 deletions(-) create mode 100644 test/go/finalizer_liveness_regression_test.go diff --git a/cl/compile.go b/cl/compile.go index fcea57b37a..3eb7c59360 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -179,11 +179,7 @@ type context struct { debugDIVars map[*types.Var]llssa.DIVar debugAllocVars map[*ssa.Alloc]*types.Var stackClears map[ssa.Instruction][]*ssa.Alloc - entryClears map[*ssa.BasicBlock][]*ssa.Alloc - loadClears map[ssa.Instruction]bool - callClobbers map[ssa.Instruction]bool - paramClobbers map[ssa.Instruction]bool - paramScans map[ssa.Instruction][]*ssa.Parameter + finalizerPkgUses map[*ssa.Package]bool runtimeCallerFuncs map[*ssa.Function]bool pcLineSeq uint64 @@ -642,18 +638,8 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun p.methodNilDerefChecks = collectMethodNilDerefChecks(f) if p.enableConservativeLivenessClears(f) { p.stackClears = p.collectStackClearPlans(f) - p.entryClears = p.collectEntryClearPlans(f) - p.loadClears = make(map[ssa.Instruction]bool) - p.callClobbers = p.collectCallClobberPlans(f) - p.paramClobbers = p.collectParamClobberPlans(f) - p.paramScans = p.collectParamScanPlans(f) } else { p.stackClears = nil - p.entryClears = nil - p.loadClears = nil - p.callClobbers = nil - p.paramClobbers = nil - p.paramScans = nil } off := make([]int, len(f.Blocks)) if isCgo { @@ -868,7 +854,6 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do if block.Index == 0 { p.enterExportedLocalContext(b) } - p.clearEntryAllocs(b, block) if block.Index == 0 && p.shouldTrackCallerFrames() { p.pushCallerLocationFrame(b, block.Parent()) } @@ -907,9 +892,6 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do fnOld := pkg.NewFunc(initFnNameOld, llssa.NoArgsNoRet, llssa.InC) b.Call(fnOld.Expr) } - if !(isCgoCfunc || isCgoC2 || isCgoCmacro) && p.shouldSkipLateSetFinalizerValue(instr) { - continue - } if isCgoCfunc || isCgoC2 || isCgoCmacro { switch instr := instr.(type) { case *ssa.Alloc: @@ -952,13 +934,6 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do continue } p.clearDeadAllocs(b, instr) - if p.callClobbers[instr] { - p.clobberPointerRegs(b) - } - p.scanParamPointers(b, instr) - if p.paramClobbers[instr] { - p.clobberPointerRegs(b) - } } // is cgo cfunc but not return yet, some funcs has multiple blocks if (isCgoCfunc || isCgoC2 || isCgoCmacro) && !cgoReturned { @@ -1192,24 +1167,80 @@ func isAllocVargs(ctx *context, v *ssa.Alloc) bool { } func (p *context) enableConservativeLivenessClears(fn *ssa.Function) bool { - if fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { + if fn == nil || isCgoExternSymbol(fn) { return false } - path := fn.Pkg.Pkg.Path() - if path == "command-line-arguments" { - return p.packageUsesRuntimeSetFinalizer(fn.Pkg) + pkg := declaredSSAPackage(fn) + if pkg == nil { + return false } - return false + return p.packageUsesRuntimeSetFinalizer(pkg) } func (p *context) packageUsesRuntimeSetFinalizer(pkg *ssa.Package) bool { + if pkg == nil { + return false + } + if uses, ok := p.finalizerPkgUses[pkg]; ok { + return uses + } + if p.finalizerPkgUses == nil { + p.finalizerPkgUses = make(map[*ssa.Package]bool) + } + uses := false + seen := make(map[*ssa.Function]bool) + check := func(fn *ssa.Function) bool { + return p.functionUsesRuntimeSetFinalizer(fn, seen) + } for _, member := range pkg.Members { - fn, ok := member.(*ssa.Function) - if ok && p.functionUsesRuntimeSetFinalizer(fn, map[*ssa.Function]bool{}) { - return true + if fn, ok := member.(*ssa.Function); ok && check(fn) { + uses = true + break } } - return false + if !uses && pkg.Prog != nil { + for _, member := range pkg.Members { + typ, ok := member.(*ssa.Type) + if !ok { + continue + } + for _, recv := range []types.Type{typ.Type(), types.NewPointer(typ.Type())} { + methods := pkg.Prog.MethodSets.MethodSet(recv) + for i := 0; i < methods.Len(); i++ { + obj, ok := methods.At(i).Obj().(*types.Func) + if !ok { + continue + } + if check(pkg.Prog.FuncValue(obj.Origin())) { + uses = true + break + } + } + if uses { + break + } + } + if uses { + break + } + } + } + p.finalizerPkgUses[pkg] = uses + return uses +} + +func declaredSSAPackage(fn *ssa.Function) *ssa.Package { + for fn != nil { + if fn.Pkg != nil { + return fn.Pkg + } + if origin := fn.Origin(); origin != nil && origin != fn { + fn = origin + continue + } + fn = fn.Parent() + } + return nil } func (p *context) functionUsesRuntimeSetFinalizer(fn *ssa.Function, seen map[*ssa.Function]bool) bool { @@ -1270,7 +1301,7 @@ func hasConservativeGCPointers(t types.Type, seen map[types.Type]bool) bool { } func (p *context) shouldClearAlloc(v *ssa.Alloc) bool { - if v == nil || v.Comment == "varargs" || v.Comment == "makeslice" { + if v == nil || v.Heap || v.Comment == "varargs" || v.Comment == "makeslice" { return false } ptr, ok := v.Type().Underlying().(*types.Pointer) @@ -1296,11 +1327,16 @@ func blockCanReach(from, to *ssa.BasicBlock, seen map[*ssa.BasicBlock]bool) bool return false } -func refBlock(ref ssa.Instruction) *ssa.BasicBlock { - if ref == nil { - return nil +func blockIsCyclic(block *ssa.BasicBlock) bool { + if block == nil { + return false } - return ref.Block() + for _, succ := range block.Succs { + if blockCanReach(succ, block, map[*ssa.BasicBlock]bool{}) { + return true + } + } + return false } func instructionUsesValue(instr ssa.Instruction, v ssa.Value) bool { @@ -1315,14 +1351,6 @@ func instructionUsesValue(instr ssa.Instruction, v ssa.Value) bool { return false } -func isCallLikeInstruction(instr ssa.Instruction) bool { - switch instr.(type) { - case *ssa.Call, *ssa.Defer, *ssa.Go: - return true - } - return false -} - func isTerminatingInstruction(instr ssa.Instruction) bool { switch instr.(type) { case *ssa.Jump, *ssa.Return, *ssa.If, *ssa.Panic: @@ -1339,123 +1367,15 @@ func (p *context) isRuntimeSetFinalizerCall(call *ssa.CallCommon) bool { if !ok || fn == nil || fn.Pkg == nil || fn.Pkg.Pkg == nil { return false } - return fn.Name() == "SetFinalizer" && - fn.Pkg.Pkg.Path() == "github.com/goplus/llgo/runtime/internal/lib/runtime" -} - -func (p *context) isOnlyRuntimeSetFinalizerArg(v ssa.Value) bool { - refs := v.Referrers() - if refs == nil || len(*refs) != 1 { + if fn.Name() != "SetFinalizer" { return false } - call, ok := (*refs)[0].(*ssa.Call) - return ok && p.isRuntimeSetFinalizerCall(&call.Call) -} - -func (p *context) shouldSkipLateSetFinalizerValue(instr ssa.Instruction) bool { - switch instr := instr.(type) { - case *ssa.MakeInterface: - return p.isOnlyRuntimeSetFinalizerArg(instr) - case *ssa.UnOp: - if instr.Op != token.MUL { - return false - } - refs := instr.Referrers() - if refs == nil || len(*refs) != 1 { - return false - } - mi, ok := (*refs)[0].(*ssa.MakeInterface) - return ok && p.isOnlyRuntimeSetFinalizerArg(mi) - } - return false -} - -func (p *context) collectValueUseBlocks(v ssa.Value, blocks map[*ssa.BasicBlock]bool, seen map[ssa.Value]bool, followPhi bool) bool { - if v == nil || seen[v] { - return true - } - seen[v] = true - refs := v.Referrers() - if refs == nil { + switch fn.Pkg.Pkg.Path() { + case "runtime", "github.com/goplus/llgo/runtime/internal/lib/runtime": return true + default: + return false } - for _, ref := range *refs { - switch ref := ref.(type) { - case *ssa.DebugRef: - continue - case *ssa.FieldAddr, *ssa.IndexAddr, *ssa.ChangeType, *ssa.Convert, *ssa.MakeInterface: - refVal, ok := ref.(ssa.Value) - if !ok { - return false - } - if !p.collectValueUseBlocks(refVal, blocks, seen, followPhi) { - return false - } - case *ssa.UnOp: - if ref.Op != token.MUL || ref.X != v { - blk := refBlock(ref) - if blk == nil { - return false - } - blocks[blk] = true - continue - } - blk := refBlock(ref) - if blk == nil { - return false - } - blocks[blk] = true - if !p.collectValueUseBlocks(ref, blocks, seen, followPhi) { - return false - } - case *ssa.Phi: - if followPhi { - if !p.collectValueUseBlocks(ref, blocks, seen, followPhi) { - return false - } - continue - } - for i, edge := range ref.Edges { - if edge == v && i < len(ref.Block().Preds) { - blocks[ref.Block().Preds[i]] = true - } - } - default: - instr, ok := ref.(ssa.Instruction) - if !ok || !instructionUsesValue(instr, v) { - return false - } - blk := refBlock(instr) - if blk == nil { - return false - } - blocks[blk] = true - } - } - return true -} - -func (p *context) valueLastUseBlock(v ssa.Value) (*ssa.BasicBlock, bool) { - blocks := make(map[*ssa.BasicBlock]bool) - if !p.collectValueUseBlocks(v, blocks, map[ssa.Value]bool{}, true) { - return nil, false - } - if len(blocks) == 0 { - return nil, true - } - for candidate := range blocks { - ok := true - for blk := range blocks { - if blk != candidate && !blockCanReach(blk, candidate, map[*ssa.BasicBlock]bool{}) { - ok = false - break - } - } - if ok { - return candidate, true - } - } - return nil, false } func (p *context) lastUseInBlock(v ssa.Value, blk *ssa.BasicBlock, order map[ssa.Instruction]int, seen map[ssa.Value]bool) (ssa.Instruction, bool) { @@ -1476,73 +1396,31 @@ func (p *context) lastUseInBlock(v ssa.Value, blk *ssa.BasicBlock, order map[ssa last = instr } } - refBeforeBlock := func(refBlk *ssa.BasicBlock) bool { - return refBlk != nil && blk != nil && refBlk != blk && blockCanReach(refBlk, blk, map[*ssa.BasicBlock]bool{}) - } for _, ref := range *refs { switch ref := ref.(type) { case *ssa.DebugRef: continue - case *ssa.FieldAddr, *ssa.IndexAddr, *ssa.ChangeType, *ssa.Convert, *ssa.MakeInterface: - refVal := ref.(ssa.Value) - refInstr := ref.(ssa.Instruction) - if refInstr.Block() != blk { - if refBeforeBlock(refInstr.Block()) { - continue - } - return nil, false - } - use, ok := p.lastUseInBlock(refVal, blk, order, seen) - if !ok { - return nil, false - } - updateLast(use) - case *ssa.UnOp: - if ref.Op != token.MUL || ref.X != v { - if ref.Block() != blk { - if refBeforeBlock(ref.Block()) { - continue - } - return nil, false - } - updateLast(ref) - continue - } - if ref.Block() != blk { - if refBeforeBlock(ref.Block()) { - continue - } - return nil, false - } - use, ok := p.lastUseInBlock(ref, blk, order, seen) - if !ok { - return nil, false - } - if use != nil { - if isCallLikeInstruction(use) { - updateLast(ref) - continue - } - updateLast(use) - } else { - updateLast(ref) - } + case *ssa.Defer, *ssa.Go, *ssa.MakeClosure: + return nil, false case *ssa.Phi: - use, ok := p.lastUseInBlock(ref, blk, order, seen) - if !ok { - return nil, false - } - updateLast(use) + return nil, false default: instr, ok := ref.(ssa.Instruction) if !ok || !instructionUsesValue(instr, v) { return nil, false } if instr.Block() != blk { - if refBeforeBlock(instr.Block()) { + return nil, false + } + if refVal, ok := ref.(ssa.Value); ok { + use, ok := p.lastUseInBlock(refVal, blk, order, seen) + if !ok { + return nil, false + } + if use != nil { + updateLast(use) continue } - return nil, false } updateLast(instr) } @@ -1558,11 +1436,12 @@ func (p *context) collectStackClearPlans(fn *ssa.Function) map[ssa.Instruction][ if !ok || !p.shouldClearAlloc(alloc) { continue } - useBlk, ok := p.valueLastUseBlock(alloc) - if !ok || useBlk == nil { - continue - } - if useBlk != alloc.Block() && alloc.Block().Index != 0 { + // Deliberately limit clearing to exact, non-escaping slots whose + // complete use graph stays in one acyclic basic block. This can + // retain stale roots, but it cannot guess across control-flow, + // closure, defer, goroutine, or heap-escape boundaries. + useBlk := alloc.Block() + if useBlk == nil || blockIsCyclic(useBlk) { continue } order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) @@ -1570,7 +1449,7 @@ func (p *context) collectStackClearPlans(fn *ssa.Function) map[ssa.Instruction][ order[useInstr] = i } last, ok := p.lastUseInBlock(alloc, useBlk, order, map[ssa.Value]bool{}) - if ok && last != nil { + if ok && last != nil && !isTerminatingInstruction(last) { plans[last] = append(plans[last], alloc) } } @@ -1578,198 +1457,13 @@ func (p *context) collectStackClearPlans(fn *ssa.Function) map[ssa.Instruction][ return plans } -func (p *context) collectEntryClearPlans(fn *ssa.Function) map[*ssa.BasicBlock][]*ssa.Alloc { - plans := make(map[*ssa.BasicBlock][]*ssa.Alloc) - for _, blk := range fn.Blocks { - if blk == nil || len(blk.Succs) < 2 { - continue - } - for _, instr := range blk.Instrs { - alloc, ok := instr.(*ssa.Alloc) - if !ok || !p.shouldClearAlloc(alloc) { - continue - } - useBlocks := make(map[*ssa.BasicBlock]bool) - if !p.collectValueUseBlocks(alloc, useBlocks, map[ssa.Value]bool{}, false) { - continue - } - liveSucc := make(map[*ssa.BasicBlock]bool, len(blk.Succs)) - for _, succ := range blk.Succs { - for useBlk := range useBlocks { - if useBlk == nil { - continue - } - if succ == useBlk || blockCanReach(succ, useBlk, map[*ssa.BasicBlock]bool{}) { - liveSucc[succ] = true - break - } - } - } - if len(liveSucc) == 0 || len(liveSucc) == len(blk.Succs) { - continue - } - for _, succ := range blk.Succs { - if !liveSucc[succ] && len(succ.Preds) == 1 { - plans[succ] = append(plans[succ], alloc) - } - } - } - } - return plans -} - -func (p *context) collectParamClobberPlans(fn *ssa.Function) map[ssa.Instruction]bool { - plans := make(map[ssa.Instruction]bool) - for _, param := range fn.Params { - if !hasConservativeGCPointers(param.Type(), map[types.Type]bool{}) { - continue - } - useBlk, ok := p.valueLastUseBlock(param) - if !ok || useBlk == nil { - continue - } - order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) - for i, useInstr := range useBlk.Instrs { - order[useInstr] = i - } - last, ok := p.lastUseInBlock(param, useBlk, order, map[ssa.Value]bool{}) - if ok && last != nil { - plans[last] = true - } - } - return plans -} - -func (p *context) collectParamScanPlans(fn *ssa.Function) map[ssa.Instruction][]*ssa.Parameter { - plans := make(map[ssa.Instruction][]*ssa.Parameter) - for _, param := range fn.Params { - if !hasConservativeGCPointers(param.Type(), map[types.Type]bool{}) { - continue - } - useBlk, ok := p.valueLastUseBlock(param) - if !ok || useBlk == nil { - continue - } - order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) - for i, useInstr := range useBlk.Instrs { - order[useInstr] = i - } - last, ok := p.lastUseInBlock(param, useBlk, order, map[ssa.Value]bool{}) - if ok && last != nil { - plans[last] = append(plans[last], param) - } - } - return plans -} - -func (p *context) collectCallClobberPlans(fn *ssa.Function) map[ssa.Instruction]bool { - plans := make(map[ssa.Instruction]bool) - for _, blk := range fn.Blocks { - for _, instr := range blk.Instrs { - call, ok := instr.(*ssa.Call) - if !ok { - continue - } - for _, arg := range call.Common().Args { - if hasConservativeGCPointers(arg.Type(), map[types.Type]bool{}) { - plans[instr] = true - break - } - } - } - } - return plans -} - -func (p *context) compileLateValue(b llssa.Builder, v ssa.Value) llssa.Expr { - switch v := v.(type) { - case *ssa.MakeInterface: - t := p.type_(v.Type(), llssa.InGo) - x := p.compileLateValue(b, v.X) - return b.MakeInterface(t, x) - case *ssa.UnOp: - if v.Op != token.MUL { - return p.compileValue(b, v) - } - x := p.compileLateValue(b, v.X) - return b.UnOp(v.Op, x) - case *ssa.FieldAddr: - x := p.compileLateValue(b, v.X) - return b.FieldAddr(x, v.Field) - case *ssa.IndexAddr: - x := p.compileLateValue(b, v.X) - idx := p.compileLateValue(b, v.Index) - return b.IndexAddr(x, idx) - case *ssa.ChangeType: - t := p.type_(v.Type(), llssa.InGo) - x := p.compileLateValue(b, v.X) - return b.ChangeType(t, x) - case *ssa.Convert: - t := p.type_(v.Type(), llssa.InGo) - x := p.compileLateValue(b, v.X) - return b.Convert(t, x) - } - return p.compileValue(b, v) -} - -func (p *context) scanStackPointer(b llssa.Builder, val llssa.Expr) { - b.Pkg.NeedRuntime = true - t := p.type_(types.Typ[types.Uintptr], llssa.InGo) - if !types.Identical(val.RawType(), t.RawType()) { - val = b.Convert(t, val) - } - fn := b.Pkg.NewFunc("llgo_clear_stack_ptr", - types.NewSignatureType(nil, nil, nil, types.NewTuple(types.NewParam(token.NoPos, nil, "target", types.Typ[types.Uintptr])), nil, false), llssa.InC) - b.Call(fn.Expr, val) -} - -func (p *context) scanPointerExpr(b llssa.Builder, val llssa.Expr) { - switch t := types.Unalias(val.RawType()).Underlying().(type) { - case *types.Pointer: - p.scanStackPointer(b, val) - case *types.Struct: - if t.NumFields() == 1 { - if _, ok := types.Unalias(t.Field(0).Type()).Underlying().(*types.Pointer); ok { - p.scanStackPointer(b, b.Field(val, 0)) - } - } - } -} - -func (p *context) scanAllocPointer(b llssa.Builder, ptr llssa.Expr) { - elem := b.Prog.Elem(ptr.Type) - switch t := types.Unalias(elem.RawType()).Underlying().(type) { - case *types.Pointer: - p.scanStackPointer(b, b.Load(ptr)) - case *types.Struct: - if t.NumFields() == 1 { - if _, ok := types.Unalias(t.Field(0).Type()).Underlying().(*types.Pointer); ok { - p.scanStackPointer(b, b.Load(b.FieldAddr(ptr, 0))) - } - } - } -} - -func (p *context) scanParamPointers(b llssa.Builder, instr ssa.Instruction) { - params := p.paramScans[instr] - for _, param := range params { - p.scanPointerExpr(b, p.compileValue(b, param)) - } -} - func (p *context) clearAlloc(b llssa.Builder, alloc *ssa.Alloc) { ptr := p.compileValue(b, alloc) - b.IfThen(b.BinOp(token.NEQ, ptr, p.prog.Zero(ptr.Type)), func() { - p.scanAllocPointer(b, ptr) - elem := b.Prog.Elem(ptr.Type) - b.Store(ptr, p.prog.Zero(elem)) - }) + elem := b.Prog.Elem(ptr.Type) + b.StoreVolatile(ptr, p.prog.Zero(elem)) } func (p *context) clearDeadAllocs(b llssa.Builder, instr ssa.Instruction) { - if p.loadClears[instr] { - return - } allocs := p.stackClears[instr] if len(allocs) == 0 { return @@ -1777,35 +1471,6 @@ func (p *context) clearDeadAllocs(b llssa.Builder, instr ssa.Instruction) { for _, alloc := range allocs { p.clearAlloc(b, alloc) } - if unop, ok := instr.(*ssa.UnOp); ok && unop.Op == token.MUL { - return - } - p.clobberPointerRegs(b) -} - -func (p *context) clearEntryAllocs(b llssa.Builder, block *ssa.BasicBlock) { - allocs := p.entryClears[block] - if len(allocs) == 0 { - return - } - for _, alloc := range allocs { - p.clearAlloc(b, alloc) - } - p.clobberPointerRegs(b) -} - -func (p *context) clobberPointerRegs(b llssa.Builder) { - b.Pkg.NeedRuntime = true - uintptrParam := func(name string) *types.Var { - return types.NewParam(token.NoPos, nil, name, types.Typ[types.Uintptr]) - } - fn := b.Pkg.NewFunc("llgo_clobber_pointer_regs", - types.NewSignatureType(nil, nil, nil, types.NewTuple( - uintptrParam("a0"), uintptrParam("a1"), uintptrParam("a2"), uintptrParam("a3"), - uintptrParam("a4"), uintptrParam("a5"), uintptrParam("a6"), uintptrParam("a7"), - ), nil, false), llssa.InC) - zero := b.Prog.IntVal(0, b.Prog.Uintptr()) - b.Call(fn.Expr, zero, zero, zero, zero, zero, zero, zero, zero) } func isPhi(i ssa.Instruction) bool { @@ -1943,14 +1608,6 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue return ret } } - if len(p.stackClears[v]) > 0 { - x := p.compileValue(b, v.X) - if ret, ok := b.LoadAndClearSinglePointer(x); ok { - p.loadClears[v] = true - p.bvals[iv] = ret - return ret - } - } } x := p.compileValue(b, v.X) if v.Op != token.ARROW { diff --git a/cl/instr.go b/cl/instr.go index ec14a11642..678c4afc6d 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -1998,12 +1998,6 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm ret = p.emitDo(b, act, ds, llssa.Builtin(fn), llssa.Builder.Call, args...) case *ssa.Function: aFn, pyFn, ftype := p.compileFunction(cv) - if p.isRuntimeSetFinalizerCall(call) && len(args) == 2 && act == llssa.Call && ds == nil { - finalizer := p.compileLateValue(b, args[1]) - obj := p.compileLateValue(b, args[0]) - ret = p.emitDo(b, act, nil, aFn.Expr, llssa.Builder.Call, obj, finalizer) - return - } // TODO(xsw): check ca != llssa.Call switch ftype { case cFunc: diff --git a/cl/liveness_internal_test.go b/cl/liveness_internal_test.go index c20df9d7b0..9d338cadfd 100644 --- a/cl/liveness_internal_test.go +++ b/cl/liveness_internal_test.go @@ -12,7 +12,6 @@ import ( "testing" "github.com/goplus/gogen/packages" - llssa "github.com/goplus/llgo/ssa" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" ) @@ -124,13 +123,20 @@ func allocs(p *int) { fn.WriteTo(&dump) t.Fatalf("missing expected allocs: %v\n%s", functionAllocs(fn), dump.String()) } - if !ctx.shouldClearAlloc(boxAlloc) { - t.Fatal("struct containing a pointer should be cleared") + if !boxAlloc.Heap { + t.Fatal("address-taken box should be marked as a heap allocation") + } + if ctx.shouldClearAlloc(boxAlloc) { + t.Fatal("heap allocation must not be cleared") } if ctx.shouldClearAlloc(intAlloc) { t.Fatal("int alloc should not be cleared") } + boxAlloc.Heap = false + if !ctx.shouldClearAlloc(boxAlloc) { + t.Fatal("non-heap stack slot containing a pointer should be cleared") + } boxAlloc.Comment = "varargs" if ctx.shouldClearAlloc(boxAlloc) { t.Fatal("varargs alloc should not be cleared") @@ -166,7 +172,7 @@ func functionAllocs(fn *ssa.Function) []*ssa.Alloc { func TestRuntimeSetFinalizerDetection(t *testing.T) { ssapkg := buildSSAPackageWithPath(t, "github.com/goplus/llgo/runtime/livetest", "livetest", `package livetest -import rt "github.com/goplus/llgo/runtime/internal/lib/runtime" +import rt "runtime" func direct(p *int) { rt.SetFinalizer(p, func(*int) {}) @@ -213,57 +219,57 @@ func none(p *int) {} if !ctx.packageUsesRuntimeSetFinalizer(ssapkg) { t.Fatal("package should report SetFinalizer use") } - if ctx.enableConservativeLivenessClears(direct) { - t.Fatal("non command-line-arguments package should not enable conservative clears") + if !ctx.enableConservativeLivenessClears(direct) { + t.Error("module package with SetFinalizer should enable conservative clears") } ssapkg.Pkg = types.NewPackage("command-line-arguments", "main") if !ctx.enableConservativeLivenessClears(direct) { t.Fatal("command-line-arguments package with SetFinalizer should enable conservative clears") } -} -func TestRuntimeSetFinalizerLateValueSkips(t *testing.T) { - ssapkg := buildSSAPackageWithPath(t, "github.com/goplus/llgo/runtime/livetest", "livetest", `package livetest + methodPkg := buildSSAPackageWithPath(t, "github.com/goplus/llgo/runtime/methodlive", "methodlive", `package methodlive -import rt "github.com/goplus/llgo/runtime/internal/lib/runtime" +import rt "runtime" -func direct(p *int) { + type setter struct{} + +func (setter) install(p *int) { rt.SetFinalizer(p, func(*int) {}) } `) - ctx := &context{} - fn := ssapkg.Func("direct") - var makeIface *ssa.MakeInterface - var deref *ssa.UnOp - for _, block := range fn.Blocks { - for _, instr := range block.Instrs { - switch instr := instr.(type) { - case *ssa.MakeInterface: - makeIface = instr - case *ssa.UnOp: - if instr.Op == token.MUL { - deref = instr - } - } - } - } - if makeIface == nil { - t.Fatal("missing MakeInterface for SetFinalizer argument") + if !ctx.packageUsesRuntimeSetFinalizer(methodPkg) { + t.Error("method-only SetFinalizer use should be detected") } - if ctx.isRuntimeSetFinalizerCall(nil) { - t.Fatal("nil call should not be SetFinalizer") - } - if !ctx.shouldSkipLateSetFinalizerValue(makeIface) { - t.Fatal("SetFinalizer-only MakeInterface should be skipped") + + genericMethodPkg := buildSSAPackageWithPath(t, "github.com/goplus/llgo/runtime/genericmethodlive", "genericmethodlive", `package genericmethodlive + +import rt "runtime" + +type setter[T any] struct{} + +func (setter[T]) install(p *T) { + rt.SetFinalizer(p, func(*T) {}) +} + +func use(p *int) { + setter[int]{}.install(p) +} +`) + if !ctx.packageUsesRuntimeSetFinalizer(genericMethodPkg) { + t.Error("generic method-only SetFinalizer use should be detected") } - if deref != nil && !ctx.shouldSkipLateSetFinalizerValue(deref) { - t.Fatal("SetFinalizer-only deref should be skipped") + var genericMethod *ssa.Function + for fn := range ssautil.AllFunctions(genericMethodPkg.Prog) { + if origin := fn.Origin(); origin != nil && origin.Name() == "install" { + genericMethod = fn + break + } } - if ctx.shouldSkipLateSetFinalizerValue(&ssa.Return{}) { - t.Fatal("unrelated instruction should not be skipped") + if genericMethod == nil { + t.Fatal("missing instantiated generic method") } - if ctx.shouldSkipLateSetFinalizerValue(&ssa.UnOp{Op: token.SUB}) { - t.Fatal("non-deref unary op should not be skipped") + if !ctx.enableConservativeLivenessClears(genericMethod) { + t.Error("instantiated generic method should inherit its package liveness setting") } } @@ -281,51 +287,69 @@ func linear(p *int) { Sink = 1 } -func branch(p *int, cond bool) { +func loop(p *int) { var box Box box.p = p - if cond { + for i := 0; i < 2; i++ { Sink = box.p - } else { - Sink = 0 } Sink = 1 } -func branchBoth(p *int, cond bool) { +func takes(*int) {} + +func deferred(p *int) { var box Box box.p = p - if cond { - Sink = box.p - } else { - Sink = box.p - } + defer takes(box.p) Sink = 1 } -func paramUse(p *int) { - Sink = p +func goroutine(p *int) { + var box Box + box.p = p + go takes(box.p) Sink = 1 } -func splitParam(p *int, cond bool) { - if cond { - Sink = p - } else { - Sink = p - } +func takesBox(Box) {} + +func callLocal(p *int) { + var box Box + box.p = p + takesBox(box) Sink = 1 } -func takes(*int) {} +func cyclicLocal(p *int, n int) { + for n > 0 { + var box Box + box.p = p + Sink = box.p + n-- + } +} -func callWithPointer(p *int) { - takes(p) +func slicedLocal(p *int) { + var values [1]*int + values[0] = p + slice := values[:] + Sink = slice[0] Sink = 1 } -func callWithInt(i int) { - Sink = i +func phiLocal(p *int, cond bool) { + var left, right Box + left.p = p + right.p = p + var box *Box + if cond { + box = &left + } else { + box = &right + } + Sink = box.p + Sink = 1 } `) ctx := &context{} @@ -340,45 +364,96 @@ func callWithInt(i int) { } } - entryPlans := ctx.collectEntryClearPlans(ssapkg.Func("branch")) - if len(entryPlans) == 0 { - t.Fatal("branch should produce entry clear plans for dead successor") + for _, name := range []string{"loop", "deferred", "goroutine"} { + if got := ctx.collectStackClearPlans(ssapkg.Func(name)); len(got) != 0 { + t.Fatalf("%s should fail closed instead of producing clear plans: %v", name, got) + } + } + + callLocal := ssapkg.Func("callLocal") + callPlans := ctx.collectStackClearPlans(callLocal) + if len(callPlans) == 0 { + t.Fatal("callLocal should produce a stack clear plan") } - if got := ctx.collectEntryClearPlans(ssapkg.Func("branchBoth")); len(got) != 0 { - t.Fatalf("branchBoth should not clear values live in both successors: %v", got) + for instr := range callPlans { + if _, ok := instr.(*ssa.Call); !ok { + t.Fatalf("callLocal clear must follow its real final use, got %T", instr) + } } - paramFn := ssapkg.Func("paramUse") - if len(ctx.collectParamClobberPlans(paramFn)) == 0 { - t.Fatal("paramUse should produce param clobber plans") + cyclicLocal := ssapkg.Func("cyclicLocal") + var cyclicAlloc *ssa.Alloc + for _, alloc := range functionAllocs(cyclicLocal) { + if !alloc.Heap && blockIsCyclic(alloc.Block()) { + cyclicAlloc = alloc + break + } + } + if cyclicAlloc == nil { + var dump strings.Builder + cyclicLocal.WriteTo(&dump) + t.Fatalf("cyclicLocal should contain a non-heap allocation in a cyclic block:\n%s", dump.String()) } - if len(ctx.collectParamScanPlans(paramFn)) == 0 { - t.Fatal("paramUse should produce param scan plans") + if got := ctx.collectStackClearPlans(cyclicLocal); len(got) != 0 { + t.Fatalf("cyclicLocal should fail closed instead of producing clear plans: %v", got) } - splitParam := ssapkg.Func("splitParam") - if got := ctx.collectParamClobberPlans(splitParam); len(got) != 0 { - t.Fatalf("splitParam has no single last-use block, got clobbers: %v", got) + + slicedLocal := ssapkg.Func("slicedLocal") + var hasSlice bool + for _, block := range slicedLocal.Blocks { + for _, instr := range block.Instrs { + if _, ok := instr.(*ssa.Slice); ok { + hasSlice = true + } + } + } + if !hasSlice { + t.Fatal("slicedLocal should exercise a slice-derived stack reference") } - if got := ctx.collectParamScanPlans(splitParam); len(got) != 0 { - t.Fatalf("splitParam has no single last-use block, got scans: %v", got) + var slicedAlloc *ssa.Alloc + for _, alloc := range functionAllocs(slicedLocal) { + ptr, ok := alloc.Type().Underlying().(*types.Pointer) + if ok { + if _, ok := ptr.Elem().Underlying().(*types.Array); ok { + slicedAlloc = alloc + slicedAlloc.Heap = false + break + } + } } - if len(ctx.collectCallClobberPlans(ssapkg.Func("callWithPointer"))) == 0 { - t.Fatal("pointer call should clobber pointer regs") + if slicedAlloc == nil { + var dump strings.Builder + slicedLocal.WriteTo(&dump) + t.Fatalf("slicedLocal should contain an array allocation:\n%s", dump.String()) } - if len(ctx.collectCallClobberPlans(ssapkg.Func("callWithInt"))) != 0 { - t.Fatal("int-only call should not clobber pointer regs") + if got := ctx.collectStackClearPlans(slicedLocal); len(got) == 0 { + var dump strings.Builder + slicedLocal.WriteTo(&dump) + t.Fatalf("slicedLocal should produce a stack clear plan:\n%s", dump.String()) + } + + phiLocal := ssapkg.Func("phiLocal") + var hasPhi bool + for _, block := range phiLocal.Blocks { + for _, instr := range block.Instrs { + if _, ok := instr.(*ssa.Phi); ok { + hasPhi = true + } + } + } + if !hasPhi { + t.Fatal("phiLocal should exercise a merged stack reference") + } + if got := ctx.collectStackClearPlans(phiLocal); len(got) != 0 { + t.Fatalf("phiLocal should fail closed instead of producing clear plans: %v", got) } } func TestConservativeLivenessGraphHelpers(t *testing.T) { ssapkg := buildSSAPackageWithPath(t, "example.com/live", "live", `package live -import "unsafe" - var Sink any -type Box struct{ p *int } - func flow(p *int, cond bool) { if cond { Sink = p @@ -387,34 +462,16 @@ func flow(p *int, cond bool) { } } -func split(p *int, cond bool) { - if cond { - Sink = p - } else { - Sink = p - } -} - func target(*int) {} func withCall(p *int) { target(p) } -func refs(p *int, arr *[2]*int, box *Box, cond bool) *int { - var q *int - if cond { - q = p - } else { - q = box.p +func loop(p *int) { + for i := 0; i < 2; i++ { + Sink = p } - Sink = arr[0] - Sink = q - return q -} - -func converted(p *int) unsafe.Pointer { - return unsafe.Pointer(p) } `) fn := ssapkg.Func("flow") @@ -424,106 +481,50 @@ func converted(p *int) unsafe.Pointer { if !blockCanReach(fn.Blocks[0], fn.Blocks[0], map[*ssa.BasicBlock]bool{}) { t.Fatal("block should reach itself") } + loop := ssapkg.Func("loop") + var cyclic int + for _, block := range loop.Blocks { + if blockIsCyclic(block) { + cyclic++ + } + } + if cyclic == 0 { + t.Fatal("loop should contain at least one cyclic block") + } if instructionUsesValue(nil, fn.Params[0]) { t.Fatal("nil instruction should not use values") } if instructionUsesValue(fn.Blocks[0].Instrs[0], nil) { t.Fatal("nil value should not be used") } - if isCallLikeInstruction(fn.Blocks[0].Instrs[0]) { - t.Fatal("if instruction should not be call-like") - } if !isTerminatingInstruction(fn.Blocks[0].Instrs[len(fn.Blocks[0].Instrs)-1]) { t.Fatal("entry block should end with a terminator") } ctx := &context{} - if blk := refBlock(nil); blk != nil { - t.Fatalf("refBlock(nil) = %v", blk) - } - blocks := make(map[*ssa.BasicBlock]bool) - if !ctx.collectValueUseBlocks(nil, blocks, map[ssa.Value]bool{}, false) { - t.Fatal("nil collectValueUseBlocks should succeed") - } - if !ctx.collectValueUseBlocks(fn.Params[0], blocks, map[ssa.Value]bool{fn.Params[0]: true}, false) { - t.Fatal("seen collectValueUseBlocks should succeed") - } - if !ctx.collectValueUseBlocks(fn.Params[0], blocks, map[ssa.Value]bool{}, false) { - t.Fatal("collectValueUseBlocks failed") - } - if len(blocks) == 0 { - t.Fatal("expected use blocks for parameter") - } - if blk, ok := ctx.valueLastUseBlock(fn.Params[0]); !ok || blk == nil { - t.Fatalf("valueLastUseBlock = %v, %v", blk, ok) - } - if blk, ok := ctx.valueLastUseBlock(nil); !ok || blk != nil { - t.Fatalf("valueLastUseBlock(nil) = %v, %v", blk, ok) - } if last, ok := ctx.lastUseInBlock(nil, fn.Blocks[0], map[ssa.Instruction]int{}, map[ssa.Value]bool{}); !ok || last != nil { t.Fatalf("lastUseInBlock(nil) = %v, %v", last, ok) } - split := ssapkg.Func("split") - if blk, ok := ctx.valueLastUseBlock(split.Params[0]); ok || blk != nil { - t.Fatalf("valueLastUseBlock(split param) = %v, %v; want no single block", blk, ok) - } - entryOrder := make(map[ssa.Instruction]int, len(split.Blocks[0].Instrs)) - for i, instr := range split.Blocks[0].Instrs { - entryOrder[instr] = i - } - if last, ok := ctx.lastUseInBlock(split.Params[0], split.Blocks[0], entryOrder, map[ssa.Value]bool{}); ok || last != nil { - t.Fatalf("lastUseInBlock(split param in entry) = %v, %v; want failure outside block", last, ok) - } - var callLike int - for _, block := range ssapkg.Func("withCall").Blocks { + withCall := ssapkg.Func("withCall") + var call *ssa.Call + for _, block := range withCall.Blocks { for _, instr := range block.Instrs { - if isCallLikeInstruction(instr) { - callLike++ + if callInstr, ok := instr.(*ssa.Call); ok { + call = callInstr } } } - if callLike == 0 { - t.Fatal("flow should include at least one call-like instruction") - } - - refs := ssapkg.Func("refs") - var lastUseCount int - for _, param := range refs.Params { - blocks := make(map[*ssa.BasicBlock]bool) - if !ctx.collectValueUseBlocks(param, blocks, map[ssa.Value]bool{}, true) { - t.Fatalf("collectValueUseBlocks failed for %s", param.Name()) - } - if len(blocks) == 0 { - t.Fatalf("expected use blocks for %s", param.Name()) - } - blk, ok := ctx.valueLastUseBlock(param) - if !ok || blk == nil { - t.Fatalf("valueLastUseBlock(%s) = %v, %v", param.Name(), blk, ok) - } - order := make(map[ssa.Instruction]int, len(blk.Instrs)) - for i, instr := range blk.Instrs { - order[instr] = i - } - if last, ok := ctx.lastUseInBlock(param, blk, order, map[ssa.Value]bool{}); !ok { - t.Fatalf("lastUseInBlock(%s) = %v, %v", param.Name(), last, ok) - } else if last != nil { - lastUseCount++ - } - } - if lastUseCount == 0 { - t.Fatal("expected at least one parameter with a concrete last use") - } - phiBlocks := make(map[*ssa.BasicBlock]bool) - if !ctx.collectValueUseBlocks(refs.Params[0], phiBlocks, map[ssa.Value]bool{}, false) { - t.Fatal("non-following phi use collection failed") + if call == nil { + t.Fatal("withCall should include a call-like instruction") } - if len(phiBlocks) == 0 { - t.Fatal("non-following phi use collection should record predecessor blocks") + block := call.Block() + order := make(map[ssa.Instruction]int, len(block.Instrs)) + for i, instr := range block.Instrs { + order[instr] = i } - converted := ssapkg.Func("converted") - if !ctx.collectValueUseBlocks(converted.Params[0], make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, true) { - t.Fatal("conversion use collection failed") + if last, ok := ctx.lastUseInBlock(withCall.Params[0], block, order, map[ssa.Value]bool{}); !ok || last != call { + t.Fatalf("lastUseInBlock(call parameter) = %v, %v; want call", last, ok) } } @@ -585,17 +586,7 @@ func derefOnly(p **int) { if instructionUsesValue(useP, useOne.Params[1]) { t.Fatal("instruction using p should not report use of q") } - if ctx.isOnlyRuntimeSetFinalizerArg(useOne.Params[1]) { - t.Fatal("unused parameter should not be treated as a SetFinalizer-only argument") - } - if ctx.shouldSkipLateSetFinalizerValue(&ssa.UnOp{Op: token.MUL}) { - t.Fatal("deref without a single MakeInterface referrer should not be skipped") - } - global := ssapkg.Members["Sink"].(*ssa.Global) - if !ctx.collectValueUseBlocks(global, make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, false) { - t.Fatal("global without referrers should be a valid value-use query") - } if last, ok := ctx.lastUseInBlock(global, useOne.Blocks[0], map[ssa.Instruction]int{}, map[ssa.Value]bool{}); !ok || last != nil { t.Fatalf("lastUseInBlock(global) = %v, %v", last, ok) } @@ -616,19 +607,14 @@ func derefOnly(p **int) { if negInstr == nil { t.Fatalf("missing unary negation:\n%s", neg.String()) } - blocks := make(map[*ssa.BasicBlock]bool) - if !ctx.collectValueUseBlocks(neg.Params[0], blocks, map[ssa.Value]bool{}, false) { - t.Fatal("non-deref unary use collection failed") - } - if !blocks[negInstr.Block()] { - t.Fatal("non-deref unary use should record its block") - } order := make(map[ssa.Instruction]int, len(negInstr.Block().Instrs)) for i, instr := range negInstr.Block().Instrs { order[instr] = i } - if last, ok := ctx.lastUseInBlock(neg.Params[0], negInstr.Block(), order, map[ssa.Value]bool{}); !ok || last != negInstr { - t.Fatalf("lastUseInBlock(neg param) = %v, %v; want unary op", last, ok) + if last, ok := ctx.lastUseInBlock(neg.Params[0], negInstr.Block(), order, map[ssa.Value]bool{}); !ok { + t.Fatalf("lastUseInBlock(neg param) = %v, %v", last, ok) + } else if _, ok := last.(*ssa.Return); !ok { + t.Fatalf("lastUseInBlock(neg param) = %T; want return", last) } callDeref := ssapkg.Func("callDeref") @@ -651,8 +637,10 @@ func derefOnly(p **int) { for i, instr := range deref.Block().Instrs { order[instr] = i } - if last, ok := ctx.lastUseInBlock(callDeref.Params[0], deref.Block(), order, map[ssa.Value]bool{}); !ok || last != deref { - t.Fatalf("lastUseInBlock(call deref param) = %v, %v; want deref", last, ok) + if last, ok := ctx.lastUseInBlock(callDeref.Params[0], deref.Block(), order, map[ssa.Value]bool{}); !ok { + t.Fatalf("lastUseInBlock(call deref param) = %v, %v", last, ok) + } else if _, ok := last.(*ssa.Call); !ok { + t.Fatalf("lastUseInBlock(call deref param) = %T; want call", last) } derefOnly := ssapkg.Func("derefOnly") @@ -671,9 +659,6 @@ func derefOnly(p **int) { if loneDeref == nil { t.Fatalf("missing lone dereference:\n%s", derefOnly.String()) } - if !ctx.collectValueUseBlocks(derefOnly.Params[0], make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, false) { - t.Fatal("lone deref use collection failed") - } order = make(map[ssa.Instruction]int, len(loneDeref.Block().Instrs)) for i, instr := range loneDeref.Block().Instrs { order[instr] = i @@ -705,12 +690,6 @@ func use(p *int) { *refs = original }() - if ctx.collectValueUseBlocks(param, make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, true) { - t.Fatal("malformed referrer graph should disable liveness clearing") - } - if block, ok := ctx.valueLastUseBlock(param); ok || block != nil { - t.Fatalf("valueLastUseBlock with malformed referrer = %v, %v; want failure", block, ok) - } order := make(map[ssa.Instruction]int, len(fn.Blocks[0].Instrs)) for i, instr := range fn.Blocks[0].Instrs { order[instr] = i @@ -747,12 +726,6 @@ func use(p *int) { *refs = original }() - if ctx.collectValueUseBlocks(param, make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, true) { - t.Fatal("malformed derived referrer graph should disable liveness clearing") - } - if block, ok := ctx.valueLastUseBlock(param); ok || block != nil { - t.Fatalf("valueLastUseBlock with malformed derived referrer = %v, %v; want failure", block, ok) - } order := make(map[ssa.Instruction]int, len(fn.Blocks[0].Instrs)) for i, instr := range fn.Blocks[0].Instrs { order[instr] = i @@ -787,9 +760,6 @@ func use(p *int) { } ctx := &context{} - if !ctx.collectValueUseBlocks(fn.Params[0], make(map[*ssa.BasicBlock]bool), map[ssa.Value]bool{}, false) { - t.Fatal("DebugRef should be ignored while collecting use blocks") - } order := make(map[ssa.Instruction]int, len(fn.Blocks[0].Instrs)) for i, instr := range fn.Blocks[0].Instrs { order[instr] = i @@ -799,25 +769,6 @@ func use(p *int) { } } -func TestConservativeLivenessScanAllocPointerSlot(t *testing.T) { - prog := newLLSSAProg(t) - pkg := prog.NewPackage("live", "live") - ptrToInt := types.NewPointer(types.Typ[types.Int]) - slotType := types.NewPointer(ptrToInt) - sig := types.NewSignatureType(nil, nil, nil, - types.NewTuple(types.NewParam(token.NoPos, nil, "slot", slotType)), nil, false) - fn := pkg.NewFunc("scanPointerSlot", sig, llssa.InGo) - b := fn.MakeBody(1) - (&context{prog: prog}).scanAllocPointer(b, fn.Param(0)) - b.Return() - b.EndBuild() - - ir := pkg.String() - if !strings.Contains(ir, "llgo_clear_stack_ptr") { - t.Fatalf("pointer slot scan should emit stack clear helper:\n%s", ir) - } -} - func TestCompileWithoutConservativeLivenessClears(t *testing.T) { ssapkg, files := buildSSAPackageWithPathAndFiles(t, "command-line-arguments", "main", `package main @@ -832,30 +783,40 @@ func main() { if err != nil { t.Fatal(err) } - if strings.Contains(pkg.String(), "llgo_clear_stack_ptr") { - t.Fatalf("package without SetFinalizer should not emit liveness clear helpers:\n%s", pkg.String()) + for _, helper := range []string{"llgo_clear_stack_ptr", "llgo_clobber_pointer_regs"} { + if strings.Contains(pkg.String(), helper) { + t.Fatalf("package without SetFinalizer should not emit %s:\n%s", helper, pkg.String()) + } + } + if strings.Contains(pkg.String(), "store volatile") { + t.Fatalf("package without SetFinalizer should not emit liveness clears:\n%s", pkg.String()) } } func TestCompileConservativeLivenessClears(t *testing.T) { ssapkg, files := buildSSAPackageWithPathAndFiles(t, "github.com/goplus/llgo/runtime/livetest", "main", `package main -import rt "github.com/goplus/llgo/runtime/internal/lib/runtime" +import rt "runtime" -type Box struct{ p *int } +type Box struct{ p, q *int } var Sink any -func main() { - x := 1 +func clearLocal(p *int) { var box Box - box.p = &x + box.p = p + box.q = p Sink = box.p - rt.SetFinalizer(&box, func(*Box) {}) + Sink = box.q Sink = 1 } + +func main() { + x := new(int) + rt.SetFinalizer(x, func(*int) {}) + clearLocal(x) +} `) - ssapkg.Pkg = types.NewPackage("command-line-arguments", "main") prog := newLLSSAProg(t) pkg, err := NewPackage(prog, ssapkg, files) @@ -863,17 +824,20 @@ func main() { t.Fatal(err) } ir := pkg.String() - for _, want := range []string{"llgo_clear_stack_ptr", "llgo_clobber_pointer_regs"} { - if !strings.Contains(ir, want) { - t.Fatalf("compiled liveness module missing %s:\n%s", want, ir) + for _, helper := range []string{"llgo_clear_stack_ptr", "llgo_clobber_pointer_regs"} { + if strings.Contains(ir, helper) { + t.Fatalf("compiled liveness module must not use %s:\n%s", helper, ir) } } - if !pkg.NeedRuntime { - t.Fatal("liveness clear helpers should mark runtime as needed") + if !strings.Contains(ir, `%"github.com/goplus/llgo/runtime/livetest.Box" = type { ptr, ptr }`) { + t.Fatalf("compiled liveness module missing two-pointer aggregate type:\n%s", ir) + } + if !strings.Contains(ir, `store volatile %"github.com/goplus/llgo/runtime/livetest.Box" zeroinitializer`) { + t.Fatalf("compiled liveness module missing volatile whole-aggregate clear:\n%s", ir) } } -func TestCompileConservativeLivenessStructParamScans(t *testing.T) { +func TestCompileConservativeLivenessDoesNotScanWholeStack(t *testing.T) { ssapkg, files := buildSSAPackageWithPathAndFiles(t, "github.com/goplus/llgo/runtime/livetest", "main", `package main import rt "github.com/goplus/llgo/runtime/internal/lib/runtime" @@ -931,10 +895,9 @@ func main() { t.Fatal(err) } ir := pkg.String() - if strings.Count(ir, "llgo_clear_stack_ptr") < 2 { - t.Fatalf("expected stack pointer scans for struct param and local:\n%s", ir) - } - if !strings.Contains(ir, "llgo_clobber_pointer_regs") { - t.Fatalf("compiled liveness module missing clobber helper:\n%s", ir) + for _, helper := range []string{"llgo_clear_stack_ptr", "llgo_clobber_pointer_regs"} { + if strings.Contains(ir, helper) { + t.Fatalf("compiled liveness module must not use %s:\n%s", helper, ir) + } } } diff --git a/runtime/internal/lib/runtime/mfinal.go b/runtime/internal/lib/runtime/mfinal.go index 71afef7b20..a62305c8a2 100644 --- a/runtime/internal/lib/runtime/mfinal.go +++ b/runtime/internal/lib/runtime/mfinal.go @@ -46,14 +46,14 @@ func initFinalizerState() { } func SetFinalizer(obj any, finalizer any) { - objFace := *(*eface)(unsafe.Pointer(&obj)) + objFace := (*eface)(unsafe.Pointer(&obj)) if objFace._type == nil { throw("runtime.SetFinalizer: first argument is nil") } if objFace._type.Kind() != abi.Pointer { throw("runtime.SetFinalizer: first argument is " + objFace._type.String() + ", not pointer") } - objPtr := ifacePointerData(&objFace) + objPtr := ifacePointerData(objFace) if objPtr == nil { throw("runtime.SetFinalizer: first argument is nil") } @@ -69,7 +69,7 @@ func SetFinalizer(obj any, finalizer any) { } finalizerState.mu.Unlock() - finalizerFace := *(*eface)(unsafe.Pointer(&finalizer)) + finalizerFace := (*eface)(unsafe.Pointer(&finalizer)) if finalizerFace._type == nil { return } diff --git a/ssa/memory.go b/ssa/memory.go index 5e6646f87b..6bd8f909c8 100644 --- a/ssa/memory.go +++ b/ssa/memory.go @@ -64,34 +64,6 @@ func (b Builder) aggregateValue(t Type, flds ...llvm.Value) Expr { return Expr{aggregateValue(b.impl, t.ll, flds...), t} } -// LoadAndClearSinglePointer atomically copies a pointer-sized value out of ptr -// and clears the source slot. It handles either *P or *struct{P}. -func (b Builder) LoadAndClearSinglePointer(ptr Expr) (Expr, bool) { - elem := b.Prog.Elem(ptr.Type) - if elem.ll.TypeKind() == llvm.PointerTypeKind { - old := b.loadAndClearPointerWord(ptr.impl, elem.ll) - return Expr{old, elem}, true - } - - st, ok := types.Unalias(elem.RawType()).Underlying().(*types.Struct) - if !ok || st.NumFields() != 1 { - return Nil, false - } - field := b.Prog.rawType(st.Field(0).Type()) - if field.ll.TypeKind() != llvm.PointerTypeKind { - return Nil, false - } - fieldPtr := llvm.CreateStructGEP(b.impl, elem.ll, ptr.impl, 0) - old := b.loadAndClearPointerWord(fieldPtr, field.ll) - return b.aggregateValue(elem, old), true -} - -func (b Builder) loadAndClearPointerWord(ptr llvm.Value, typ llvm.Type) llvm.Value { - old := llvm.CreateLoad(b.impl, typ, ptr) - b.impl.CreateStore(llvm.ConstNull(typ), ptr) - return old -} - func aggregateValue(b llvm.Builder, tll llvm.Type, flds ...llvm.Value) llvm.Value { agg := llvm.Undef(tll) for i, fld := range flds { @@ -417,6 +389,15 @@ func (b Builder) Store(ptr, val Expr) Expr { return Expr{b.impl.CreateStore(val.impl, ptr.impl), b.Prog.Void()} } +// StoreVolatile stores val at ptr without allowing an optimizer to remove or +// combine the store. Conservative GC stack-slot clearing is externally +// observable even when ordinary program dataflow sees no subsequent load. +func (b Builder) StoreVolatile(ptr, val Expr) Expr { + store := b.Store(ptr, val) + store.impl.SetVolatile(true) + return store +} + // Advance returns the pointer ptr advanced by offset. func (b Builder) Advance(ptr Expr, offset Expr) Expr { dbgInstrf("Advance %v, %v\n", ptr.impl, offset.impl) diff --git a/ssa/ssa_test.go b/ssa/ssa_test.go index 0efa654561..2d1b222506 100644 --- a/ssa/ssa_test.go +++ b/ssa/ssa_test.go @@ -1858,71 +1858,6 @@ func TestZeroSizedLoadEmitsNilDerefGuard(t *testing.T) { } } -func TestLoadAndClearSinglePointer(t *testing.T) { - prog := NewProgram(nil) - prog.sizes = types.SizesFor("gc", runtime.GOARCH) - pkg := prog.NewPackage("bar", "foo/bar") - - ptrToInt := types.NewPointer(types.Typ[types.Int]) - wrapStruct := types.NewStruct([]*types.Var{ - types.NewField(token.NoPos, nil, "p", ptrToInt, false), - }, nil) - - params := types.NewTuple( - types.NewVar(0, nil, "p", types.NewPointer(ptrToInt)), - types.NewVar(0, nil, "s", types.NewPointer(wrapStruct)), - ) - results := types.NewTuple( - types.NewVar(0, nil, "", ptrToInt), - types.NewVar(0, nil, "", wrapStruct), - ) - sig := types.NewSignatureType(nil, nil, nil, params, results, false) - fn := pkg.NewFunc("loadAndClear", sig, InGo) - b := fn.MakeBody(1) - pv, ok := b.LoadAndClearSinglePointer(fn.Param(0)) - if !ok { - t.Fatal("pointer slot should be load-and-clearable") - } - sv, ok := b.LoadAndClearSinglePointer(fn.Param(1)) - if !ok { - t.Fatal("single-pointer struct slot should be load-and-clearable") - } - if got, want := sv.impl.Type().String(), sv.Type.ll.String(); got != want { - t.Fatalf("single-pointer struct load-and-clear type = %s, want %s", got, want) - } - b.Return(pv, sv) - b.EndBuild() - - ir := fn.impl.String() - if got := strings.Count(ir, "store ptr null"); got != 2 { - t.Fatalf("LoadAndClearSinglePointer should clear both pointer slots, got %d stores:\n%s", got, ir) - } - if got := strings.Count(ir, "load ptr"); got < 2 { - t.Fatalf("LoadAndClearSinglePointer should load both pointer slots, got %d loads:\n%s", got, ir) - } - - noPtrStruct := types.NewStruct([]*types.Var{ - types.NewField(token.NoPos, nil, "i", types.Typ[types.Int], false), - }, nil) - multiStruct := types.NewStruct([]*types.Var{ - types.NewField(token.NoPos, nil, "p", ptrToInt, false), - types.NewField(token.NoPos, nil, "q", ptrToInt, false), - }, nil) - falseCases := []types.Type{ - types.NewPointer(types.Typ[types.Int]), - types.NewPointer(noPtrStruct), - types.NewPointer(multiStruct), - } - for i, typ := range falseCases { - fn := pkg.NewFunc(fmt.Sprintf("rejectLoadAndClear%d", i), types.NewSignatureType(nil, nil, nil, - types.NewTuple(types.NewVar(0, nil, "p", typ)), nil, false), InGo) - b := fn.MakeBody(1) - if _, ok := b.LoadAndClearSinglePointer(fn.Param(0)); ok { - t.Fatalf("LoadAndClearSinglePointer accepted %v", typ) - } - } -} - func TestTypeAssertSingleElemArrayUsesInsertValue(t *testing.T) { prog := NewProgram(nil) prog.sizes = types.SizesFor("gc", runtime.GOARCH) @@ -2516,6 +2451,24 @@ attributes #0 = { null_pointer_is_valid "frame-pointer"="non-leaf" } `) } +func TestStoreVolatile(t *testing.T) { + prog := NewProgram(nil) + pkg := prog.NewPackage("bar", "foo/bar") + params := types.NewTuple( + types.NewVar(0, nil, "p", types.NewPointer(types.Typ[types.Int32])), + ) + sig := types.NewSignatureType(nil, nil, nil, params, nil, false) + fn := pkg.NewFunc("clear", sig, InGo) + b := fn.MakeBody(1) + b.StoreVolatile(fn.Param(0), prog.IntVal(0, prog.Int32())) + b.Return() + + ir := fn.impl.String() + if !strings.Contains(ir, "store volatile i32 0, ptr %0") { + t.Fatalf("StoreVolatile did not emit a volatile store:\n%s", ir) + } +} + func TestBasicType(t *testing.T) { type typeInfo struct { typ Type diff --git a/test/go/finalizer_liveness_regression_test.go b/test/go/finalizer_liveness_regression_test.go new file mode 100644 index 0000000000..43e5dc904f --- /dev/null +++ b/test/go/finalizer_liveness_regression_test.go @@ -0,0 +1,259 @@ +/* + * 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" + "testing" +) + +const finalizerLivenessProbe = `package main + +import ( + "os" + "runtime" + "time" + "unsafe" +) + +type Box struct { + p *int +} + +type HeapObject [8]int64 + +type StackSlot struct { + p *HeapObject +} + +var ( + savedClosure func() + savedBox *Box + expected uintptr + evalSlot *int +) + +func loopCase() { + x := 42 + var box Box + box.p = &x + for i := 0; i < 3; i++ { + if box.p == nil || *box.p != 42 { + panic("box was cleared while live across loop backedge") + } + } +} + +func closureCase() { + x := 42 + box := Box{p: &x} + savedClosure = func() { + if box.p == nil || *box.p != 42 { + panic("captured heap allocation was cleared") + } + } + savedClosure() +} + +func globalEscapeCase() { + x := 42 + savedBox = &Box{p: &x} + if savedBox.p == nil || *savedBox.p != 42 { + panic("globally escaped heap allocation was cleared") + } +} + +//go:noinline +func checkDeferred(box *Box) { + if box.p == nil || *box.p != 42 { + panic("deferred argument was cleared before RunDefers") + } +} + +func deferCase() { + x := 42 + box := Box{p: &x} + defer checkDeferred(&box) +} + +//go:noinline +func consumeBox(*Box) {} + +//go:noinline +func checkAlias(p *int) { + if p == nil || *p != 42 { + panic("independent live alias was cleared") + } +} + +func aliasCase() { + h := new(int) + *h = 42 + box := Box{p: h} + alias := h + consumeBox(&box) + checkAlias(alias) +} + +func goroutineCase() { + x := 42 + box := Box{p: &x} + start := make(chan struct{}) + done := make(chan struct{}) + go func(p *Box) { + <-start + if p.p == nil || *p.p != 42 { + panic("goroutine argument was cleared before use") + } + close(done) + }(&box) + close(start) + <-done +} + +func uintptrCase() { + h := new(int) + box := Box{p: h} + bits := uintptr(unsafe.Pointer(h)) + expected = bits + consumeBox(&box) + if bits != expected { + panic("live uintptr bits were rewritten by stack scan") + } +} + +func clearEvalSlot() any { + evalSlot = nil + return func(*int) {} +} + +//go:noinline +func loadEvalSlot() *int { + return evalSlot +} + +func evalOrderCase() { + p := new(int) + evalSlot = p + runtime.SetFinalizer(loadEvalSlot(), clearEvalSlot()) + runtime.KeepAlive(p) +} + +func sameBlockFinalizationCase() { + finalized := make(chan struct{}, 1) + var slot StackSlot + slot.p = new(HeapObject) + runtime.SetFinalizer(slot.p, func(*HeapObject) { + finalized <- struct{}{} + }) + + for i := 0; i < 100; i++ { + runtime.GC() + select { + case <-finalized: + return + default: + } + runtime.Gosched() + time.Sleep(10 * time.Millisecond) + } + panic("same-block dead stack slot kept finalizer object alive") +} + +func main() { + if len(os.Args) != 2 { + panic("missing case name") + } + activation := new(int) + runtime.SetFinalizer(activation, func(*int) {}) + switch os.Args[1] { + case "loop": + loopCase() + case "closure": + closureCase() + case "global-escape": + globalEscapeCase() + case "defer": + deferCase() + case "alias": + aliasCase() + case "goroutine": + goroutineCase() + case "uintptr": + uintptrCase() + case "eval-order": + evalOrderCase() + case "same-block-finalization": + sameBlockFinalizationCase() + default: + panic("unknown case") + } + runtime.KeepAlive(activation) +} +` + +func buildFinalizerLivenessProbe(t *testing.T) (hostBin, llgoBin string) { + t.Helper() + dir := t.TempDir() + mainFile := filepath.Join(dir, "main.go") + if err := os.WriteFile(mainFile, []byte(finalizerLivenessProbe), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/finalizerprobe\n\ngo 1.24\n"), 0o644); err != nil { + t.Fatal(err) + } + + hostBin = filepath.Join(dir, "host-probe") + runGoCmd(t, dir, "build", "-o", hostBin, ".") + + llgoBin = filepath.Join(dir, "llgo-probe") + out, err := runLLGoInModule(t, dir, "build", "-o", llgoBin, ".") + if err != nil { + t.Fatalf("llgo build failed: %v\n%s", err, out) + } + return hostBin, llgoBin +} + +func runFinalizerLivenessProbe(t *testing.T, bin, caseName string) { + t.Helper() + out, err := exec.Command(bin, caseName).CombinedOutput() + if err != nil { + t.Fatalf("%s failed: %v\n%s", filepath.Base(bin), err, out) + } +} + +func TestRuntimeSetFinalizerPreservesLiveValues(t *testing.T) { + hostBin, llgoBin := buildFinalizerLivenessProbe(t) + for _, caseName := range []string{ + "loop", + "closure", + "global-escape", + "defer", + "alias", + "goroutine", + "uintptr", + "eval-order", + "same-block-finalization", + } { + t.Run(caseName, func(t *testing.T) { + runFinalizerLivenessProbe(t, hostBin, caseName) + runFinalizerLivenessProbe(t, llgoBin, caseName) + }) + } +} diff --git a/test/go/finalizer_test.go b/test/go/finalizer_test.go index bafbc39f8a..ec986129d9 100644 --- a/test/go/finalizer_test.go +++ b/test/go/finalizer_test.go @@ -17,8 +17,6 @@ package gotest import ( - "os" - "path/filepath" "runtime" "testing" "time" @@ -96,116 +94,6 @@ func TestRuntimeSetFinalizerCancel(t *testing.T) { } } -const finalizerStackLivenessProbe = `package main - -import ( - "fmt" - "runtime" -) - -type HeapObj [8]int64 - -type StkObj struct { - h *HeapObj -} - -var n int -var c int = -1 -var null StkObj -var sink *HeapObj - -func gc() { - runtime.GC() - runtime.GC() - runtime.GC() - n++ -} - -func keepAliveCase() { - c = -1 - n = 0 - f() - gc() - if c != 1 { - panic(fmt.Sprintf("keepalive collection phase = %d, want 1", c)) - } -} - -func f() { - var s StkObj - s.h = new(HeapObj) - runtime.SetFinalizer(s.h, func(h *HeapObj) { - c = n - }) - g(&s) - gc() -} - -func g(s *StkObj) { - gc() - runtime.KeepAlive(s) - gc() -} - -//go:noinline -func use(p *StkObj) { -} - -//go:noinline -func ambiguousArgCase(s StkObj, b bool) { - var p *StkObj - if b { - p = &s - } else { - p = &null - } - use(p) - gc() - sink = p.h - gc() - sink = nil - gc() -} - -func runAmbiguousArgCase(b bool, want int) { - var s StkObj - s.h = new(HeapObj) - c = -1 - n = 0 - runtime.SetFinalizer(s.h, func(h *HeapObj) { - c = n - }) - ambiguousArgCase(s, b) - if c != want { - panic(fmt.Sprintf("ambiguous arg b=%v collection phase = %d, want %d", b, c, want)) - } -} - -func main() { - keepAliveCase() - runAmbiguousArgCase(true, 2) - runAmbiguousArgCase(false, 0) -} -` - -func TestRuntimeSetFinalizerStackObjectLiveness(t *testing.T) { - dir, err := os.MkdirTemp("", "llgo-finalizer-stack-*") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) - mainFile := filepath.Join(dir, "main.go") - if err := os.WriteFile(mainFile, []byte(finalizerStackLivenessProbe), 0644); err != nil { - t.Fatal(err) - } - - runGoCmd(t, dir, "run", mainFile) - - root := findLLGoRoot(t) - t.Setenv("LLGO_ROOT", root) - runGoCmd(t, root, "run", "./cmd/llgo", "run", mainFile) -} - func runGCWithTimeout(t *testing.T) { t.Helper() done := make(chan struct{}) diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index 1c7ebb9ce3..3bb23b9e96 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2788,13 +2788,7 @@ xfails: directive: runoutput case: rangegen.go reason: go1.26 goroot ci-mode runoutput failure on linux/amd64 - - version: go1.24 - platform: darwin/arm64 - directive: run - case: deferfin.go - reason: latest main goroot run failure on darwin/arm64 - - version: go1.25 - platform: darwin/arm64 + - platform: darwin/arm64 directive: run case: heapsampling.go reason: latest main goroot run failure on darwin/arm64 @@ -2814,6 +2808,14 @@ xfails: directive: run case: recover4.go reason: latest main goroot run failure on darwin/arm64 + - platform: darwin/arm64 + directive: run + case: stackobj.go + reason: conservative GC lacks precise cross-frame stack-object liveness + - platform: darwin/arm64 + directive: run + case: stackobj3.go + reason: conservative GC lacks precise ambiguous-parameter liveness - platform: darwin/arm64 directive: run case: fixedbugs/bug347.go @@ -2872,6 +2874,11 @@ xfails: directive: run case: fixedbugs/issue5963.go reason: latest main goroot run failure on darwin/arm64 + - version: go1.25 + platform: linux/amd64 + directive: run + case: deferfin.go + reason: exiting pthread may retain conservative stack or register roots after goroutine completion - version: go1.25 platform: linux/amd64 directive: run @@ -2987,12 +2994,27 @@ xfails: directive: run case: recover4.go reason: go1.25 goroot run failure on linux/amd64 + - version: go1.25 + platform: linux/amd64 + directive: run + case: stackobj.go + reason: conservative GC lacks precise cross-frame stack-object liveness + - version: go1.25 + platform: linux/amd64 + directive: run + case: stackobj3.go + reason: conservative GC lacks precise ambiguous-parameter liveness - version: go1.25 platform: linux/amd64 directive: run case: tinyfin.go reason: go1.25 goroot run failure on linux/amd64 + - version: go1.24 + platform: linux/amd64 + directive: run + case: deferfin.go + reason: exiting pthread may retain conservative stack or register roots after goroutine completion - version: go1.24 platform: linux/amd64 directive: run @@ -3053,6 +3075,16 @@ xfails: directive: run case: recover1.go reason: go1.24 goroot run failure on linux/amd64 + - version: go1.24 + platform: linux/amd64 + directive: run + case: stackobj.go + reason: conservative GC lacks precise cross-frame stack-object liveness + - version: go1.24 + platform: linux/amd64 + directive: run + case: stackobj3.go + reason: conservative GC lacks precise ambiguous-parameter liveness - version: go1.24 platform: linux/amd64 directive: run @@ -3123,6 +3155,11 @@ xfails: directive: run case: maymorestack.go reason: go1.26 goroot ci-mode run failure on darwin/arm64 + - version: go1.26 + platform: linux/amd64 + directive: run + case: deferfin.go + reason: exiting pthread may retain conservative stack or register roots after goroutine completion - version: go1.26 platform: linux/amd64 directive: run @@ -3138,6 +3175,16 @@ xfails: directive: run case: recover4.go reason: go1.26 goroot run failure on linux/amd64 + - version: go1.26 + platform: linux/amd64 + directive: run + case: stackobj.go + reason: conservative GC lacks precise cross-frame stack-object liveness + - version: go1.26 + platform: linux/amd64 + directive: run + case: stackobj3.go + reason: conservative GC lacks precise ambiguous-parameter liveness - version: go1.26 platform: linux/amd64 directive: run From b2a33ae1151a0c5ac010c460fbc2853effdea745 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 29 Jul 2026 16:29:22 +0800 Subject: [PATCH 4/9] cl,runtime: address finalizer liveness review --- cl/compile.go | 11 +- cl/liveness_internal_test.go | 137 +++++++-------------- runtime/internal/lib/runtime/runtime_gc.go | 1 + 3 files changed, 57 insertions(+), 92 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index 3eb7c59360..8cb4bac081 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -1430,6 +1430,7 @@ func (p *context) lastUseInBlock(v ssa.Value, blk *ssa.BasicBlock, order map[ssa func (p *context) collectStackClearPlans(fn *ssa.Function) map[ssa.Instruction][]*ssa.Alloc { plans := make(map[ssa.Instruction][]*ssa.Alloc) + blockCyclicity := make(map[*ssa.BasicBlock]bool) for _, blk := range fn.Blocks { for _, instr := range blk.Instrs { alloc, ok := instr.(*ssa.Alloc) @@ -1441,7 +1442,15 @@ func (p *context) collectStackClearPlans(fn *ssa.Function) map[ssa.Instruction][ // retain stale roots, but it cannot guess across control-flow, // closure, defer, goroutine, or heap-escape boundaries. useBlk := alloc.Block() - if useBlk == nil || blockIsCyclic(useBlk) { + if useBlk == nil { + continue + } + cyclic, ok := blockCyclicity[useBlk] + if !ok { + cyclic = blockIsCyclic(useBlk) + blockCyclicity[useBlk] = cyclic + } + if cyclic { continue } order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) diff --git a/cl/liveness_internal_test.go b/cl/liveness_internal_test.go index 9d338cadfd..fd128d8a14 100644 --- a/cl/liveness_internal_test.go +++ b/cl/liveness_internal_test.go @@ -281,9 +281,11 @@ type Box struct{ p *int } var Sink any func linear(p *int) { - var box Box - box.p = p - Sink = box.p + var first, second Box + first.p = p + second.p = p + Sink = first.p + Sink = second.p Sink = 1 } @@ -323,9 +325,11 @@ func callLocal(p *int) { func cyclicLocal(p *int, n int) { for n > 0 { - var box Box - box.p = p - Sink = box.p + var first, second Box + first.p = p + second.p = p + Sink = first.p + Sink = second.p n-- } } @@ -358,10 +362,18 @@ func phiLocal(p *int, cond bool) { if len(stackPlans) == 0 { t.Fatal("linear should produce stack clear plans") } + var linearAllocs []*ssa.Alloc for instr := range stackPlans { if isTerminatingInstruction(instr) { t.Fatalf("stack clear should not be scheduled after terminator %T", instr) } + linearAllocs = append(linearAllocs, stackPlans[instr]...) + } + if len(linearAllocs) != 2 { + t.Fatalf("linear should plan both same-block allocations, got %d: %v", len(linearAllocs), stackPlans) + } + if linearAllocs[0].Block() != linearAllocs[1].Block() { + t.Fatalf("linear allocations should share a block: %v, %v", linearAllocs[0].Block(), linearAllocs[1].Block()) } for _, name := range []string{"loop", "deferred", "goroutine"} { @@ -382,17 +394,22 @@ func phiLocal(p *int, cond bool) { } cyclicLocal := ssapkg.Func("cyclicLocal") - var cyclicAlloc *ssa.Alloc + var cyclicBlock *ssa.BasicBlock + var cyclicAllocs int for _, alloc := range functionAllocs(cyclicLocal) { - if !alloc.Heap && blockIsCyclic(alloc.Block()) { - cyclicAlloc = alloc - break + if ctx.shouldClearAlloc(alloc) && blockIsCyclic(alloc.Block()) { + if cyclicBlock == nil { + cyclicBlock = alloc.Block() + } + if alloc.Block() == cyclicBlock { + cyclicAllocs++ + } } } - if cyclicAlloc == nil { + if cyclicAllocs < 2 { var dump strings.Builder cyclicLocal.WriteTo(&dump) - t.Fatalf("cyclicLocal should contain a non-heap allocation in a cyclic block:\n%s", dump.String()) + t.Fatalf("cyclicLocal should contain two eligible allocations in one cyclic block, got %d:\n%s", cyclicAllocs, dump.String()) } if got := ctx.collectStackClearPlans(cyclicLocal); len(got) != 0 { t.Fatalf("cyclicLocal should fail closed instead of producing clear plans: %v", got) @@ -772,22 +789,33 @@ func use(p *int) { func TestCompileWithoutConservativeLivenessClears(t *testing.T) { ssapkg, files := buildSSAPackageWithPathAndFiles(t, "command-line-arguments", "main", `package main +type Box struct{ p *int } + +var Sink any + +func clearLocal(p *int) { + var box Box + box.p = p + Sink = box.p + Sink = 1 +} + func main() { x := 1 - _ = &x + clearLocal(&x) } `) + ctx := &context{} + if plans := ctx.collectStackClearPlans(ssapkg.Func("clearLocal")); len(plans) == 0 { + t.Fatal("test fixture should be eligible for conservative liveness clearing") + } + prog := newLLSSAProg(t) pkg, err := NewPackage(prog, ssapkg, files) if err != nil { t.Fatal(err) } - for _, helper := range []string{"llgo_clear_stack_ptr", "llgo_clobber_pointer_regs"} { - if strings.Contains(pkg.String(), helper) { - t.Fatalf("package without SetFinalizer should not emit %s:\n%s", helper, pkg.String()) - } - } if strings.Contains(pkg.String(), "store volatile") { t.Fatalf("package without SetFinalizer should not emit liveness clears:\n%s", pkg.String()) } @@ -824,80 +852,7 @@ func main() { t.Fatal(err) } ir := pkg.String() - for _, helper := range []string{"llgo_clear_stack_ptr", "llgo_clobber_pointer_regs"} { - if strings.Contains(ir, helper) { - t.Fatalf("compiled liveness module must not use %s:\n%s", helper, ir) - } - } - if !strings.Contains(ir, `%"github.com/goplus/llgo/runtime/livetest.Box" = type { ptr, ptr }`) { - t.Fatalf("compiled liveness module missing two-pointer aggregate type:\n%s", ir) - } if !strings.Contains(ir, `store volatile %"github.com/goplus/llgo/runtime/livetest.Box" zeroinitializer`) { t.Fatalf("compiled liveness module missing volatile whole-aggregate clear:\n%s", ir) } } - -func TestCompileConservativeLivenessDoesNotScanWholeStack(t *testing.T) { - ssapkg, files := buildSSAPackageWithPathAndFiles(t, "github.com/goplus/llgo/runtime/livetest", "main", `package main - -import rt "github.com/goplus/llgo/runtime/internal/lib/runtime" -import "unsafe" - -type Cell struct{ p *int } -type Ptr *int - -var Sink any - -func consume(cell Cell) { - Sink = cell.p - Sink = 1 -} - -func consumePtr(p *int) { - Sink = p - Sink = 1 -} - -func branch(cell Cell, cond bool) { - if cond { - Sink = cell.p - } else { - Sink = 0 - } - Sink = 1 -} - -func main() { - x := 1 - y := 2 - arr := [2]*int{&x, &y} - cell := Cell{p: &x} - p := &x - pp := &p - ptr := Ptr(&x) - rt.SetFinalizer(&cell, func(*Cell) {}) - rt.SetFinalizer(&p, func(**int) {}) - rt.SetFinalizer(*pp, nil) - rt.SetFinalizer(&cell.p, func(**int) {}) - rt.SetFinalizer(&arr[0], func(**int) {}) - rt.SetFinalizer(unsafe.Pointer(&x), nil) - rt.SetFinalizer(ptr, nil) - consume(cell) - consumePtr(p) - branch(cell, x == y) -} - `) - ssapkg.Pkg = types.NewPackage("command-line-arguments", "main") - - prog := newLLSSAProg(t) - pkg, err := NewPackage(prog, ssapkg, files) - if err != nil { - t.Fatal(err) - } - ir := pkg.String() - for _, helper := range []string{"llgo_clear_stack_ptr", "llgo_clobber_pointer_regs"} { - if strings.Contains(ir, helper) { - t.Fatalf("compiled liveness module must not use %s:\n%s", helper, ir) - } - } -} diff --git a/runtime/internal/lib/runtime/runtime_gc.go b/runtime/internal/lib/runtime/runtime_gc.go index 810d076ebd..c000c0180b 100644 --- a/runtime/internal/lib/runtime/runtime_gc.go +++ b/runtime/internal/lib/runtime/runtime_gc.go @@ -36,6 +36,7 @@ func ReadMemStats(m *runtime.MemStats) { } func GC() { + // Scrub stale conservative stack roots before each collection cycle. bdwgc.ClearStack(nil) bdwgc.Gcollect() runFinalizers() From 85f7eea530219385517e94ee074d02eb5d6d7c96 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 29 Jul 2026 21:41:56 +0800 Subject: [PATCH 5/9] cl,runtime: fail closed on hidden stack aliases --- cl/compile.go | 96 +++++++++++-- cl/liveness_internal_test.go | 134 +++++++++++++++++- runtime/internal/lib/runtime/runtime_gc.go | 3 +- test/go/finalizer_liveness_regression_test.go | 33 +++-- 4 files changed, 247 insertions(+), 19 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index 8cb4bac081..9e24c59158 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -1339,11 +1339,29 @@ func blockIsCyclic(block *ssa.BasicBlock) bool { return false } -func instructionUsesValue(instr ssa.Instruction, v ssa.Value) bool { +type instructionOperandScratch struct { + inline [8]*ssa.Value + operands []*ssa.Value +} + +type stackLivenessState struct { + value ssa.Value + slotAddress bool +} + +func (s *instructionOperandScratch) uses(instr ssa.Instruction, v ssa.Value) bool { if instr == nil || v == nil { return false } - for _, operand := range instr.Operands(nil) { + if s.operands == nil { + s.operands = s.inline[:0] + } else { + s.operands = s.operands[:0] + } + // Referrer lists are mutable in x/tools. Re-scan operands deliberately + // so malformed or stale referrers make the liveness proof fail closed. + s.operands = instr.Operands(s.operands) + for _, operand := range s.operands { if operand != nil && *operand == v { return true } @@ -1351,6 +1369,40 @@ func instructionUsesValue(instr ssa.Instruction, v ssa.Value) bool { return false } +func instructionUsesValue(instr ssa.Instruction, v ssa.Value) bool { + var scratch instructionOperandScratch + return scratch.uses(instr, v) +} + +func instructionRetainsAddress(instr ssa.Instruction, v ssa.Value) bool { + // Side-effecting instructions can hide a stack address from the SSA + // referrer graph, so the liveness walk cannot follow later aliases. + switch instr := instr.(type) { + case *ssa.Store: + return instr.Val == v + case *ssa.MapUpdate: + return instr.Key == v || instr.Value == v + case *ssa.Send: + return instr.X == v + case *ssa.Call: + if instr.Call.Value == v { + return true + } + for _, arg := range instr.Call.Args { + if arg == v { + return true + } + } + case *ssa.Select: + for _, state := range instr.States { + if state.Dir == types.SendOnly && state.Send == v { + return true + } + } + } + return false +} + func isTerminatingInstruction(instr ssa.Instruction) bool { switch instr.(type) { case *ssa.Jump, *ssa.Return, *ssa.If, *ssa.Panic: @@ -1379,10 +1431,29 @@ func (p *context) isRuntimeSetFinalizerCall(call *ssa.CallCommon) bool { } func (p *context) lastUseInBlock(v ssa.Value, blk *ssa.BasicBlock, order map[ssa.Instruction]int, seen map[ssa.Value]bool) (ssa.Instruction, bool) { - if v == nil || seen[v] { + var scratch instructionOperandScratch + states := make(map[stackLivenessState]bool, len(seen)*2) + for value := range seen { + states[stackLivenessState{value: value}] = true + states[stackLivenessState{value: value, slotAddress: true}] = true + } + _, slotAddress := v.(*ssa.Alloc) + return p.lastUseInBlockValue(v, blk, order, states, slotAddress, &scratch) +} + +func (p *context) lastUseInBlockValue( + v ssa.Value, + blk *ssa.BasicBlock, + order map[ssa.Instruction]int, + seen map[stackLivenessState]bool, + slotAddress bool, + scratch *instructionOperandScratch, +) (ssa.Instruction, bool) { + state := stackLivenessState{value: v, slotAddress: slotAddress} + if v == nil || seen[state] { return nil, true } - seen[v] = true + seen[state] = true refs := v.Referrers() if refs == nil { return nil, true @@ -1400,20 +1471,27 @@ func (p *context) lastUseInBlock(v ssa.Value, blk *ssa.BasicBlock, order map[ssa switch ref := ref.(type) { case *ssa.DebugRef: continue - case *ssa.Defer, *ssa.Go, *ssa.MakeClosure: - return nil, false - case *ssa.Phi: + case *ssa.Defer, *ssa.Go, *ssa.MakeClosure, *ssa.Phi: return nil, false default: instr, ok := ref.(ssa.Instruction) - if !ok || !instructionUsesValue(instr, v) { + if !ok || !scratch.uses(instr, v) { return nil, false } if instr.Block() != blk { return nil, false } + if slotAddress && instructionRetainsAddress(instr, v) { + return nil, false + } if refVal, ok := ref.(ssa.Value); ok { - use, ok := p.lastUseInBlock(refVal, blk, order, seen) + nextSlotAddress := slotAddress + if unop, ok := refVal.(*ssa.UnOp); ok && unop.Op == token.MUL { + // A load copies the slot contents; the result no longer + // aliases the stack storage that will be cleared. + nextSlotAddress = false + } + use, ok := p.lastUseInBlockValue(refVal, blk, order, seen, nextSlotAddress, scratch) if !ok { return nil, false } diff --git a/cl/liveness_internal_test.go b/cl/liveness_internal_test.go index fd128d8a14..0e8ab97fe2 100644 --- a/cl/liveness_internal_test.go +++ b/cl/liveness_internal_test.go @@ -278,7 +278,10 @@ func TestConservativeLivenessPlanCollectors(t *testing.T) { type Box struct{ p *int } -var Sink any +var ( + Sink any + Held *Box +) func linear(p *int) { var first, second Box @@ -355,6 +358,28 @@ func phiLocal(p *int, cond bool) { Sink = box.p Sink = 1 } + +func storedAlias(p *int) { + var box Box + var alias **int + box.p = p + aliasSlot := &alias + *aliasSlot = &box.p + Sink = **aliasSlot + Sink = 1 +} + +func holdBox(box *Box) { + Held = box +} + +func calledAlias(p *int) { + var box Box + box.p = p + holdBox(&box) + Sink = Held.p + Sink = 1 +} `) ctx := &context{} linear := ssapkg.Func("linear") @@ -464,6 +489,80 @@ func phiLocal(p *int, cond bool) { if got := ctx.collectStackClearPlans(phiLocal); len(got) != 0 { t.Fatalf("phiLocal should fail closed instead of producing clear plans: %v", got) } + + findStructAlloc := func(fn *ssa.Function) *ssa.Alloc { + t.Helper() + for _, alloc := range functionAllocs(fn) { + ptr, ok := alloc.Type().Underlying().(*types.Pointer) + if !ok { + continue + } + if _, ok := ptr.Elem().Underlying().(*types.Struct); ok { + return alloc + } + } + var dump strings.Builder + fn.WriteTo(&dump) + t.Fatalf("%s should contain a struct allocation:\n%s", fn.Name(), dump.String()) + return nil + } + + storedAlias := ssapkg.Func("storedAlias") + boxAlloc := findStructAlloc(storedAlias) + var storesFieldPointer bool + for _, ref := range *boxAlloc.Referrers() { + fieldAddr, ok := ref.(*ssa.FieldAddr) + if !ok { + continue + } + for _, block := range storedAlias.Blocks { + for _, instr := range block.Instrs { + if store, ok := instr.(*ssa.Store); ok && store.Val == fieldAddr { + storesFieldPointer = true + } + } + } + } + if !storesFieldPointer { + var dump strings.Builder + storedAlias.WriteTo(&dump) + t.Fatalf("storedAlias should store the address of box.p in another stack slot:\n%s", dump.String()) + } + // Current x/tools marks box as escaping, but do not make safety depend on + // that implementation detail: simulate a less conservative escape result. + boxAlloc.Heap = false + for _, allocs := range ctx.collectStackClearPlans(storedAlias) { + for _, alloc := range allocs { + if alloc == boxAlloc { + t.Fatalf("storedAlias should fail closed for a pointer stored through another stack slot: %v", alloc) + } + } + } + + calledAlias := ssapkg.Func("calledAlias") + calledBoxAlloc := findStructAlloc(calledAlias) + var callsWithAddress bool + for _, ref := range *calledBoxAlloc.Referrers() { + call, ok := ref.(*ssa.Call) + if ok && instructionUsesValue(call, calledBoxAlloc) { + callsWithAddress = true + } + } + if !callsWithAddress { + var dump strings.Builder + calledAlias.WriteTo(&dump) + t.Fatalf("calledAlias should pass the Box address to a call:\n%s", dump.String()) + } + // Likewise, make the call boundary independently fail closed even if a + // future SSA builder no longer heap-promotes the explicit address. + calledBoxAlloc.Heap = false + for _, allocs := range ctx.collectStackClearPlans(calledAlias) { + for _, alloc := range allocs { + if alloc == calledBoxAlloc { + t.Fatalf("calledAlias should fail closed when a call can retain the stack address: %v", alloc) + } + } + } } func TestConservativeLivenessGraphHelpers(t *testing.T) { @@ -517,6 +616,39 @@ func loop(p *int) { if !isTerminatingInstruction(fn.Blocks[0].Instrs[len(fn.Blocks[0].Instrs)-1]) { t.Fatal("entry block should end with a terminator") } + for name, instr := range map[string]ssa.Instruction{ + "store": &ssa.Store{Val: fn.Params[0]}, + "map-key": &ssa.MapUpdate{Key: fn.Params[0]}, + "map-value": &ssa.MapUpdate{Value: fn.Params[0]}, + "channel": &ssa.Send{X: fn.Params[0]}, + "call": &ssa.Call{Call: ssa.CallCommon{ + Args: []ssa.Value{fn.Params[0]}, + }}, + "call-value": &ssa.Call{Call: ssa.CallCommon{ + Value: fn.Params[0], + }}, + "select": &ssa.Select{States: []*ssa.SelectState{{ + Dir: types.SendOnly, + Send: fn.Params[0], + }}}, + } { + if !instructionRetainsAddress(instr, fn.Params[0]) { + t.Errorf("%s should retain an address", name) + } + } + for name, instr := range map[string]ssa.Instruction{ + "store-address": &ssa.Store{Addr: fn.Params[0]}, + "map": &ssa.MapUpdate{Map: fn.Params[0]}, + "channel": &ssa.Send{Chan: fn.Params[0]}, + "select-channel": &ssa.Select{States: []*ssa.SelectState{{ + Dir: types.RecvOnly, + Chan: fn.Params[0], + }}}, + } { + if instructionRetainsAddress(instr, fn.Params[0]) { + t.Errorf("%s should not treat its destination as a stored address", name) + } + } ctx := &context{} if last, ok := ctx.lastUseInBlock(nil, fn.Blocks[0], map[ssa.Instruction]int{}, map[ssa.Value]bool{}); !ok || last != nil { diff --git a/runtime/internal/lib/runtime/runtime_gc.go b/runtime/internal/lib/runtime/runtime_gc.go index c000c0180b..5a5d6d315b 100644 --- a/runtime/internal/lib/runtime/runtime_gc.go +++ b/runtime/internal/lib/runtime/runtime_gc.go @@ -36,7 +36,8 @@ func ReadMemStats(m *runtime.MemStats) { } func GC() { - // Scrub stale conservative stack roots before each collection cycle. + // Scrub stale conservative pointers from unused stack space before each + // collection cycle. bdwgc.ClearStack(nil) bdwgc.Gcollect() runFinalizers() diff --git a/test/go/finalizer_liveness_regression_test.go b/test/go/finalizer_liveness_regression_test.go index 43e5dc904f..57ab450dab 100644 --- a/test/go/finalizer_liveness_regression_test.go +++ b/test/go/finalizer_liveness_regression_test.go @@ -38,9 +38,7 @@ type Box struct { type HeapObject [8]int64 -type StackSlot struct { - p *HeapObject -} +type StackSlots [8]*HeapObject var ( savedClosure func() @@ -155,11 +153,14 @@ func evalOrderCase() { runtime.KeepAlive(p) } -func sameBlockFinalizationCase() { +//go:noinline +func sameBlockFinalizationCase(writeIndex, readIndex int) { finalized := make(chan struct{}, 1) - var slot StackSlot - slot.p = new(HeapObject) - runtime.SetFinalizer(slot.p, func(*HeapObject) { + var slots StackSlots + // Keep the dynamic store and load distinct in SSA while the caller supplies + // the same index, so this exercises clearing the exact dead stack allocation. + slots[writeIndex] = new(HeapObject) + runtime.SetFinalizer(slots[readIndex], func(*HeapObject) { finalized <- struct{}{} }) @@ -176,6 +177,18 @@ func sameBlockFinalizationCase() { panic("same-block dead stack slot kept finalizer object alive") } +func storedAliasCase() { + x := 42 + var box Box + var alias **int + box.p = &x + aliasSlot := &alias + *aliasSlot = &box.p + if **aliasSlot == nil || ***aliasSlot != 42 { + panic("stack slot was cleared before a stored alias read") + } +} + func main() { if len(os.Args) != 2 { panic("missing case name") @@ -200,7 +213,10 @@ func main() { case "eval-order": evalOrderCase() case "same-block-finalization": - sameBlockFinalizationCase() + index := len(os.Args[1]) & (len(StackSlots{}) - 1) + sameBlockFinalizationCase(index, index) + case "stored-alias": + storedAliasCase() default: panic("unknown case") } @@ -250,6 +266,7 @@ func TestRuntimeSetFinalizerPreservesLiveValues(t *testing.T) { "uintptr", "eval-order", "same-block-finalization", + "stored-alias", } { t.Run(caseName, func(t *testing.T) { runFinalizerLivenessProbe(t, hostBin, caseName) From 878e07b491f05755e60d10cf9b9f3cf4a2d4d16d Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 29 Jul 2026 23:25:13 +0800 Subject: [PATCH 6/9] cl,runtime: clarify liveness safety invariants --- cl/compile.go | 14 +++++++++++++- runtime/internal/lib/runtime/runtime_gc.go | 7 +++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index 9e24c59158..c27aaf6ead 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -1377,6 +1377,12 @@ func instructionUsesValue(instr ssa.Instruction, v ssa.Value) bool { func instructionRetainsAddress(instr ssa.Instruction, v ssa.Value) bool { // Side-effecting instructions can hide a stack address from the SSA // referrer graph, so the liveness walk cannot follow later aliases. + // + // Keep this switch in sync with the SSA instruction set: every instruction + // that can persist v beyond the current instruction must either be handled + // here, produce an ssa.Value whose uses the recursive walk can follow, or + // make the analysis fail closed. In particular, a new side-effecting, + // non-ssa.Value instruction that retains an operand must be added here. switch instr := instr.(type) { case *ssa.Store: return instr.Val == v @@ -1545,7 +1551,13 @@ func (p *context) collectStackClearPlans(fn *ssa.Function) map[ssa.Instruction][ } func (p *context) clearAlloc(b llssa.Builder, alloc *ssa.Alloc) { - ptr := p.compileValue(b, alloc) + // Eligible allocs are lowered before their later clear sites. Reuse that + // exact stack pointer; rematerializing the alloc here would clear unrelated + // storage and invalidate the liveness proof. + ptr, ok := p.bvals[alloc] + if !ok { + log.Panicln("stack clear for unmaterialized alloc:", alloc) + } elem := b.Prog.Elem(ptr.Type) b.StoreVolatile(ptr, p.prog.Zero(elem)) } diff --git a/runtime/internal/lib/runtime/runtime_gc.go b/runtime/internal/lib/runtime/runtime_gc.go index 5a5d6d315b..79bd5c3d87 100644 --- a/runtime/internal/lib/runtime/runtime_gc.go +++ b/runtime/internal/lib/runtime/runtime_gc.go @@ -36,14 +36,17 @@ func ReadMemStats(m *runtime.MemStats) { } func GC() { - // Scrub stale conservative pointers from unused stack space before each - // collection cycle. + // GC_clear_stack only scrubs the unused stack tail below this frame. It + // cannot reach dead slots in active callers; compiler-emitted volatile + // clears handle those. This remains useful as best-effort cleanup for + // storage vacated before GC was entered. bdwgc.ClearStack(nil) bdwgc.Gcollect() runFinalizers() // BDW finalizers are observed on a subsequent collection cycle. // Run one extra cycle so weak-pointer cleanup hooks (unique/weak) see // finalized state before we trigger map cleanup callbacks. + // Scrub the unused tail again before that second collection. bdwgc.ClearStack(nil) bdwgc.Gcollect() runFinalizers() From 5e9b6cd7e1c8d42c9311d6fc8a4b86d4777e6da3 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 01:18:28 +0800 Subject: [PATCH 7/9] cl,runtime: reject unscheduled liveness referrers --- cl/compile.go | 11 +++++++++-- cl/liveness_internal_test.go | 5 +++++ runtime/internal/lib/runtime/runtime_gc.go | 10 +++++----- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index c27aaf6ead..fc74368231 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -1358,8 +1358,8 @@ func (s *instructionOperandScratch) uses(instr ssa.Instruction, v ssa.Value) boo } else { s.operands = s.operands[:0] } - // Referrer lists are mutable in x/tools. Re-scan operands deliberately - // so malformed or stale referrers make the liveness proof fail closed. + // Referrer lists are mutable in x/tools. Re-scan operands deliberately so + // stale entries that no longer name v make the liveness proof fail closed. s.operands = instr.Operands(s.operands) for _, operand := range s.operands { if operand != nil && *operand == v { @@ -1464,6 +1464,10 @@ func (p *context) lastUseInBlockValue( if refs == nil { return nil, true } + // x/tools defines Referrers for function-local values as the inverse of + // Instruction.Operands. Rely on that builder contract for completeness, + // but reject stale entries that no longer name v or are no longer + // scheduled in this block. var last ssa.Instruction updateLast := func(instr ssa.Instruction) { if instr == nil { @@ -1487,6 +1491,9 @@ func (p *context) lastUseInBlockValue( if instr.Block() != blk { return nil, false } + if _, ok := order[instr]; !ok { + return nil, false + } if slotAddress && instructionRetainsAddress(instr, v) { return nil, false } diff --git a/cl/liveness_internal_test.go b/cl/liveness_internal_test.go index 0e8ab97fe2..549ddd8348 100644 --- a/cl/liveness_internal_test.go +++ b/cl/liveness_internal_test.go @@ -848,6 +848,11 @@ func use(p *int) { } } + t.Run("missing-order-entry", func(t *testing.T) { + if last, ok := ctx.lastUseInBlock(param, fn.Blocks[0], map[ssa.Instruction]int{}, map[ssa.Value]bool{}); ok || last != nil { + t.Fatalf("lastUseInBlock with unscheduled referrer = %v, %v; want failure", last, ok) + } + }) t.Run("unary", func(t *testing.T) { check(t, &ssa.UnOp{Op: token.SUB, X: param}) }) diff --git a/runtime/internal/lib/runtime/runtime_gc.go b/runtime/internal/lib/runtime/runtime_gc.go index 79bd5c3d87..d7b48ec0a5 100644 --- a/runtime/internal/lib/runtime/runtime_gc.go +++ b/runtime/internal/lib/runtime/runtime_gc.go @@ -36,17 +36,17 @@ func ReadMemStats(m *runtime.MemStats) { } func GC() { - // GC_clear_stack only scrubs the unused stack tail below this frame. It - // cannot reach dead slots in active callers; compiler-emitted volatile - // clears handle those. This remains useful as best-effort cleanup for - // storage vacated before GC was entered. + // GC_clear_stack only scrubs some inaccessible stack space below this + // frame. It cannot reach dead slots in active callers; compiler-emitted + // volatile clears handle those. This remains useful as best-effort cleanup + // for storage vacated before GC was entered. bdwgc.ClearStack(nil) bdwgc.Gcollect() runFinalizers() // BDW finalizers are observed on a subsequent collection cycle. // Run one extra cycle so weak-pointer cleanup hooks (unique/weak) see // finalized state before we trigger map cleanup callbacks. - // Scrub the unused tail again before that second collection. + // Scrub some inaccessible stack space again before that second collection. bdwgc.ClearStack(nil) bdwgc.Gcollect() runFinalizers() From e538d817daac1b9d7793f9957c0099c9edb9d45c Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 03:23:29 +0800 Subject: [PATCH 8/9] cl: fail closed on unknown liveness instructions --- cl/compile.go | 154 +++++++++++++++++++++++------------ cl/liveness_internal_test.go | 56 +++++++------ 2 files changed, 132 insertions(+), 78 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index fc74368231..592466076d 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -1308,35 +1308,74 @@ func (p *context) shouldClearAlloc(v *ssa.Alloc) bool { return ok && hasConservativeGCPointers(ptr.Elem(), map[types.Type]bool{}) } -func blockCanReach(from, to *ssa.BasicBlock, seen map[*ssa.BasicBlock]bool) bool { - if from == nil || to == nil { - return false - } - if from == to { - return true - } - if seen[from] { - return false - } - seen[from] = true - for _, succ := range from.Succs { - if blockCanReach(succ, to, seen) { - return true +func cyclicBlocks(blocks []*ssa.BasicBlock) map[*ssa.BasicBlock]bool { + // Compute strongly connected components once per function so liveness + // candidates do not repeat reachability walks over the same CFG. + cyclic := make(map[*ssa.BasicBlock]bool) + indices := make(map[*ssa.BasicBlock]int, len(blocks)) + lowlinks := make(map[*ssa.BasicBlock]int, len(blocks)) + onStack := make(map[*ssa.BasicBlock]bool, len(blocks)) + stack := make([]*ssa.BasicBlock, 0, len(blocks)) + nextIndex := 1 + + var visit func(*ssa.BasicBlock) + visit = func(block *ssa.BasicBlock) { + if block == nil { + return } - } - return false -} + index := nextIndex + nextIndex++ + indices[block] = index + lowlinks[block] = index + stack = append(stack, block) + onStack[block] = true -func blockIsCyclic(block *ssa.BasicBlock) bool { - if block == nil { - return false + for _, succ := range block.Succs { + if succ == nil { + continue + } + if indices[succ] == 0 { + visit(succ) + lowlinks[block] = min(lowlinks[block], lowlinks[succ]) + } else if onStack[succ] { + lowlinks[block] = min(lowlinks[block], indices[succ]) + } + } + if lowlinks[block] != index { + return + } + + var component []*ssa.BasicBlock + for { + last := len(stack) - 1 + member := stack[last] + stack = stack[:last] + onStack[member] = false + component = append(component, member) + if member == block { + break + } + } + if len(component) > 1 { + for _, member := range component { + cyclic[member] = true + } + return + } + for _, succ := range block.Succs { + if succ == block { + cyclic[block] = true + return + } + } } - for _, succ := range block.Succs { - if blockCanReach(succ, block, map[*ssa.BasicBlock]bool{}) { - return true + + for _, block := range blocks { + if block != nil && indices[block] == 0 { + visit(block) } } - return false + return cyclic } type instructionOperandScratch struct { @@ -1378,35 +1417,51 @@ func instructionRetainsAddress(instr ssa.Instruction, v ssa.Value) bool { // Side-effecting instructions can hide a stack address from the SSA // referrer graph, so the liveness walk cannot follow later aliases. // - // Keep this switch in sync with the SSA instruction set: every instruction - // that can persist v beyond the current instruction must either be handled - // here, produce an ssa.Value whose uses the recursive walk can follow, or - // make the analysis fail closed. In particular, a new side-effecting, - // non-ssa.Value instruction that retains an operand must be added here. + // This switch deliberately defaults to retaining: every known instruction + // must either identify its non-retaining destination operand below or be a + // pure value instruction whose uses the recursive walk can follow. A new + // SSA instruction therefore fails closed until it is classified here. switch instr := instr.(type) { case *ssa.Store: - return instr.Val == v + if instr.Val == v { + return true + } + return instr.Addr != v case *ssa.MapUpdate: - return instr.Key == v || instr.Value == v - case *ssa.Send: - return instr.X == v - case *ssa.Call: - if instr.Call.Value == v { + if instr.Key == v || instr.Value == v { return true } - for _, arg := range instr.Call.Args { - if arg == v { - return true - } + return instr.Map != v + case *ssa.Send: + if instr.X == v { + return true } + return instr.Chan != v + case *ssa.Call: + // Calls may retain any operand, including invoke receivers. + return true case *ssa.Select: + channelOperand := false for _, state := range instr.States { if state.Dir == types.SendOnly && state.Send == v { return true } + if state.Chan == v { + channelOperand = true + } } + return !channelOperand + case *ssa.Alloc, *ssa.BinOp, *ssa.UnOp, *ssa.ChangeType, + *ssa.Convert, *ssa.MultiConvert, *ssa.ChangeInterface, + *ssa.SliceToArrayPointer, *ssa.MakeInterface, *ssa.MakeMap, + *ssa.MakeChan, *ssa.MakeSlice, *ssa.Slice, *ssa.FieldAddr, + *ssa.Field, *ssa.IndexAddr, *ssa.Index, *ssa.Lookup, *ssa.Range, + *ssa.Next, *ssa.TypeAssert, *ssa.Extract: + // These instructions only produce values. Recursively walking the + // result's referrers preserves address provenance until a load. + return false } - return false + return true } func isTerminatingInstruction(instr ssa.Instruction) bool { @@ -1436,13 +1491,9 @@ func (p *context) isRuntimeSetFinalizerCall(call *ssa.CallCommon) bool { } } -func (p *context) lastUseInBlock(v ssa.Value, blk *ssa.BasicBlock, order map[ssa.Instruction]int, seen map[ssa.Value]bool) (ssa.Instruction, bool) { +func (p *context) lastUseInBlock(v ssa.Value, blk *ssa.BasicBlock, order map[ssa.Instruction]int) (ssa.Instruction, bool) { var scratch instructionOperandScratch - states := make(map[stackLivenessState]bool, len(seen)*2) - for value := range seen { - states[stackLivenessState{value: value}] = true - states[stackLivenessState{value: value, slotAddress: true}] = true - } + states := make(map[stackLivenessState]bool) _, slotAddress := v.(*ssa.Alloc) return p.lastUseInBlockValue(v, blk, order, states, slotAddress, &scratch) } @@ -1521,7 +1572,7 @@ func (p *context) lastUseInBlockValue( func (p *context) collectStackClearPlans(fn *ssa.Function) map[ssa.Instruction][]*ssa.Alloc { plans := make(map[ssa.Instruction][]*ssa.Alloc) - blockCyclicity := make(map[*ssa.BasicBlock]bool) + blockCyclicity := cyclicBlocks(fn.Blocks) for _, blk := range fn.Blocks { for _, instr := range blk.Instrs { alloc, ok := instr.(*ssa.Alloc) @@ -1536,19 +1587,14 @@ func (p *context) collectStackClearPlans(fn *ssa.Function) map[ssa.Instruction][ if useBlk == nil { continue } - cyclic, ok := blockCyclicity[useBlk] - if !ok { - cyclic = blockIsCyclic(useBlk) - blockCyclicity[useBlk] = cyclic - } - if cyclic { + if blockCyclicity[useBlk] { continue } order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) for i, useInstr := range useBlk.Instrs { order[useInstr] = i } - last, ok := p.lastUseInBlock(alloc, useBlk, order, map[ssa.Value]bool{}) + last, ok := p.lastUseInBlock(alloc, useBlk, order) if ok && last != nil && !isTerminatingInstruction(last) { plans[last] = append(plans[last], alloc) } diff --git a/cl/liveness_internal_test.go b/cl/liveness_internal_test.go index 549ddd8348..5b54e8774c 100644 --- a/cl/liveness_internal_test.go +++ b/cl/liveness_internal_test.go @@ -419,10 +419,11 @@ func calledAlias(p *int) { } cyclicLocal := ssapkg.Func("cyclicLocal") + cyclicLocalBlocks := cyclicBlocks(cyclicLocal.Blocks) var cyclicBlock *ssa.BasicBlock var cyclicAllocs int for _, alloc := range functionAllocs(cyclicLocal) { - if ctx.shouldClearAlloc(alloc) && blockIsCyclic(alloc.Block()) { + if ctx.shouldClearAlloc(alloc) && cyclicLocalBlocks[alloc.Block()] { if cyclicBlock == nil { cyclicBlock = alloc.Block() } @@ -591,20 +592,25 @@ func loop(p *int) { } `) fn := ssapkg.Func("flow") - if blockCanReach(nil, fn.Blocks[0], map[*ssa.BasicBlock]bool{}) { - t.Fatal("nil block should not reach anything") + if got := cyclicBlocks(nil); len(got) != 0 { + t.Fatalf("nil block list should have no cycles: %v", got) } - if !blockCanReach(fn.Blocks[0], fn.Blocks[0], map[*ssa.BasicBlock]bool{}) { - t.Fatal("block should reach itself") + cycleA, cycleB, acyclic := &ssa.BasicBlock{}, &ssa.BasicBlock{}, &ssa.BasicBlock{} + cycleA.Succs = []*ssa.BasicBlock{cycleB} + cycleB.Succs = []*ssa.BasicBlock{cycleA, acyclic} + if got := cyclicBlocks([]*ssa.BasicBlock{cycleA, cycleB, acyclic, nil}); !got[cycleA] || !got[cycleB] || got[acyclic] { + t.Fatalf("SCC cycle classification = %v", got) } - loop := ssapkg.Func("loop") - var cyclic int - for _, block := range loop.Blocks { - if blockIsCyclic(block) { - cyclic++ - } + selfLoop := &ssa.BasicBlock{} + selfLoop.Succs = []*ssa.BasicBlock{selfLoop} + if got := cyclicBlocks([]*ssa.BasicBlock{selfLoop}); !got[selfLoop] { + t.Fatalf("self-loop classification = %v", got) } - if cyclic == 0 { + if got := cyclicBlocks(fn.Blocks); len(got) != 0 { + t.Fatalf("flow should have no cyclic blocks: %v", got) + } + loop := ssapkg.Func("loop") + if cyclic := cyclicBlocks(loop.Blocks); len(cyclic) == 0 { t.Fatal("loop should contain at least one cyclic block") } if instructionUsesValue(nil, fn.Params[0]) { @@ -627,6 +633,7 @@ func loop(p *int) { "call-value": &ssa.Call{Call: ssa.CallCommon{ Value: fn.Params[0], }}, + "unclassified-non-value": &ssa.Return{Results: []ssa.Value{fn.Params[0]}}, "select": &ssa.Select{States: []*ssa.SelectState{{ Dir: types.SendOnly, Send: fn.Params[0], @@ -640,6 +647,7 @@ func loop(p *int) { "store-address": &ssa.Store{Addr: fn.Params[0]}, "map": &ssa.MapUpdate{Map: fn.Params[0]}, "channel": &ssa.Send{Chan: fn.Params[0]}, + "pure-value": &ssa.ChangeType{X: fn.Params[0]}, "select-channel": &ssa.Select{States: []*ssa.SelectState{{ Dir: types.RecvOnly, Chan: fn.Params[0], @@ -651,7 +659,7 @@ func loop(p *int) { } ctx := &context{} - if last, ok := ctx.lastUseInBlock(nil, fn.Blocks[0], map[ssa.Instruction]int{}, map[ssa.Value]bool{}); !ok || last != nil { + if last, ok := ctx.lastUseInBlock(nil, fn.Blocks[0], map[ssa.Instruction]int{}); !ok || last != nil { t.Fatalf("lastUseInBlock(nil) = %v, %v", last, ok) } @@ -672,7 +680,7 @@ func loop(p *int) { for i, instr := range block.Instrs { order[instr] = i } - if last, ok := ctx.lastUseInBlock(withCall.Params[0], block, order, map[ssa.Value]bool{}); !ok || last != call { + if last, ok := ctx.lastUseInBlock(withCall.Params[0], block, order); !ok || last != call { t.Fatalf("lastUseInBlock(call parameter) = %v, %v; want call", last, ok) } } @@ -712,8 +720,8 @@ func derefOnly(p **int) { if len(branch.Blocks) < 2 { t.Fatalf("branch should have successors:\n%s", branch.String()) } - if blockCanReach(branch.Blocks[0], branch.Blocks[1], map[*ssa.BasicBlock]bool{branch.Blocks[0]: true}) { - t.Fatal("seen entry block should stop reachability recursion") + if got := cyclicBlocks(branch.Blocks); len(got) != 0 { + t.Fatalf("branch should have no cyclic blocks: %v", got) } useOne := ssapkg.Func("useOne") @@ -736,7 +744,7 @@ func derefOnly(p **int) { t.Fatal("instruction using p should not report use of q") } global := ssapkg.Members["Sink"].(*ssa.Global) - if last, ok := ctx.lastUseInBlock(global, useOne.Blocks[0], map[ssa.Instruction]int{}, map[ssa.Value]bool{}); !ok || last != nil { + if last, ok := ctx.lastUseInBlock(global, useOne.Blocks[0], map[ssa.Instruction]int{}); !ok || last != nil { t.Fatalf("lastUseInBlock(global) = %v, %v", last, ok) } @@ -760,7 +768,7 @@ func derefOnly(p **int) { for i, instr := range negInstr.Block().Instrs { order[instr] = i } - if last, ok := ctx.lastUseInBlock(neg.Params[0], negInstr.Block(), order, map[ssa.Value]bool{}); !ok { + if last, ok := ctx.lastUseInBlock(neg.Params[0], negInstr.Block(), order); !ok { t.Fatalf("lastUseInBlock(neg param) = %v, %v", last, ok) } else if _, ok := last.(*ssa.Return); !ok { t.Fatalf("lastUseInBlock(neg param) = %T; want return", last) @@ -786,7 +794,7 @@ func derefOnly(p **int) { for i, instr := range deref.Block().Instrs { order[instr] = i } - if last, ok := ctx.lastUseInBlock(callDeref.Params[0], deref.Block(), order, map[ssa.Value]bool{}); !ok { + if last, ok := ctx.lastUseInBlock(callDeref.Params[0], deref.Block(), order); !ok { t.Fatalf("lastUseInBlock(call deref param) = %v, %v", last, ok) } else if _, ok := last.(*ssa.Call); !ok { t.Fatalf("lastUseInBlock(call deref param) = %T; want call", last) @@ -812,7 +820,7 @@ func derefOnly(p **int) { for i, instr := range loneDeref.Block().Instrs { order[instr] = i } - if last, ok := ctx.lastUseInBlock(derefOnly.Params[0], loneDeref.Block(), order, map[ssa.Value]bool{}); !ok || last != loneDeref { + if last, ok := ctx.lastUseInBlock(derefOnly.Params[0], loneDeref.Block(), order); !ok || last != loneDeref { t.Fatalf("lastUseInBlock(lone deref param) = %v, %v; want deref", last, ok) } } @@ -843,13 +851,13 @@ func use(p *int) { for i, instr := range fn.Blocks[0].Instrs { order[instr] = i } - if last, ok := ctx.lastUseInBlock(param, fn.Blocks[0], order, map[ssa.Value]bool{}); ok || last != nil { + if last, ok := ctx.lastUseInBlock(param, fn.Blocks[0], order); ok || last != nil { t.Fatalf("lastUseInBlock with malformed referrer = %v, %v; want failure", last, ok) } } t.Run("missing-order-entry", func(t *testing.T) { - if last, ok := ctx.lastUseInBlock(param, fn.Blocks[0], map[ssa.Instruction]int{}, map[ssa.Value]bool{}); ok || last != nil { + if last, ok := ctx.lastUseInBlock(param, fn.Blocks[0], map[ssa.Instruction]int{}); ok || last != nil { t.Fatalf("lastUseInBlock with unscheduled referrer = %v, %v; want failure", last, ok) } }) @@ -884,7 +892,7 @@ func use(p *int) { for i, instr := range fn.Blocks[0].Instrs { order[instr] = i } - if last, ok := ctx.lastUseInBlock(param, fn.Blocks[0], order, map[ssa.Value]bool{}); ok || last != nil { + if last, ok := ctx.lastUseInBlock(param, fn.Blocks[0], order); ok || last != nil { t.Fatalf("lastUseInBlock with malformed derived referrer = %v, %v; want failure", last, ok) } }) @@ -918,7 +926,7 @@ func use(p *int) { for i, instr := range fn.Blocks[0].Instrs { order[instr] = i } - if last, ok := ctx.lastUseInBlock(fn.Params[0], fn.Blocks[0], order, map[ssa.Value]bool{}); !ok || last == nil { + if last, ok := ctx.lastUseInBlock(fn.Params[0], fn.Blocks[0], order); !ok || last == nil { t.Fatalf("lastUseInBlock with DebugRef = %v, %v", last, ok) } } From af7157782696d8cd5a21e0c19cfa2296ce875047 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 04:07:54 +0800 Subject: [PATCH 9/9] cl: reuse liveness order per block --- cl/compile.go | 17 ++++++++++------- cl/liveness_internal_test.go | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index 592466076d..777affdc15 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -1574,6 +1574,10 @@ func (p *context) collectStackClearPlans(fn *ssa.Function) map[ssa.Instruction][ plans := make(map[ssa.Instruction][]*ssa.Alloc) blockCyclicity := cyclicBlocks(fn.Blocks) for _, blk := range fn.Blocks { + if blockCyclicity[blk] { + continue + } + var order map[ssa.Instruction]int for _, instr := range blk.Instrs { alloc, ok := instr.(*ssa.Alloc) if !ok || !p.shouldClearAlloc(alloc) { @@ -1584,15 +1588,14 @@ func (p *context) collectStackClearPlans(fn *ssa.Function) map[ssa.Instruction][ // retain stale roots, but it cannot guess across control-flow, // closure, defer, goroutine, or heap-escape boundaries. useBlk := alloc.Block() - if useBlk == nil { + if useBlk == nil || useBlk != blk { continue } - if blockCyclicity[useBlk] { - continue - } - order := make(map[ssa.Instruction]int, len(useBlk.Instrs)) - for i, useInstr := range useBlk.Instrs { - order[useInstr] = i + if order == nil { + order = make(map[ssa.Instruction]int, len(blk.Instrs)) + for i, useInstr := range blk.Instrs { + order[useInstr] = i + } } last, ok := p.lastUseInBlock(alloc, useBlk, order) if ok && last != nil && !isTerminatingInstruction(last) { diff --git a/cl/liveness_internal_test.go b/cl/liveness_internal_test.go index 5b54e8774c..423f1a2ec6 100644 --- a/cl/liveness_internal_test.go +++ b/cl/liveness_internal_test.go @@ -997,7 +997,7 @@ func main() { t.Fatal(err) } ir := pkg.String() - if !strings.Contains(ir, `store volatile %"github.com/goplus/llgo/runtime/livetest.Box" zeroinitializer`) { + if !strings.Contains(ir, "store volatile %main.Box zeroinitializer") { t.Fatalf("compiled liveness module missing volatile whole-aggregate clear:\n%s", ir) } }