defer: enable loop lowering

This commit is contained in:
Li Jie
2025-10-15 13:36:52 +08:00
parent 3f8c95cf87
commit 16709411a0
18 changed files with 418 additions and 52 deletions

View File

@@ -261,7 +261,7 @@ All Go syntax (including `cgo`) is already supported. Here are some examples:
### Defer
LLGo `defer` does not support usage in loops. This is not a bug but a feature, because we think that using `defer` in a loop is a very unrecommended practice.
LLGo now supports `defer` within loops, matching Go's semantics of executing defers in LIFO order for every iteration. The usual caveat from Go still applies: be mindful of loop-heavy defer usage because it allocates per iteration.
### Garbage Collection (GC)

View File

@@ -300,6 +300,9 @@ github_com_goplus_llgo_runtime_internal_clite_pthread_sync_init(void) GO_SYMBOL_
void
github_com_goplus_llgo_runtime_internal_clite_signal_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/clite/signal.init")
void
github_com_goplus_llgo_runtime_internal_clite_tls_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/clite/tls.init")
void
github_com_goplus_llgo_runtime_internal_runtime_goarch_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/runtime/goarch.init")

View File

@@ -0,0 +1,51 @@
package main
func main() {
for _, label := range complexOrder() {
println(label)
}
}
func complexOrder() (res []string) {
record := func(label string) { res = append(res, label) }
defer record(label1("cleanup-final", 0))
defer record(label1("cleanup-before-loop", 0))
for i := 0; i < 2; i++ {
defer record(label1("exit-outer", i))
for j := 0; j < 2; j++ {
if j == 0 {
defer record(label2("branch-even", i, j))
} else {
defer record(label2("branch-odd", i, j))
}
for k := 0; k < 2; k++ {
nested := label3("nested", i, j, k)
defer record(nested)
if k == 1 {
defer record(label3("nested-tail", i, j, k))
}
}
}
}
defer record(label1("post-loop", 0))
return
}
func label1(prefix string, a int) string {
return prefix + "-" + digit(a)
}
func label2(prefix string, a, b int) string {
return prefix + "-" + digit(a) + "-" + digit(b)
}
func label3(prefix string, a, b, c int) string {
return prefix + "-" + digit(a) + "-" + digit(b) + "-" + digit(c)
}
func digit(n int) string {
return string(rune('0' + n))
}

View File

@@ -0,0 +1 @@
;

View File

@@ -0,0 +1,7 @@
package main
func main() {
for i := 0; i < 3; i++ {
defer println("loop", i)
}
}

View File

@@ -0,0 +1 @@
;

View File

