From 16709411a058be10c6e1d2016c72211931b94657 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 15 Oct 2025 13:36:52 +0800 Subject: [PATCH] defer: enable loop lowering --- README.md | 2 +- _demo/go/export/libexport.h.want | 3 + cl/_testgo/defercomplex/in.go | 51 ++++++++++++ cl/_testgo/defercomplex/out.ll | 1 + cl/_testgo/deferloop/in.go | 7 ++ cl/_testgo/deferloop/out.ll | 1 + cl/compile.go | 60 ++++++++++---- doc/defer-tls-gc.md | 32 ++++++++ .../crosscompile/compile/libc/libc_test.go | 2 + runtime/internal/runtime/defer_tls.go | 35 +++++++++ runtime/internal/runtime/z_defer_gc.go | 33 ++++++++ runtime/internal/runtime/z_defer_nogc.go | 32 ++++++++ ssa/decl.go | 2 +- ssa/di.go | 12 +-- ssa/eh.go | 78 +++++++++++++++---- ssa/eh_loop_test.go | 53 +++++++++++++ ssa/stmt_builder.go | 6 -- test/defer_test.go | 60 ++++++++++++++ 18 files changed, 418 insertions(+), 52 deletions(-) create mode 100644 cl/_testgo/defercomplex/in.go create mode 100644 cl/_testgo/defercomplex/out.ll create mode 100644 cl/_testgo/deferloop/in.go create mode 100644 cl/_testgo/deferloop/out.ll create mode 100644 doc/defer-tls-gc.md create mode 100644 runtime/internal/runtime/defer_tls.go create mode 100644 runtime/internal/runtime/z_defer_gc.go create mode 100644 runtime/internal/runtime/z_defer_nogc.go create mode 100644 ssa/eh_loop_test.go create mode 100644 test/defer_test.go diff --git a/README.md b/README.md index 570de5d5..b716dd75 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/_demo/go/export/libexport.h.want b/_demo/go/export/libexport.h.want index d652c758..75fe8570 100644 --- a/_demo/go/export/libexport.h.want +++ b/_demo/go/export/libexport.h.want @@ -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") diff --git a/cl/_testgo/defercomplex/in.go b/cl/_testgo/defercomplex/in.go new file mode 100644 index 00000000..63413926 --- /dev/null +++ b/cl/_testgo/defercomplex/in.go @@ -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)) +} diff --git a/cl/_testgo/defercomplex/out.ll b/cl/_testgo/defercomplex/out.ll new file mode 100644 index 00000000..092bc2b0 --- /dev/null +++ b/cl/_testgo/defercomplex/out.ll @@ -0,0 +1 @@ +; diff --git a/cl/_testgo/deferloop/in.go b/cl/_testgo/deferloop/in.go new file mode 100644 index 00000000..b26aa970 --- /dev/null +++ b/cl/_testgo/deferloop/in.go @@ -0,0 +1,7 @@ +package main + +func main() { + for i := 0; i < 3; i++ { + defer println("loop", i) + } +} diff --git a/cl/_testgo/deferloop/out.ll b/cl/_testgo/deferloop/out.ll new file mode 100644 index 00000000..092bc2b0 --- /dev/null +++ b/cl/_testgo/deferloop/out.ll @@ -0,0 +1 @@ +; diff --git a/cl/compile.go b/cl/compile.go index 9521ff14..9d871b30 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -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()) diff --git a/doc/defer-tls-gc.md b/doc/defer-tls-gc.md new file mode 100644 index 00000000..459451da --- /dev/null +++ b/doc/defer-tls-gc.md @@ -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. diff --git a/internal/crosscompile/compile/libc/libc_test.go b/internal/crosscompile/compile/libc/libc_test.go index b866f1dd..e46e2ccc 100644 --- a/internal/crosscompile/compile/libc/libc_test.go +++ b/internal/crosscompile/compile/libc/libc_test.go @@ -1,3 +1,5 @@ +//go:build !llgo + package libc import ( diff --git a/runtime/internal/runtime/defer_tls.go b/runtime/internal/runtime/defer_tls.go new file mode 100644 index 00000000..82fee35c --- /dev/null +++ b/runtime/internal/runtime/defer_tls.go @@ -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() +} diff --git a/runtime/internal/runtime/z_defer_gc.go b/runtime/internal/runtime/z_defer_gc.go new file mode 100644 index 00000000..fdfd008b --- /dev/null +++ b/runtime/internal/runtime/z_defer_gc.go @@ -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)) + } +} diff --git a/runtime/internal/runtime/z_defer_nogc.go b/runtime/internal/runtime/z_defer_nogc.go new file mode 100644 index 00000000..336462c8 --- /dev/null +++ b/runtime/internal/runtime/z_defer_nogc.go @@ -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) + } +} diff --git a/ssa/decl.go b/ssa/decl.go index b877addb..bb298715 100644 --- a/ssa/decl.go +++ b/ssa/decl.go @@ -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. diff --git a/ssa/di.go b/ssa/di.go index 5778cd4f..5b9acc3c 100644 --- a/ssa/di.go +++ b/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) } // ----------------------------------------------------------------------------- diff --git a/ssa/eh.go b/ssa/eh.go index bc866539..dea4a42c 100644 --- a/ssa/eh.go +++ b/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 diff --git a/ssa/eh_loop_test.go b/ssa/eh_loop_test.go new file mode 100644 index 00000000..8125ce67 --- /dev/null +++ b/ssa/eh_loop_test.go @@ -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) + } +} diff --git a/ssa/stmt_builder.go b/ssa/stmt_builder.go index 10128ce6..bd7d93e2 100644 --- a/ssa/stmt_builder.go +++ b/ssa/stmt_builder.go @@ -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) } diff --git a/test/defer_test.go b/test/defer_test.go new file mode 100644 index 00000000..1c606c2c --- /dev/null +++ b/test/defer_test.go @@ -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) + } +}