defer: enable loop lowering
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
51
cl/_testgo/defercomplex/in.go
Normal file
51
cl/_testgo/defercomplex/in.go
Normal 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))
|
||||
}
|
||||
1
cl/_testgo/defercomplex/out.ll
Normal file
1
cl/_testgo/defercomplex/out.ll
Normal file
@@ -0,0 +1 @@
|
||||
;
|
||||
7
cl/_testgo/deferloop/in.go
Normal file
7
cl/_testgo/deferloop/in.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
for i := 0; i < 3; i++ {
|
||||
defer println("loop", i)
|
||||
}
|
||||
}
|
||||
1
cl/_testgo/deferloop/out.ll
Normal file
1
cl/_testgo/deferloop/out.ll
Normal file
@@ -0,0 +1 @@
|
||||
;
|
||||
@@ -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
32
doc/defer-tls-gc.md
Normal 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.
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !llgo
|
||||
|
||||
package libc
|
||||
|
||||
import (
|
||||
|
||||
35
runtime/internal/runtime/defer_tls.go
Normal file
35
runtime/internal/runtime/defer_tls.go
Normal 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()
|
||||
}
|
||||
33
runtime/internal/runtime/z_defer_gc.go
Normal file
33
runtime/internal/runtime/z_defer_gc.go
Normal 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))
|
||||
}
|
||||
}
|
||||
32
runtime/internal/runtime/z_defer_nogc.go
Normal file
32
runtime/internal/runtime/z_defer_nogc.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
12
ssa/di.go
12
ssa/di.go
@@ -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)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
78
ssa/eh.go
78
ssa/eh.go
@@ -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
53
ssa/eh_loop_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
60
test/defer_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user