@@ -98,18 +98,19 @@ type pkgInfo struct {
type none = struct{}
type context struct {
prog llssa.Program
pkg llssa.Package
fn llssa.Function
fset *token.FileSet
goProg *ssa.Program
goTyps *types.Package
goPkg *ssa.Package
pyMod string
skips map[string]none
loaded map[*types.Package]*pkgInfo // loaded packages
bvals map[ssa.Value]llssa.Expr // block values
vargs map[*ssa.Alloc][]llssa.Expr // varargs
prog llssa.Program
pkg llssa.Package
fn llssa.Function
fset *token.FileSet
goProg *ssa.Program
goTyps *types.Package
goPkg *ssa.Package
pyMod string
skips map[string]none
loaded map[*types.Package]*pkgInfo // loaded packages
bvals map[ssa.Value]llssa.Expr // block values
vargs map[*ssa.Alloc][]llssa.Expr // varargs
paramDIVars map[*types.Var]llssa.DIVar
patches Patches
blkInfos []blocks.Info
@@ -263,6 +264,8 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
if f.Recover != nil { // set recover block
fn.SetRecover(fn.Block(f.Recover.Index))
}
dbgEnabled := enableDbg && (f == nil || f.Origin() == nil)
dbgSymsEnabled := enableDbgSyms && (f == nil || f.Origin() == nil)
p.inits = append(p.inits, func() {
p.fn = fn
p.state = state // restore pkgState when compiling funcBody
@@ -270,6 +273,11 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
p.fn = nil
}()
p.phis = nil
if dbgSymsEnabled {
p.paramDIVars = make(map[*types.Var]llssa.DIVar)
} else {
p.paramDIVars = nil
}
if debugGoSSA {
f.WriteTo(os.Stderr)
}
@@ -277,7 +285,7 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
log.Println("==> FuncBody", name)
}
b := fn.NewBuilder()
if enableDbg {
if dbgEnabled {
pos := p.goProg.Fset.Position(f.Pos())
bodyPos := p.getFuncBodyPos(f)
b.DebugFunction(fn, pos, bodyPos)
@@ -335,6 +343,9 @@ func isGlobal(v *types.Var) bool {
}
func (p *context) debugRef(b llssa.Builder, v *ssa.DebugRef) {
if !enableDbgSyms || v.Parent().Origin() != nil {
return
}
object := v.Object()
variable, ok := object.(*types.Var)
if !ok {
@@ -364,6 +375,9 @@ func (p *context) debugRef(b llssa.Builder, v *ssa.DebugRef) {
}
func (p *context) debugParams(b llssa.Builder, f *ssa.Function) {
if !enableDbgSyms || f.Origin() != nil {
return
}
for i, param := range f.Params {
variable := param.Object().(*types.Var)
pos := p.goProg.Fset.Position(param.Pos())
@@ -371,6 +385,9 @@ func (p *context) debugParams(b llssa.Builder, f *ssa.Function) {
ty := param.Type()
argNo := i + 1
div := b.DIVarParam(p.fn, pos, param.Name(), p.type_(ty, llssa.InGo), argNo)
if p.paramDIVars != nil {
p.paramDIVars[variable] = div
}
b.DIParam(variable, v, div, p.fn, pos, p.fn.Block(0))
}
}
@@ -388,7 +405,7 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do
b.Printf("call " + fn.Name() + "\n\x00")
}
// place here to avoid wrong current-block
if enableDbgSyms && block.Index == 0 {
if enableDbgSyms && block.Parent().Origin() == nil && block.Index == 0 {
p.debugParams(b, block.Parent())
}
if doModInit {
@@ -783,7 +800,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) {
p.compileInstrOrValue(b, iv, false)
return
}
if enableDbg {
if enableDbg && instr.Parent().Origin() == nil {
scope := p.getDebugLocScope(instr.Parent(), instr.Pos())
if scope != nil {
diScope := b.DIScope(p.fn, scope)
@@ -846,7 +863,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) {
x := p.compileValue(b, v.X)
b.Send(ch, x)
case *ssa.DebugRef:
if enableDbgSyms {
if enableDbgSyms && v.Parent().Origin() == nil {
p.debugRef(b, v)
}
default:
@@ -855,12 +872,21 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) {
}
func (p *context) getLocalVariable(b llssa.Builder, fn *ssa.Function, v *types.Var) llssa.DIVar {
if p.paramDIVars != nil {
if div, ok := p.paramDIVars[v]; ok {
return div
}
}
pos := p.fset.Position(v.Pos())
t := p.type_(v.Type(), llssa.InGo)
for i, param := range fn.Params {
if param.Object().(*types.Var) == v {
argNo := i + 1
return b.DIVarParam(p.fn, pos, v.Name(), t, argNo)
div := b.DIVarParam(p.fn, pos, v.Name(), t, argNo)
if p.paramDIVars != nil {
p.paramDIVars[v] = div
}
return div
}
}
scope := b.DIScope(p.fn, v.Parent())

32
doc/defer-tls-gc.md Normal file
View File

@@ -0,0 +1,32 @@
# Defer Loop GC Integration
## Background
`defer` chains are stored in a per-thread TLS slot so that unwind paths can locate the active `*runtime.Defer`. With the default allocator (`AllocU`) backed by Boehm GC (bdwgc), those TLS-resident pointers were invisible to the collector. In stress scenarios—e.g. `TestDeferLoopStress` with 1,000,000 defers—the collector reclaimed the defer nodes, leaving dangling pointers and causing crashes inside the deferred closures.
Prior experiments (`test-defer-dont-free` branch) confirmed the crash disappeared when allocations bypassed GC (plain `malloc` without `free`), pointing to a root-registration gap rather than logical corruption.
## Solution Overview
1. **GC-aware TLS slot helper** *(from PR [#1347](https://github.com/goplus/llgo/pull/1347))*
- Added `runtime/internal/clite/tls`, which exposes `tls.Alloc` to create per-thread storage that is automatically registered as a Boehm GC root.
- `SetThreadDefer` delegates to this helper so every thread reuses the same GC-safe slot without bespoke plumbing.
- The package handles TLS key creation, root registration/removal, and invokes an optional destructor when a thread exits.
2. **SSA codegen synchronization**
- `ssa/eh.go` now calls `runtime.SetThreadDefer` whenever it updates the TLS pointer (on first allocation and when restoring the previous link during unwind).
- Defer argument nodes and the `runtime.Defer` struct itself are allocated with `aggregateAllocU`, ensuring new memory comes from GC-managed heaps, and nodes are released via `runtime.FreeDeferNode`.
3. **Non-GC builds**
- The `tls` helper falls back to a malloc-backed TLS slot without GC registration, while `FreeDeferNode` continues to release nodes via `c.Free` when building with `-tags nogc`.
## Testing
Run the stress and regression suites to validate the integration:
```sh
./llgo.sh test ./test -run TestDeferLoopStress
./llgo.sh test ./test
```
The updated `TestDeferLoopStress` now asserts 1,000,000 loop defers execute without failure, catching regressions in GC root tracking.

View File

@@ -1,3 +1,5 @@
//go:build !llgo
package libc
import (

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package runtime
import "github.com/goplus/llgo/runtime/internal/clite/tls"
var deferTLS = tls.Alloc[*Defer](func(head **Defer) {
if head != nil {
*head = nil
}
})
// SetThreadDefer associates the current thread with the given defer chain.
func SetThreadDefer(head *Defer) {
deferTLS.Set(head)
}
// ClearThreadDefer resets the current thread's defer chain to nil.
func ClearThreadDefer() {
deferTLS.Clear()
}

View File

@@ -0,0 +1,33 @@
//go:build !nogc
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package runtime
import (
"unsafe"
c "github.com/goplus/llgo/runtime/internal/clite"
"github.com/goplus/llgo/runtime/internal/clite/bdwgc"
)
// FreeDeferNode releases a defer argument node allocated from the Boehm heap.
func FreeDeferNode(ptr unsafe.Pointer) {
if ptr != nil {
bdwgc.Free(c.Pointer(ptr))
}
}

View File

@@ -0,0 +1,32 @@
//go:build nogc
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package runtime
import (
"unsafe"
c "github.com/goplus/llgo/runtime/internal/clite"
)
// FreeDeferNode releases the defer node when GC integration is disabled.
func FreeDeferNode(ptr unsafe.Pointer) {
if ptr != nil {
c.Free(ptr)
}
}

View File

@@ -294,7 +294,7 @@ func (p Function) NewBuilder() Builder {
// TODO(xsw): Finalize may cause panic, so comment it.
// b.Finalize()
return &aBuilder{b, nil, p, p.Pkg, prog,
make(map[Expr]dbgExpr), make(map[*types.Scope]DIScope)}
make(map[*types.Scope]DIScope)}
}
// HasBody reports whether the function has a body.

View File

@@ -662,14 +662,8 @@ func (b diBuilder) createExpression(ops []uint64) DIExpression {
// Copy to alloca'd memory to get declareable address.
func (b Builder) constructDebugAddr(v Expr) (dbgPtr Expr, dbgVal Expr, exists bool) {
if v, ok := b.dbgVars[v]; ok {
return v.ptr, v.val, true
}
t := v.Type.RawType().Underlying()
dbgPtr, dbgVal = b.doConstructDebugAddr(v, t)
dbgExpr := dbgExpr{dbgPtr, dbgVal}
b.dbgVars[v] = dbgExpr
b.dbgVars[dbgVal] = dbgExpr
return dbgPtr, dbgVal, false
}
@@ -874,11 +868,7 @@ func (b Builder) DebugFunction(f Function, pos token.Position, bodyPos token.Pos
}
func (b Builder) Param(idx int) Expr {
p := b.Func.Param(idx)
if v, ok := b.dbgVars[p]; ok {
return v.val
}
return p
return b.Func.Param(idx)
}
// -----------------------------------------------------------------------------

View File

@@ -234,13 +234,17 @@ func (b Builder) getDefer(kind DoAction) *aDefer {
zero := prog.Val(uintptr(0))
link := Expr{b.pthreadGetspecific(key).impl, prog.DeferPtr()}
jb := b.AllocaSigjmpBuf()
ptr := b.aggregateAlloca(prog.Defer(), jb.impl, zero.impl, link.impl, procBlk.Addr().impl)
ptr := b.aggregateAllocU(prog.Defer(), jb.impl, zero.impl, link.impl, procBlk.Addr().impl)
deferData := Expr{ptr, prog.DeferPtr()}
b.pthreadSetspecific(key, deferData)
b.Call(b.Pkg.rtFunc("SetThreadDefer"), deferData)
bitsPtr := b.FieldAddr(deferData, deferBits)
rethPtr := b.FieldAddr(deferData, deferRethrow)
rundPtr := b.FieldAddr(deferData, deferRunDefers)
argsPtr := b.FieldAddr(deferData, deferArgs)
// Initialize the args list so later guards (e.g. DeferAlways/DeferInLoop)
// can safely detect an empty chain without a prior push.
b.Store(argsPtr, prog.Nil(prog.VoidPtr()))
czero := prog.IntVal(0, prog.CInt())
retval := b.Sigsetjmp(jb, czero)
@@ -300,8 +304,10 @@ func (b Builder) Defer(kind DoAction, fn Expr, args ...Expr) {
b.Store(self.bitsPtr, b.BinOp(token.OR, bits, nextbit))
case DeferAlways:
// nothing to do
case DeferInLoop:
// Loop defers rely on a dedicated drain loop inserted below.
default:
panic("todo: DeferInLoop is not supported - " + b.Func.Name())
panic("unknown defer kind")
}
typ := b.saveDeferArgs(self, fn, args)
self.stmts = append(self.stmts, func(bits Expr) {
@@ -313,7 +319,37 @@ func (b Builder) Defer(kind DoAction, fn Expr, args ...Expr) {
b.callDefer(self, typ, fn, args)
})
case DeferAlways:
zero := b.Prog.Nil(b.Prog.VoidPtr())
list := b.Load(self.argsPtr)
has := b.BinOp(token.NEQ, list, zero)
b.IfThen(has, func() {
b.callDefer(self, typ, fn, args)
})
case DeferInLoop:
prog := b.Prog
condBlk := b.Func.MakeBlock()
bodyBlk := b.Func.MakeBlock()
exitBlk := b.Func.MakeBlock()
// Control flow:
// condBlk: check argsPtr for non-nil to see if there's work to drain.
// bodyBlk: execute a single defer node, then jump back to condBlk.
// exitBlk: reached when the list is empty (argsPtr == nil).
// This mirrors runtime's linked-list unwinding semantics for loop defers.
// jump to condition check before executing
b.Jump(condBlk)
b.SetBlockEx(condBlk, AtEnd, true)
list := b.Load(self.argsPtr)
has := b.BinOp(token.NEQ, list, prog.Nil(prog.VoidPtr()))
b.If(has, bodyBlk, exitBlk)
b.SetBlockEx(bodyBlk, AtEnd, true)
b.callDefer(self, typ, fn, args)
b.Jump(condBlk)
b.SetBlockEx(exitBlk, AtEnd, true)
default:
panic("unknown defer kind")
}
})
}
@@ -354,7 +390,7 @@ func (b Builder) saveDeferArgs(self *aDefer, fn Expr, args []Expr) Type {
flds[i+offset] = arg.impl
}
typ := prog.Struct(typs...)
ptr := Expr{b.aggregateMalloc(typ, flds...), prog.VoidPtr()}
ptr := Expr{b.aggregateAllocU(typ, flds...), prog.VoidPtr()}
b.Store(self.argsPtr, ptr)
return typ
}
@@ -365,19 +401,28 @@ func (b Builder) callDefer(self *aDefer, typ Type, fn Expr, args []Expr) {
return
}
prog := b.Prog
ptr := b.Load(self.argsPtr)
data := b.Load(Expr{ptr.impl, prog.Pointer(typ)})
offset := 1
b.Store(self.argsPtr, Expr{b.getField(data, 0).impl, prog.VoidPtr()})
if fn.kind == vkClosure {
fn = b.getField(data, 1)
offset++
}
for i := 0; i < len(args); i++ {
args[i] = b.getField(data, i+offset)
}
b.Call(fn, args...)
b.free(ptr)
zero := prog.Nil(prog.VoidPtr())
list := b.Load(self.argsPtr)
has := b.BinOp(token.NEQ, list, zero)
// The guard is required because callDefer is reused by endDefer() after the
// list has been drained. Without this check we would dereference a nil
// pointer when no loop defers were recorded.
b.IfThen(has, func() {
ptr := b.Load(self.argsPtr)
data := b.Load(Expr{ptr.impl, prog.Pointer(typ)})
offset := 1
b.Store(self.argsPtr, Expr{b.getField(data, 0).impl, prog.VoidPtr()})
callFn := fn
if callFn.kind == vkClosure {
callFn = b.getField(data, 1)
offset++
}
for i := 0; i < len(args); i++ {
args[i] = b.getField(data, i+offset)
}
b.Call(callFn, args...)
b.Call(b.Pkg.rtFunc("FreeDeferNode"), ptr)
})
}
// RunDefers emits instructions to run deferred instructions.
@@ -432,6 +477,7 @@ func (p Function) endDefer(b Builder) {
}
link := b.getField(b.Load(self.data), deferLink)
b.pthreadSetspecific(self.key, link)
b.Call(b.Pkg.rtFunc("SetThreadDefer"), link)
b.IndirectJump(b.Load(rundPtr), nexts)
b.SetBlockEx(panicBlk, AtEnd, false) // panicBlk: exec runDefers and rethrow

53
ssa/eh_loop_test.go Normal file
View File

@@ -0,0 +1,53 @@
//go:build !llgo
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ssa_test
import (
"strings"
"testing"
"github.com/goplus/llgo/ssa"
"github.com/goplus/llgo/ssa/ssatest"
)
func TestDeferInLoopIR(t *testing.T) {
prog := ssatest.NewProgram(t, nil)
pkg := prog.NewPackage("foo", "foo")
callee := pkg.NewFunc("callee", ssa.NoArgsNoRet, ssa.InGo)
cb := callee.MakeBody(1)
cb.Return()
cb.EndBuild()
fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo)
b := fn.MakeBody(1)
fn.SetRecover(fn.MakeBlock())
// Ensure entry block has a terminator like real codegen
b.Return()
b.SetBlockEx(fn.Block(0), ssa.BeforeLast, true)
b.Defer(ssa.DeferInLoop, callee.Expr)
b.EndBuild()
ir := pkg.Module().String()
if !strings.Contains(ir, "icmp ne ptr") {
t.Fatalf("expected loop defer condition in IR, got:\n%s", ir)
}
}

View File

@@ -57,11 +57,6 @@ func (p BasicBlock) Addr() Expr {
// -----------------------------------------------------------------------------
type dbgExpr struct {
ptr Expr
val Expr
}
type aBuilder struct {
impl llvm.Builder
blk BasicBlock
@@ -69,7 +64,6 @@ type aBuilder struct {
Pkg Package
Prog Program
dbgVars map[Expr]dbgExpr // save copied address and values for debug info
diScopeCache map[*types.Scope]DIScope // avoid duplicated DILexicalBlock(s)
}

60
test/defer_test.go Normal file
View File

@@ -0,0 +1,60 @@
//go:build llgo
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). 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 test
import (
"reflect"
"testing"
)
// runLoopDefers exercises a defer statement inside a loop and relies on
// defers executing after the loop but before the function returns.
func runLoopDefers() (result []int) {
for i := 0; i < 3; i++ {
v := i
defer func() {
result = append(result, v)
}()
}
return
}
func runLoopDeferCount(n int) (count int) {
for i := 0; i < n; i++ {
defer func() {
count++
}()
}
return
}
func TestDeferInLoopOrder(t *testing.T) {
got := runLoopDefers()
want := []int{2, 1, 0}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected defer order: got %v, want %v", got, want)
}
}
func TestDeferLoopStress(t *testing.T) {
const n = 1_000_000
if got := runLoopDeferCount(n); got != n {
t.Fatalf("unexpected count: got %d, want %d", got, n)
}
}