Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions compiler/defer.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package compiler
// frames.

import (
"github.com/tinygo-org/tinygo/compiler/llvmutil"
"github.com/tinygo-org/tinygo/ir"
"golang.org/x/tools/go/ssa"
"tinygo.org/x/go-llvm"
Expand All @@ -34,6 +35,40 @@ func (c *Compiler) deferInitFunc(frame *Frame) {
c.builder.CreateStore(llvm.ConstPointerNull(deferType), frame.deferPtr)
}

// isInLoop checks if there is a path from a basic block to itself.
func isInLoop(start *ssa.BasicBlock) bool {
// Use a breadth-first search to scan backwards through the block graph.
queue := []*ssa.BasicBlock{start}
checked := map[*ssa.BasicBlock]struct{}{}

for len(queue) > 0 {
// pop a block off of the queue
block := queue[len(queue)-1]
queue = queue[:len(queue)-1]

// Search through predecessors.
// Searching backwards means that this is pretty fast when the block is close to the start of the function.
// Defers are often placed near the start of the function.
for _, pred := range block.Preds {
if pred == start {
// cycle found
return true
}

if _, ok := checked[pred]; ok {
// block already checked
continue
}

// add to queue and checked map
queue = append(queue, pred)
checked[pred] = struct{}{}
}
}

return false
}

// emitDefer emits a single defer instruction, to be run when this function
// returns.
func (c *Compiler) emitDefer(frame *Frame, instr *ssa.Defer) {
Expand Down Expand Up @@ -127,12 +162,22 @@ func (c *Compiler) emitDefer(frame *Frame, instr *ssa.Defer) {
deferFrame = c.builder.CreateInsertValue(deferFrame, value, i, "")
}

// Put this struct in an alloca.
alloca := c.builder.CreateAlloca(deferFrameType, "defer.alloca")
c.builder.CreateStore(deferFrame, alloca)
// Put this struct in an allocation.
var alloca llvm.Value
if !isInLoop(instr.Block()) {
// This can safely use a stack allocation.
alloca = llvmutil.CreateEntryBlockAlloca(c.builder, deferFrameType, "defer.alloca")
} else {
// This may be hit a variable number of times, so use a heap allocation.
size := c.targetData.TypeAllocSize(deferFrameType)
sizeValue := llvm.ConstInt(c.uintptrType, size, false)
allocCall := c.createRuntimeCall("alloc", []llvm.Value{sizeValue}, "defer.alloc.call")
alloca = c.builder.CreateBitCast(allocCall, llvm.PointerType(deferFrameType, 0), "defer.alloc")
}
if c.NeedsStackObjects() {
c.trackPointer(alloca)
}
c.builder.CreateStore(deferFrame, alloca)

// Push it on top of the linked list by replacing deferPtr.
allocaCast := c.builder.CreateBitCast(alloca, next.Type(), "defer.alloca.cast")
Expand Down
9 changes: 9 additions & 0 deletions testdata/calls.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ func main() {
// deferred functions
testDefer()

// defers in loop
testDeferLoop()

// Take a bound method and use it as a function pointer.
// This function pointer needs a context pointer.
testBound(thing.String)
Expand Down Expand Up @@ -85,6 +88,12 @@ func testDefer() {
println("deferring...")
}

func testDeferLoop() {
for j := 0; j < 4; j++ {
defer deferred("loop", j)
}
}

func deferred(msg string, i int) {
println(msg, i)
}
Expand Down
4 changes: 4 additions & 0 deletions testdata/calls.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Thing.Print: foo arg: bar
...run as defer 3
...run closure deferred: 4
...run as defer 1
loop 3
loop 2
loop 1
loop 0
bound method: foo
thing inside closure: foo
inside fp closure: foo 3
Expand Down