diff --git a/GNUmakefile b/GNUmakefile index b72a03ba39..0b905677bb 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -385,22 +385,22 @@ TEST_PACKAGES_FAST = \ # archive/zip requires os.ReadAt, which is not yet supported on windows # bytes requires mmap # compress/flate appears to hang on wasi -# crypto/aes fails on wasi, needs panic()/recover() +# crypto/aes needs reflect.Type.Method(), not yet implemented # crypto/des fails on wasi, needs panic()/recover() # crypto/hmac fails on wasi, it exits with a "slice out of range" panic # debug/plan9obj requires os.ReadAt, which is not yet supported on windows # encoding/xml takes a minute on linux and gives a stack overflow on wasi -# image requires recover(), which is not yet supported on wasi +# image fails on wasi, needs panic()/recover() # io/ioutil requires os.ReadDir, which is not yet supported on windows or wasi -# mime: fail on wasi; neds panic()/recover() +# mime: fails on wasi, needs panic()/recover() # mime/multipart: needs wasip1 syscall.FDFLAG_NONBLOCK # mime/quotedprintable requires syscall.Faccessat # net/mail: needs wasip1 syscall.FDFLAG_NONBLOCK # net/ntextproto: needs wasip1 syscall.FDFLAG_NONBLOCK -# regexp/syntax: fails on wasip1; needs panic()/recover() -# strconv requires recover() which is not yet supported on wasi -# text/tabwriter requires recover(), which is not yet supported on wasi -# text/template/parse requires recover(), which is not yet supported on wasi +# regexp/syntax: fails on wasip1, needs panic()/recover() +# strconv: fails on wasi, needs panic()/recover() +# text/tabwriter: fails on wasi, needs panic()/recover() +# text/template/parse: fails on wasi, needs panic()/recover() # testing/fstest requires os.ReadDir, which is not yet supported on windows or wasi # Additional standard library packages that pass tests on individual platforms @@ -425,6 +425,7 @@ TEST_PACKAGES_LINUX := \ os/user \ regexp/syntax \ strconv \ + testing/fstest \ text/tabwriter \ text/template/parse @@ -435,7 +436,11 @@ TEST_PACKAGES_WINDOWS := \ compress/flate \ crypto/des \ crypto/hmac \ + image \ + mime \ + regexp/syntax \ strconv \ + text/tabwriter \ text/template/parse \ $(nil) diff --git a/builder/sizes_test.go b/builder/sizes_test.go index fd985a8977..4d80a1b798 100644 --- a/builder/sizes_test.go +++ b/builder/sizes_test.go @@ -42,9 +42,9 @@ func TestBinarySize(t *testing.T) { // This is a small number of very diverse targets that we want to test. tests := []sizeTest{ // microcontrollers - {"hifive1b", "examples/echo", 3680, 280, 0, 2252}, - {"microbit", "examples/serial", 2694, 342, 8, 2248}, - {"wioterminal", "examples/pininterrupt", 7074, 1510, 120, 7248}, + {"hifive1b", "examples/echo", 3817, 299, 0, 2252}, + {"microbit", "examples/serial", 2820, 356, 8, 2248}, + {"wioterminal", "examples/pininterrupt", 7206, 1510, 120, 7248}, // TODO: also check wasm. Right now this is difficult, because // wasm binaries are run through wasm-opt and therefore the @@ -99,7 +99,7 @@ func TestSizeFull(t *testing.T) { t.Fatal("could not read program size:", err) } for _, pkg := range sizes.sortedPackageNames() { - if pkg == "(padding)" || pkg == "(unknown)" { + if pkg == "(padding)" || pkg == "(unknown)" || pkg == "Go types" { // TODO: correctly attribute all unknown binary size. continue } diff --git a/compileopts/target.go b/compileopts/target.go index 0900a7ba2b..70c7047462 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -485,6 +485,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) { "--no-insert-timestamp", "--no-dynamicbase", ) + spec.ExtraFiles = append(spec.ExtraFiles, + "src/runtime/runtime_windows.c") case "wasm", "wasip1", "wasip2": return nil, fmt.Errorf("GOOS=%s but GOARCH is unset. Please set GOARCH to wasm", options.GOOS) default: diff --git a/compiler/asserts.go b/compiler/asserts.go index c890a3317e..7e3a8b1504 100644 --- a/compiler/asserts.go +++ b/compiler/asserts.go @@ -241,24 +241,37 @@ func (b *builder) createRuntimeAssert(assert llvm.Value, blockPrefix, assertFunc } } - // Put the fault block at the end of the function and the next block at the - // current insert position. - faultBlock := b.ctx.AddBasicBlock(b.llvmFn, blockPrefix+".throw") + faultBlock := b.getRuntimeAssertBlock(blockPrefix, assertFunc) nextBlock := b.insertBasicBlock(blockPrefix + ".next") b.currentBlockInfo.exit = nextBlock // adjust outgoing block for phi nodes // Now branch to the out-of-bounds or the regular block. b.CreateCondBr(assert, faultBlock, nextBlock) - // Fail: the assert triggered so panic. - b.SetInsertPointAtEnd(faultBlock) - b.createRuntimeCall(assertFunc, nil, "") - b.CreateUnreachable() - // Ok: assert didn't trigger so continue normally. b.SetInsertPointAtEnd(nextBlock) } +func (b *builder) getRuntimeAssertBlock(blockPrefix, assertFunc string) llvm.BasicBlock { + if b.runtimeAssertBlocks == nil { + b.runtimeAssertBlocks = make(map[string]llvm.BasicBlock) + } + if block := b.runtimeAssertBlocks[assertFunc]; !block.IsNil() { + return block + } + savedBlock := b.GetInsertBlock() + block := b.ctx.AddBasicBlock(b.llvmFn, blockPrefix+".throw") + b.runtimeAssertBlocks[assertFunc] = block + b.SetInsertPointAtEnd(block) + if b.hasDeferFrame() { + b.createFaultCheckpoint() + } + b.createRuntimeCall(assertFunc, nil, "") + b.CreateUnreachable() + b.SetInsertPointAtEnd(savedBlock) + return block +} + // extendInteger extends the value to at least targetType using a zero or sign // extend. The resulting value is not truncated: it may still be bigger than // targetType. diff --git a/compiler/channel.go b/compiler/channel.go index 0ff2ab7f32..82139de8ba 100644 --- a/compiler/channel.go +++ b/compiler/channel.go @@ -48,7 +48,7 @@ func (b *builder) createChanSend(instr *ssa.Send) { channelOpAlloca, channelOpAllocaSize := b.createTemporaryAlloca(channelOp, "chan.op") // Do the send. - b.createRuntimeCall("chanSend", []llvm.Value{ch, valueAlloca, channelOpAlloca}, "") + b.createRuntimeInvoke("chanSend", []llvm.Value{ch, valueAlloca, channelOpAlloca}, "") // End the lifetime of the allocas. // This also works around a bug in CoroSplit, at least in LLVM 8: @@ -101,7 +101,7 @@ func (b *builder) createChanRecv(unop *ssa.UnOp) llvm.Value { // createChanClose closes the given channel. func (b *builder) createChanClose(ch llvm.Value) { - b.createRuntimeCall("chanClose", []llvm.Value{ch}, "") + b.createRuntimeInvoke("chanClose", []llvm.Value{ch}, "") } // createSelect emits all IR necessary for a select statements. That's a diff --git a/compiler/compiler.go b/compiler/compiler.go index eaf7b58b1c..c08687e8e7 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -176,6 +176,9 @@ type builder struct { deferBuiltinFuncs map[ssa.Value]deferBuiltin runDefersBlock []llvm.BasicBlock afterDefersBlock []llvm.BasicBlock + + runtimeAssertBlocks map[string]llvm.BasicBlock + interfaceAssertBlock llvm.BasicBlock } func newBuilder(c *compilerContext, irbuilder llvm.Builder, f *ssa.Function) *builder { @@ -1895,6 +1898,12 @@ func (b *builder) createBuiltin(argTypes []types.Type, argValues []llvm.Value, c // not of the current function. useParentFrame = 1 } + // Prevent inlining of functions that call recover(), matching the + // Go compiler's behavior. If this function were inlined into a + // deferred function, recover() would incorrectly succeed because + // the inlined code runs in the deferred function's context. + noinline := b.ctx.CreateEnumAttribute(llvm.AttributeKindID("noinline"), 0) + b.llvmFn.AddFunctionAttr(noinline) return b.createRuntimeCall("_recover", []llvm.Value{llvm.ConstInt(b.ctx.Int1Type(), useParentFrame, false)}, ""), nil case "ssa:wrapnilchk": // TODO: do an actual nil check? diff --git a/compiler/defer.go b/compiler/defer.go index ec2bbe00e1..b0bc3a9674 100644 --- a/compiler/defer.go +++ b/compiler/defer.go @@ -60,10 +60,6 @@ func (b *builder) deferInitFunc() { b.deferExprFuncs = make(map[ssa.Value]int) b.deferBuiltinFuncs = make(map[ssa.Value]deferBuiltin) - // Create defer list pointer. - b.deferPtr = b.CreateAlloca(b.dataPtrType, "deferPtr") - b.CreateStore(llvm.ConstPointerNull(b.dataPtrType), b.deferPtr) - if b.hasDeferFrame() { // Set up the defer frame with the current stack pointer. // This assumes that the stack pointer doesn't move outside of the @@ -73,12 +69,22 @@ func (b *builder) deferInitFunc() { // in the setjmp-like inline assembly. deferFrameType := b.getLLVMRuntimeType("deferFrame") b.deferFrame = b.CreateAlloca(deferFrameType, "deferframe.buf") + // The field index must match the DeferPtr field in runtime.deferFrame, + // defined in src/runtime/panic.go. + b.deferPtr = b.CreateInBoundsGEP(deferFrameType, b.deferFrame, []llvm.Value{ + llvm.ConstInt(b.ctx.Int32Type(), 0, false), + llvm.ConstInt(b.ctx.Int32Type(), 6, false), // DeferPtr field + }, "deferPtr") stackPointer := b.readStackPointer() b.createRuntimeCall("setupDeferFrame", []llvm.Value{b.deferFrame, stackPointer}, "") // Create the landing pad block, which is where control transfers after // a panic. b.landingpad = b.ctx.AddBasicBlock(b.llvmFn, "lpad") + } else { + // Create defer list pointer. + b.deferPtr = b.CreateAlloca(b.dataPtrType, "deferPtr") + b.CreateStore(llvm.ConstPointerNull(b.dataPtrType), b.deferPtr) } } @@ -237,6 +243,17 @@ func (b *builder) createInvokeCheckpoint() { b.currentBlockInfo.exit = continueBB } +// createFaultCheckpoint is like createInvokeCheckpoint but for use in fault +// blocks (e.g., bounds check failures). Unlike createInvokeCheckpoint, it does +// not update currentBlockInfo.exit because the fault block is a dead-end that +// does not participate in phi node resolution. +func (b *builder) createFaultCheckpoint() { + isZero := b.createCheckpoint(b.deferFrame) + continueBB := b.insertBasicBlock("") + b.CreateCondBr(isZero, continueBB, b.landingpad) + b.SetInsertPointAtEnd(continueBB) +} + // isInLoop checks if there is a path from the current block to itself. // Use Tarjan's strongly connected components algorithm to search for cycles. // A one-node SCC is a cycle iff there is an edge from the node to itself. diff --git a/compiler/interface.go b/compiler/interface.go index 5f7e7e345b..1ee478bf51 100644 --- a/compiler/interface.go +++ b/compiler/interface.go @@ -796,41 +796,66 @@ func (b *builder) createTypeAssert(expr *ssa.TypeAssert) llvm.Value { prevBlock := b.GetInsertBlock() okBlock := b.insertBasicBlock("typeassert.ok") - nextBlock := b.insertBasicBlock("typeassert.next") - b.currentBlockInfo.exit = nextBlock // adjust outgoing block for phi nodes - b.CreateCondBr(commaOk, okBlock, nextBlock) - - // Retrieve the value from the interface if the type assert was - // successful. - b.SetInsertPointAtEnd(okBlock) - var valueOk llvm.Value - if _, ok := expr.AssertedType.Underlying().(*types.Interface); ok { - // Type assert on interface type. Easy: just return the same - // interface value. - valueOk = itf - } else { - // Type assert on concrete type. Extract the underlying type from - // the interface (but only after checking it matches). - valueOk = b.extractValueFromInterface(itf, assertedType) - } - b.CreateBr(nextBlock) - - // Continue after the if statement. - b.SetInsertPointAtEnd(nextBlock) - phi := b.CreatePHI(assertedType, "typeassert.value") - phi.AddIncoming([]llvm.Value{llvm.ConstNull(assertedType), valueOk}, []llvm.BasicBlock{prevBlock, okBlock}) if expr.CommaOk { + nextBlock := b.insertBasicBlock("typeassert.next") + b.currentBlockInfo.exit = nextBlock + b.CreateCondBr(commaOk, okBlock, nextBlock) + + // Retrieve the value from the interface if the type assert was + // successful. + b.SetInsertPointAtEnd(okBlock) + var valueOk llvm.Value + if _, ok := expr.AssertedType.Underlying().(*types.Interface); ok { + // Type assert on interface type. Easy: just return the same + // interface value. + valueOk = itf + } else { + // Type assert on concrete type. Extract the underlying type from + // the interface (but only after checking it matches). + valueOk = b.extractValueFromInterface(itf, assertedType) + } + b.CreateBr(nextBlock) + + // Continue after the if statement. + b.SetInsertPointAtEnd(nextBlock) + phi := b.CreatePHI(assertedType, "typeassert.value") + phi.AddIncoming([]llvm.Value{llvm.ConstNull(assertedType), valueOk}, []llvm.BasicBlock{prevBlock, okBlock}) + tuple := b.ctx.ConstStruct([]llvm.Value{llvm.Undef(assertedType), llvm.Undef(b.ctx.Int1Type())}, false) // create empty tuple tuple = b.CreateInsertValue(tuple, phi, 0, "") // insert value tuple = b.CreateInsertValue(tuple, commaOk, 1, "") // insert 'comma ok' boolean return tuple } else { - // This is kind of dirty as the branch above becomes mostly useless, - // but hopefully this gets optimized away. - b.createRuntimeCall("interfaceTypeAssert", []llvm.Value{commaOk}, "") - return phi + // Type assert without comma-ok. If it fails, panic. + faultBlock := b.getInterfaceAssertBlock() + b.currentBlockInfo.exit = okBlock + b.CreateCondBr(commaOk, okBlock, faultBlock) + + // OK: extract the value from the interface. + b.SetInsertPointAtEnd(okBlock) + if _, ok := expr.AssertedType.Underlying().(*types.Interface); ok { + return itf + } + return b.extractValueFromInterface(itf, assertedType) + } +} + +func (b *builder) getInterfaceAssertBlock() llvm.BasicBlock { + if !b.interfaceAssertBlock.IsNil() { + return b.interfaceAssertBlock + } + savedBlock := b.GetInsertBlock() + block := b.ctx.AddBasicBlock(b.llvmFn, "typeassert.throw") + b.interfaceAssertBlock = block + b.SetInsertPointAtEnd(block) + if b.hasDeferFrame() { + b.createFaultCheckpoint() } + b.createRuntimeCall("interfaceTypeAssert", []llvm.Value{llvm.ConstInt(b.ctx.Int1Type(), 0, false)}, "") + b.CreateUnreachable() + b.SetInsertPointAtEnd(savedBlock) + return block } // getMethodsString returns a string to be used in the "tinygo-methods" string diff --git a/compiler/map.go b/compiler/map.go index 59d4bbf818..fda2cec2c4 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -133,7 +133,7 @@ func (b *builder) createMapUpdate(keyType types.Type, m, key, value llvm.Value, if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 { // key is a string params := []llvm.Value{m, key, valueAlloca} - b.createRuntimeCall("hashmapStringSet", params, "") + b.createRuntimeInvoke("hashmapStringSet", params, "") } else { // Key stored at actual type. keyAlloca, keySize := b.createTemporaryAlloca(key.Type(), "hashmap.key") @@ -143,7 +143,7 @@ func (b *builder) createMapUpdate(keyType types.Type, m, key, value llvm.Value, fnName = "hashmapGenericSet" } params := []llvm.Value{m, keyAlloca, valueAlloca} - b.createRuntimeCall(fnName, params, "") + b.createRuntimeInvoke(fnName, params, "") b.emitLifetimeEnd(keyAlloca, keySize) } b.emitLifetimeEnd(valueAlloca, valueSize) diff --git a/compiler/testdata/defer-cortex-m-qemu.ll b/compiler/testdata/defer-cortex-m-qemu.ll index ec40049dd6..852fcff7bd 100644 --- a/compiler/testdata/defer-cortex-m-qemu.ll +++ b/compiler/testdata/defer-cortex-m-qemu.ll @@ -3,7 +3,7 @@ source_filename = "defer.go" target datalayout = "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64" target triple = "thumbv7m-unknown-unknown-eabi" -%runtime.deferFrame = type { ptr, ptr, [0 x ptr], ptr, i8, %runtime._interface } +%runtime.deferFrame = type { ptr, ptr, [0 x ptr], ptr, i8, %runtime._interface, ptr } %runtime._interface = type { ptr, ptr } ; Function Attrs: nounwind @@ -18,14 +18,14 @@ declare void @main.external(ptr) #1 define hidden void @main.deferSimple(ptr %context) unnamed_addr #0 { entry: %defer.alloca = alloca { i32, ptr }, align 4 - %deferPtr = alloca ptr, align 4 - store ptr null, ptr %deferPtr, align 4 %deferframe.buf = alloca %runtime.deferFrame, align 4 + %deferPtr = getelementptr inbounds nuw i8, ptr %deferframe.buf, i32 24 %0 = call ptr @llvm.stacksave.p0() call void @runtime.setupDeferFrame(ptr nonnull %deferframe.buf, ptr %0, ptr undef) #4 + %defer.next = load ptr, ptr %deferPtr, align 4 store i32 0, ptr %defer.alloca, align 4 %defer.alloca.repack15 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 4 - store ptr null, ptr %defer.alloca.repack15, align 4 + store ptr %defer.next, ptr %defer.alloca.repack15, align 4 store ptr %defer.alloca, ptr %deferPtr, align 4 %setjmp = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 %setjmp.result = icmp eq i32 %setjmp, 0 @@ -111,9 +111,9 @@ rundefers.end3: ; preds = %rundefers.loophead6 ; Function Attrs: nocallback nofree nosync nounwind willreturn declare ptr @llvm.stacksave.p0() #2 -declare void @runtime.setupDeferFrame(ptr dereferenceable_or_null(24), ptr, ptr) #1 +declare void @runtime.setupDeferFrame(ptr dereferenceable_or_null(28), ptr, ptr) #1 -declare void @runtime.destroyDeferFrame(ptr dereferenceable_or_null(24), ptr) #1 +declare void @runtime.destroyDeferFrame(ptr dereferenceable_or_null(28), ptr) #1 ; Function Attrs: nounwind define internal void @"main.deferSimple$1"(ptr %context) unnamed_addr #0 { @@ -135,18 +135,18 @@ define hidden void @main.deferMultiple(ptr %context) unnamed_addr #0 { entry: %defer.alloca2 = alloca { i32, ptr }, align 4 %defer.alloca = alloca { i32, ptr }, align 4 - %deferPtr = alloca ptr, align 4 - store ptr null, ptr %deferPtr, align 4 %deferframe.buf = alloca %runtime.deferFrame, align 4 + %deferPtr = getelementptr inbounds nuw i8, ptr %deferframe.buf, i32 24 %0 = call ptr @llvm.stacksave.p0() call void @runtime.setupDeferFrame(ptr nonnull %deferframe.buf, ptr %0, ptr undef) #4 + %defer.next = load ptr, ptr %deferPtr, align 4 store i32 0, ptr %defer.alloca, align 4 %defer.alloca.repack22 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 4 - store ptr null, ptr %defer.alloca.repack22, align 4 + store ptr %defer.next, ptr %defer.alloca.repack22, align 4 store ptr %defer.alloca, ptr %deferPtr, align 4 store i32 1, ptr %defer.alloca2, align 4 - %defer.alloca2.repack23 = getelementptr inbounds nuw i8, ptr %defer.alloca2, i32 4 - store ptr %defer.alloca, ptr %defer.alloca2.repack23, align 4 + %defer.alloca2.repack24 = getelementptr inbounds nuw i8, ptr %defer.alloca2, i32 4 + store ptr %defer.alloca, ptr %defer.alloca2.repack24, align 4 store ptr %defer.alloca2, ptr %deferPtr, align 4 %setjmp = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 %setjmp.result = icmp eq i32 %setjmp, 0 @@ -270,9 +270,8 @@ entry: ; Function Attrs: nounwind define hidden void @main.deferInfiniteLoop(ptr %context) unnamed_addr #0 { entry: - %deferPtr = alloca ptr, align 4 - store ptr null, ptr %deferPtr, align 4 %deferframe.buf = alloca %runtime.deferFrame, align 4 + %deferPtr = getelementptr inbounds nuw i8, ptr %deferframe.buf, i32 24 %0 = call ptr @llvm.stacksave.p0() call void @runtime.setupDeferFrame(ptr nonnull %deferframe.buf, ptr %0, ptr undef) #4 br label %for.body @@ -318,9 +317,8 @@ declare noalias nonnull ptr @runtime.alloc(i32, ptr, ptr) #3 ; Function Attrs: nounwind define hidden void @main.deferLoop(ptr %context) unnamed_addr #0 { entry: - %deferPtr = alloca ptr, align 4 - store ptr null, ptr %deferPtr, align 4 %deferframe.buf = alloca %runtime.deferFrame, align 4 + %deferPtr = getelementptr inbounds nuw i8, ptr %deferframe.buf, i32 24 %0 = call ptr @llvm.stacksave.p0() call void @runtime.setupDeferFrame(ptr nonnull %deferframe.buf, ptr %0, ptr undef) #4 br label %for.loop @@ -408,9 +406,8 @@ rundefers.end1: ; preds = %rundefers.loophead4 define hidden void @main.deferBetweenLoops(ptr %context) unnamed_addr #0 { entry: %defer.alloca = alloca { i32, ptr, i32 }, align 4 - %deferPtr = alloca ptr, align 4 - store ptr null, ptr %deferPtr, align 4 %deferframe.buf = alloca %runtime.deferFrame, align 4 + %deferPtr = getelementptr inbounds nuw i8, ptr %deferframe.buf, i32 24 %0 = call ptr @llvm.stacksave.p0() call void @runtime.setupDeferFrame(ptr nonnull %deferframe.buf, ptr %0, ptr undef) #4 br label %for.loop diff --git a/compiler/testdata/generics.ll b/compiler/testdata/generics.ll index de682b76cb..2a3fcd0abf 100644 --- a/compiler/testdata/generics.ll +++ b/compiler/testdata/generics.ll @@ -29,13 +29,13 @@ entry: %a = call align 4 dereferenceable(8) ptr @runtime.alloc(i32 8, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #3 call void @runtime.trackPointer(ptr nonnull %a, ptr nonnull %stackalloc, ptr undef) #3 store float %a.X, ptr %a, align 4 - %a.repack9 = getelementptr inbounds nuw i8, ptr %a, i32 4 - store float %a.Y, ptr %a.repack9, align 4 + %a.repack5 = getelementptr inbounds nuw i8, ptr %a, i32 4 + store float %a.Y, ptr %a.repack5, align 4 %b = call align 4 dereferenceable(8) ptr @runtime.alloc(i32 8, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #3 call void @runtime.trackPointer(ptr nonnull %b, ptr nonnull %stackalloc, ptr undef) #3 store float %b.X, ptr %b, align 4 - %b.repack11 = getelementptr inbounds nuw i8, ptr %b, i32 4 - store float %b.Y, ptr %b.repack11, align 4 + %b.repack7 = getelementptr inbounds nuw i8, ptr %b, i32 4 + store float %b.Y, ptr %b.repack7, align 4 call void @main.checkSize(i32 4, ptr undef) #3 call void @main.checkSize(i32 8, ptr undef) #3 %complit = call align 4 dereferenceable(8) ptr @runtime.alloc(i32 8, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #3 @@ -43,29 +43,29 @@ entry: br i1 false, label %deref.throw, label %deref.next deref.next: ; preds = %entry - br i1 false, label %deref.throw1, label %deref.next2 + br i1 false, label %deref.throw, label %deref.next1 -deref.next2: ; preds = %deref.next +deref.next1: ; preds = %deref.next %0 = load float, ptr %a, align 4 %1 = load float, ptr %b, align 4 %2 = fadd float %0, %1 - br i1 false, label %deref.throw3, label %deref.next4 + br i1 false, label %deref.throw, label %deref.next2 -deref.next4: ; preds = %deref.next2 - br i1 false, label %deref.throw5, label %deref.next6 +deref.next2: ; preds = %deref.next1 + br i1 false, label %deref.throw, label %deref.next3 -deref.next6: ; preds = %deref.next4 +deref.next3: ; preds = %deref.next2 %3 = getelementptr inbounds nuw i8, ptr %b, i32 4 %4 = getelementptr inbounds nuw i8, ptr %a, i32 4 %5 = load float, ptr %4, align 4 %6 = load float, ptr %3, align 4 - br i1 false, label %store.throw, label %store.next + br i1 false, label %deref.throw, label %store.next -store.next: ; preds = %deref.next6 +store.next: ; preds = %deref.next3 store float %2, ptr %complit, align 4 - br i1 false, label %store.throw7, label %store.next8 + br i1 false, label %deref.throw, label %store.next4 -store.next8: ; preds = %store.next +store.next4: ; preds = %store.next %7 = getelementptr inbounds nuw i8, ptr %complit, i32 4 %8 = fadd float %5, %6 store float %8, ptr %7, align 4 @@ -74,22 +74,7 @@ store.next8: ; preds = %store.next %10 = insertvalue %"main.Point[float32]" %9, float %8, 1 ret %"main.Point[float32]" %10 -deref.throw: ; preds = %entry - unreachable - -deref.throw1: ; preds = %deref.next - unreachable - -deref.throw3: ; preds = %deref.next2 - unreachable - -deref.throw5: ; preds = %deref.next4 - unreachable - -store.throw: ; preds = %deref.next6 - unreachable - -store.throw7: ; preds = %store.next +deref.throw: ; preds = %store.next, %deref.next3, %deref.next2, %deref.next1, %deref.next, %entry unreachable } @@ -107,13 +92,13 @@ entry: %a = call align 4 dereferenceable(8) ptr @runtime.alloc(i32 8, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #3 call void @runtime.trackPointer(ptr nonnull %a, ptr nonnull %stackalloc, ptr undef) #3 store i32 %a.X, ptr %a, align 4 - %a.repack9 = getelementptr inbounds nuw i8, ptr %a, i32 4 - store i32 %a.Y, ptr %a.repack9, align 4 + %a.repack5 = getelementptr inbounds nuw i8, ptr %a, i32 4 + store i32 %a.Y, ptr %a.repack5, align 4 %b = call align 4 dereferenceable(8) ptr @runtime.alloc(i32 8, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #3 call void @runtime.trackPointer(ptr nonnull %b, ptr nonnull %stackalloc, ptr undef) #3 store i32 %b.X, ptr %b, align 4 - %b.repack11 = getelementptr inbounds nuw i8, ptr %b, i32 4 - store i32 %b.Y, ptr %b.repack11, align 4 + %b.repack7 = getelementptr inbounds nuw i8, ptr %b, i32 4 + store i32 %b.Y, ptr %b.repack7, align 4 call void @main.checkSize(i32 4, ptr undef) #3 call void @main.checkSize(i32 8, ptr undef) #3 %complit = call align 4 dereferenceable(8) ptr @runtime.alloc(i32 8, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #3 @@ -121,29 +106,29 @@ entry: br i1 false, label %deref.throw, label %deref.next deref.next: ; preds = %entry - br i1 false, label %deref.throw1, label %deref.next2 + br i1 false, label %deref.throw, label %deref.next1 -deref.next2: ; preds = %deref.next +deref.next1: ; preds = %deref.next %0 = load i32, ptr %a, align 4 %1 = load i32, ptr %b, align 4 %2 = add i32 %0, %1 - br i1 false, label %deref.throw3, label %deref.next4 + br i1 false, label %deref.throw, label %deref.next2 -deref.next4: ; preds = %deref.next2 - br i1 false, label %deref.throw5, label %deref.next6 +deref.next2: ; preds = %deref.next1 + br i1 false, label %deref.throw, label %deref.next3 -deref.next6: ; preds = %deref.next4 +deref.next3: ; preds = %deref.next2 %3 = getelementptr inbounds nuw i8, ptr %b, i32 4 %4 = getelementptr inbounds nuw i8, ptr %a, i32 4 %5 = load i32, ptr %4, align 4 %6 = load i32, ptr %3, align 4 - br i1 false, label %store.throw, label %store.next + br i1 false, label %deref.throw, label %store.next -store.next: ; preds = %deref.next6 +store.next: ; preds = %deref.next3 store i32 %2, ptr %complit, align 4 - br i1 false, label %store.throw7, label %store.next8 + br i1 false, label %deref.throw, label %store.next4 -store.next8: ; preds = %store.next +store.next4: ; preds = %store.next %7 = getelementptr inbounds nuw i8, ptr %complit, i32 4 %8 = add i32 %5, %6 store i32 %8, ptr %7, align 4 @@ -152,22 +137,7 @@ store.next8: ; preds = %store.next %10 = insertvalue %"main.Point[int]" %9, i32 %8, 1 ret %"main.Point[int]" %10 -deref.throw: ; preds = %entry - unreachable - -deref.throw1: ; preds = %deref.next - unreachable - -deref.throw3: ; preds = %deref.next2 - unreachable - -deref.throw5: ; preds = %deref.next4 - unreachable - -store.throw: ; preds = %deref.next6 - unreachable - -store.throw7: ; preds = %store.next +deref.throw: ; preds = %store.next, %deref.next3, %deref.next2, %deref.next1, %deref.next, %entry unreachable } diff --git a/src/runtime/arch_386.go b/src/runtime/arch_386.go index 90ec8e8baf..43130a8fed 100644 --- a/src/runtime/arch_386.go +++ b/src/runtime/arch_386.go @@ -12,6 +12,7 @@ const callInstSize = 5 // "call someFunction" is 5 bytes const ( linux_MAP_ANONYMOUS = 0x20 linux_SIGBUS = 7 + linux_SIGFPE = 8 linux_SIGILL = 4 linux_SIGSEGV = 11 ) diff --git a/src/runtime/arch_amd64.go b/src/runtime/arch_amd64.go index 436d6e3849..09985cf515 100644 --- a/src/runtime/arch_amd64.go +++ b/src/runtime/arch_amd64.go @@ -12,6 +12,7 @@ const callInstSize = 5 // "call someFunction" is 5 bytes const ( linux_MAP_ANONYMOUS = 0x20 linux_SIGBUS = 7 + linux_SIGFPE = 8 linux_SIGILL = 4 linux_SIGSEGV = 11 ) diff --git a/src/runtime/arch_arm.go b/src/runtime/arch_arm.go index ea6b540d2a..65e0502f72 100644 --- a/src/runtime/arch_arm.go +++ b/src/runtime/arch_arm.go @@ -14,6 +14,7 @@ const callInstSize = 4 // "bl someFunction" is 4 bytes const ( linux_MAP_ANONYMOUS = 0x20 linux_SIGBUS = 7 + linux_SIGFPE = 8 linux_SIGILL = 4 linux_SIGSEGV = 11 ) diff --git a/src/runtime/arch_arm64.go b/src/runtime/arch_arm64.go index 6d3c856cf6..3da65dc6df 100644 --- a/src/runtime/arch_arm64.go +++ b/src/runtime/arch_arm64.go @@ -12,6 +12,7 @@ const callInstSize = 4 // "bl someFunction" is 4 bytes const ( linux_MAP_ANONYMOUS = 0x20 linux_SIGBUS = 7 + linux_SIGFPE = 8 linux_SIGILL = 4 linux_SIGSEGV = 11 ) diff --git a/src/runtime/arch_mips.go b/src/runtime/arch_mips.go index 5a7d05c898..e83e8c1970 100644 --- a/src/runtime/arch_mips.go +++ b/src/runtime/arch_mips.go @@ -12,6 +12,7 @@ const callInstSize = 8 // "jal someFunc" is 4 bytes, plus a MIPS delay slot const ( linux_MAP_ANONYMOUS = 0x800 linux_SIGBUS = 10 + linux_SIGFPE = 8 linux_SIGILL = 4 linux_SIGSEGV = 11 ) diff --git a/src/runtime/arch_mipsle.go b/src/runtime/arch_mipsle.go index 498cf862b7..e391b36692 100644 --- a/src/runtime/arch_mipsle.go +++ b/src/runtime/arch_mipsle.go @@ -12,6 +12,7 @@ const callInstSize = 8 // "jal someFunc" is 4 bytes, plus a MIPS delay slot const ( linux_MAP_ANONYMOUS = 0x800 linux_SIGBUS = 10 + linux_SIGFPE = 8 linux_SIGILL = 4 linux_SIGSEGV = 11 ) diff --git a/src/runtime/error.go b/src/runtime/error.go index 3ae5ea3aae..cd5429d1d3 100644 --- a/src/runtime/error.go +++ b/src/runtime/error.go @@ -6,3 +6,9 @@ type Error interface { RuntimeError() } + +// plainError is a runtime.Error implementation for plain string messages. +type plainError string + +func (e plainError) Error() string { return string(e) } +func (e plainError) RuntimeError() {} diff --git a/src/runtime/os_darwin.go b/src/runtime/os_darwin.go index 7197c43973..b6e56863ba 100644 --- a/src/runtime/os_darwin.go +++ b/src/runtime/os_darwin.go @@ -24,6 +24,7 @@ const ( // https://opensource.apple.com/source/xnu/xnu-7195.141.2/bsd/sys/signal.h.auto.html const ( sig_SIGBUS = 10 + sig_SIGFPE = 8 sig_SIGILL = 4 sig_SIGSEGV = 11 ) diff --git a/src/runtime/os_linux.go b/src/runtime/os_linux.go index 0ae105c5fc..e76fc27f6e 100644 --- a/src/runtime/os_linux.go +++ b/src/runtime/os_linux.go @@ -27,6 +27,7 @@ const ( const ( sig_SIGBUS = linux_SIGBUS + sig_SIGFPE = linux_SIGFPE sig_SIGILL = linux_SIGILL sig_SIGSEGV = linux_SIGSEGV ) diff --git a/src/runtime/panic.go b/src/runtime/panic.go index 5eac60ecd9..f28cb75a47 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -31,8 +31,8 @@ func panicStrategy() uint8 // DeferFrame is a stack allocated object that stores information for the // current "defer frame", which is used in functions that use the `defer` // keyword. -// The compiler knows about the JumpPC struct offset, so it should not be moved -// without also updating compiler/defer.go. +// The compiler knows about the JumpPC struct offset and the DeferPtr field +// index, so they should not be moved without also updating compiler/defer.go. type deferFrame struct { JumpSP unsafe.Pointer // stack pointer to return to JumpPC unsafe.Pointer // pc to return to @@ -40,6 +40,7 @@ type deferFrame struct { Previous *deferFrame // previous recover buffer pointer Panicking panicState // not panicking, panicking, or in Goexit PanicValue interface{} // panic value, might be nil for panic(nil) for example + DeferPtr unsafe.Pointer // head of the stack-allocated defer list } type panicState uint8 @@ -92,6 +93,17 @@ func runtimePanicAt(addr unsafe.Pointer, msg string) { if panicStrategy() == tinygo.PanicStrategyTrap { trap() } + if supportsRecover() && !interrupt.In() { + frame := (*deferFrame)(task.Current().DeferFrame) + if frame != nil { + // Use the normal panic mechanism so that this runtime error + // can be recovered with recover(). + frame.PanicValue = plainError(msg) + frame.Panicking = panicTrue + tinygo_longjmp(frame) + // unreachable + } + } if hasReturnAddr { // Note: the string "panic: runtime error at " is also used in // runtime_cortexm_hardfault.go. It is kept the same so that the string @@ -125,6 +137,7 @@ func setupDeferFrame(frame *deferFrame, jumpSP unsafe.Pointer) { frame.Previous = (*deferFrame)(currentTask.DeferFrame) frame.JumpSP = jumpSP frame.Panicking = panicFalse + frame.DeferPtr = nil currentTask.DeferFrame = unsafe.Pointer(frame) } @@ -147,6 +160,16 @@ func destroyDeferFrame(frame *deferFrame) { // panicking goroutine. // useParentFrame is set when the caller of runtime._recover has a defer frame // itself. In that case, recover() shouldn't check that frame but one frame up. +// +// TODO: Go only allows recover() to succeed when called directly from a +// deferred function, not from a sub-call (e.g. defer func() { sub() }() where +// sub() calls recover()). The Go compiler enforces this by walking the stack +// to count frames between gorecover and gopanic. TinyGo currently does not +// have a stack unwinder, so this restriction is not enforced at runtime. +// Functions calling recover() are marked noinline to prevent the most common +// case (inlined sub-call), but non-inlined sub-calls can still incorrectly +// recover. Fixing this properly requires either frame pointer support or a +// lightweight stack unwinder. func _recover(useParentFrame bool) interface{} { if !supportsRecover() || interrupt.In() { // Either we're compiling without stack unwinding support, or we're @@ -155,9 +178,6 @@ func _recover(useParentFrame bool) interface{} { // function. return nil } - // TODO: somehow check that recover() is called directly by a deferred - // function in a panicking goroutine. Maybe this can be done by comparing - // the frame pointer? frame := (*deferFrame)(task.Current().DeferFrame) if useParentFrame { // Don't recover panic from the current frame (which can't be panicking diff --git a/src/runtime/runtime_unix.c b/src/runtime/runtime_unix.c index 79dd7ce915..f6ed030e49 100644 --- a/src/runtime/runtime_unix.c +++ b/src/runtime/runtime_unix.c @@ -12,8 +12,98 @@ void tinygo_handle_fatal_signal(int sig, uintptr_t addr); +// tinygo_sigpanic is defined in Go. It turns a signal into a Go panic +// that can be recovered with recover(). The signal number is passed via +// the tinygo_caught_signal global. +void tinygo_sigpanic(void); + +// Set by the signal handler before redirecting to tinygo_sigpanic. +int tinygo_caught_signal; + +// Whether sigpanic-based recovery is supported for the current +// architecture. Set to 0 on architectures where we can't reliably +// modify the ucontext to redirect execution. +static int can_sigpanic(void) { +#if defined(__x86_64__) || defined(__i386__) || defined(__aarch64__) || defined(__arm64__) || defined(__arm__) || defined(__mips__) + return 1; +#else + return 0; +#endif +} + +// Try to redirect execution from the signal handler to tinygo_sigpanic. +// Returns 1 on success, 0 if the architecture doesn't support it. +static int redirect_to_sigpanic(int sig, ucontext_t *uctx) { + if (!can_sigpanic()) { + return 0; + } + tinygo_caught_signal = sig; + +#if __APPLE__ + #if __arm64__ + // ARM64: set LR to the faulting PC (as return address), set PC to sigpanic + uctx->uc_mcontext->__ss.__lr = uctx->uc_mcontext->__ss.__pc; + uctx->uc_mcontext->__ss.__pc = (uint64_t)&tinygo_sigpanic; + #elif __x86_64__ + // x86_64: push the faulting PC onto the stack, set RIP to sigpanic + uintptr_t sp = uctx->uc_mcontext->__ss.__rsp; + sp -= sizeof(uintptr_t); + *(uintptr_t *)sp = uctx->uc_mcontext->__ss.__rip; + uctx->uc_mcontext->__ss.__rsp = sp; + uctx->uc_mcontext->__ss.__rip = (uint64_t)&tinygo_sigpanic; + #else + return 0; + #endif +#elif __linux__ + #if __x86_64__ + uintptr_t sp = uctx->uc_mcontext.gregs[REG_RSP]; + sp -= sizeof(uintptr_t); + *(uintptr_t *)sp = uctx->uc_mcontext.gregs[REG_RIP]; + uctx->uc_mcontext.gregs[REG_RSP] = sp; + uctx->uc_mcontext.gregs[REG_RIP] = (uintptr_t)&tinygo_sigpanic; + #elif __i386__ + uintptr_t sp = uctx->uc_mcontext.gregs[REG_ESP]; + sp -= sizeof(uintptr_t); + *(uintptr_t *)sp = uctx->uc_mcontext.gregs[REG_EIP]; + uctx->uc_mcontext.gregs[REG_ESP] = sp; + uctx->uc_mcontext.gregs[REG_EIP] = (uintptr_t)&tinygo_sigpanic; + #elif __aarch64__ + uctx->uc_mcontext.regs[30] = uctx->uc_mcontext.pc; // LR = faulting PC + uctx->uc_mcontext.pc = (uintptr_t)&tinygo_sigpanic; + #elif __arm__ + uctx->uc_mcontext.arm_lr = uctx->uc_mcontext.arm_pc; + uctx->uc_mcontext.arm_pc = (uintptr_t)&tinygo_sigpanic; + #elif defined(__mips__) + // MIPS: set RA (gregs[31]) to the faulting PC, set PC to sigpanic. + uctx->uc_mcontext.gregs[31] = uctx->uc_mcontext.pc; + uctx->uc_mcontext.pc = (uintptr_t)&tinygo_sigpanic; + #else + return 0; + #endif +#else + return 0; +#endif + + return 1; +} + static void signal_handler(int sig, siginfo_t *info, void *context) { ucontext_t* uctx = context; + + // Try to redirect to sigpanic for a recoverable panic. + if (redirect_to_sigpanic(sig, uctx)) { + // Re-register the signal handler since SA_RESETHAND cleared it. + // We need it active in case the sigpanic itself faults (e.g., + // stack overflow during panic). + struct sigaction act; + memset(&act, 0, sizeof(act)); + act.sa_flags = SA_SIGINFO | SA_RESETHAND; + act.sa_sigaction = &signal_handler; + sigaction(sig, &act, NULL); + return; // return from signal handler; execution resumes at sigpanic + } + + // Fallback: extract the faulting address and call the fatal handler. uintptr_t addr = 0; #if __APPLE__ #if __arm64__ @@ -51,6 +141,7 @@ void tinygo_register_fatal_signals(void) { // Register the signal handler for common issues. There are more signals, // which can be added if needed. sigaction(SIGBUS, &act, NULL); + sigaction(SIGFPE, &act, NULL); sigaction(SIGILL, &act, NULL); sigaction(SIGSEGV, &act, NULL); } diff --git a/src/runtime/runtime_unix.go b/src/runtime/runtime_unix.go index 99f28411f3..0e26549417 100644 --- a/src/runtime/runtime_unix.go +++ b/src/runtime/runtime_unix.go @@ -139,6 +139,26 @@ func runMain() { //export tinygo_register_fatal_signals func tinygo_register_fatal_signals() +//go:extern tinygo_caught_signal +var tinygo_caught_signal int32 + +// tinygo_sigpanic is called when a signal (SIGSEGV, SIGFPE, etc.) is caught +// and the signal handler has redirected execution here. It turns the signal +// into a Go panic that can be recovered with recover(). +// +//export tinygo_sigpanic +func tinygo_sigpanic() { + sig := tinygo_caught_signal + switch sig { + case sig_SIGSEGV, sig_SIGBUS: + runtimePanic("nil pointer dereference") + case sig_SIGFPE: + runtimePanic("divide by zero") + default: + runtimePanic("signal") + } +} + // Print fatal errors when they happen, including the instruction location. // With the particular formatting below, `tinygo run` can extract the location // where the signal happened and try to show the source location based on DWARF diff --git a/src/runtime/runtime_windows.c b/src/runtime/runtime_windows.c new file mode 100644 index 0000000000..4498d0f0b1 --- /dev/null +++ b/src/runtime/runtime_windows.c @@ -0,0 +1,29 @@ +//go:build none + +// This file is included on Windows (despite the //go:build line above). + +#include +#include + +void tinygo_sigpanic_windows(int32_t exception_code); + +static LONG WINAPI tinygo_exception_handler(EXCEPTION_POINTERS *info) { + DWORD code = info->ExceptionRecord->ExceptionCode; + switch (code) { + case EXCEPTION_ACCESS_VIOLATION: + case EXCEPTION_IN_PAGE_ERROR: + case EXCEPTION_INT_DIVIDE_BY_ZERO: + case EXCEPTION_INT_OVERFLOW: + tinygo_sigpanic_windows((int32_t)code); + // If runtimePanic triggers longjmp, we never reach here. + // If it doesn't (no defer frame), it will abort and we also + // never reach here. + return EXCEPTION_CONTINUE_SEARCH; + default: + return EXCEPTION_CONTINUE_SEARCH; + } +} + +void tinygo_init_exception_handler(void) { + AddVectoredExceptionHandler(1, tinygo_exception_handler); +} diff --git a/src/runtime/runtime_windows.go b/src/runtime/runtime_windows.go index 8419fb4f8c..8393565105 100644 --- a/src/runtime/runtime_windows.go +++ b/src/runtime/runtime_windows.go @@ -60,6 +60,10 @@ func mainCRTStartup() int { _QueryPerformanceFrequency(&performanceFrequency) } + // Register vectored exception handler so that access violations and + // divide-by-zero exceptions can be recovered with defer/recover. + tinygo_init_exception_handler() + // Obtain the initial stack pointer right before calling the run() function. // The run function has been moved to a separate (non-inlined) function so // that the correct stack pointer is read. @@ -294,3 +298,27 @@ func hardwareRand() (n uint64, ok bool) { // //export SystemFunction036 func _RtlGenRandom(buf unsafe.Pointer, len int) bool + +const ( + _EXCEPTION_ACCESS_VIOLATION = 0xC0000005 + _EXCEPTION_IN_PAGE_ERROR = 0xC0000006 + _EXCEPTION_INT_DIVIDE_BY_ZERO = 0xC0000094 + _EXCEPTION_INT_OVERFLOW = 0xC0000095 +) + +//export tinygo_init_exception_handler +func tinygo_init_exception_handler() + +//export tinygo_sigpanic_windows +func tinygo_sigpanic_windows(exceptionCode int32) { + switch uint32(exceptionCode) { + case _EXCEPTION_ACCESS_VIOLATION, _EXCEPTION_IN_PAGE_ERROR: + runtimePanic("nil pointer dereference") + case _EXCEPTION_INT_DIVIDE_BY_ZERO: + runtimePanic("divide by zero") + case _EXCEPTION_INT_OVERFLOW: + runtimePanic("integer overflow") + default: + runtimePanic("unknown exception") + } +} diff --git a/testdata/recover.go b/testdata/recover.go index 6fdf282e7b..619bfa01a8 100644 --- a/testdata/recover.go +++ b/testdata/recover.go @@ -30,8 +30,23 @@ func main() { println("\n# defer panic") deferPanic() + println("\n# indirect recover") + indirectRecover() + println("\n# runtime.Goexit") runtimeGoexit() + + println("\n# repanic") + recoverRepanic() + + println("\n# recover runtime errors") + recoverRuntimeError() + + println("\n# recover from nil map and closed channel") + recoverNilMapAndChan() + + println("\n# recover from hardware signals") + recoverSignals() } func recoverSimple() { @@ -114,6 +129,24 @@ func deferPanic() { println("defer panic") } +// TODO: Go only allows recover() to succeed when called directly from a +// deferred function. Update this test once runtime recover can distinguish it. +func indirectRecover() { + defer func() { + if r := indirectRecoverHelper(); r == nil { + println("indirect recover returned nil") + } else { + printitf("indirect recover returned:", r) + } + }() + panic("indirect panic") +} + +//go:noinline +func indirectRecoverHelper() interface{} { + return recover() +} + func runtimeGoexit() { wg.Add(1) go func() { @@ -127,6 +160,27 @@ func runtimeGoexit() { wg.Wait() } +// Test that a repanic inside a deferred function propagates correctly +// instead of re-running the same defer. This is a regression test for +// tinygo-org/tinygo issue 3449. +func recoverRepanic() { + // Two defers: inner recovers and repanics, outer should catch it. + defer func() { + r := recover() + if r != nil { + printitf("outer recovered:", r) + } + }() + defer func() { + r := recover() + if r != nil { + println("inner, repanicking") + panic(r) + } + }() + panic("repanic value") +} + func printitf(msg string, itf interface{}) { switch itf := itf.(type) { case string: @@ -135,3 +189,90 @@ func printitf(msg string, itf interface{}) { println(msg, itf) } } + +// Test recovering from runtime errors (bounds checks, type assertions, etc.) +func recoverRuntimeError() { + recoverMustPanic("index", func() { + s := make([]int, 5) + _ = s[99] + }) + recoverMustPanic("index from helper", func() { + s := []byte{1} + _ = readOutOfBounds(s) + }) + recoverMustPanic("slice", func() { + s := make([]int, 5) + _ = s[3:99] + }) + recoverMustPanic("type assert", func() { + var x interface{} = 1 + _ = x.(string) + }) + recoverEmptyInterfaceTypeAssert() +} + +//go:noinline +func readOutOfBounds(s []byte) byte { + return s[2] +} + +func recoverEmptyInterfaceTypeAssert() { + defer func() { + r := recover() + if r != nil { + println(" failed empty interface type assert") + } else { + println(" recovered: empty interface type assert") + } + }() + var intf interface{} = 3 + typed := intf.(interface{}) + useEmptyInterface(typed) +} + +func useEmptyInterface(typed interface{}) { + if typed.(int) != 3 { + println(" failed empty interface value") + } +} + +func recoverMustPanic(name string, f func()) { + defer func() { + r := recover() + if r != nil { + println(" recovered:", name) + } else { + println(" failed to recover:", name) + } + }() + f() +} + +// Test recovering from nil map assignment and closed channel send. +func recoverNilMapAndChan() { + recoverMustPanic("nil map", func() { + var m map[string]int + m["x"] = 1 + }) + recoverMustPanic("closed chan", func() { + ch := make(chan int) + close(ch) + ch <- 1 + }) + recoverMustPanic("close nil chan", func() { + var ch chan int + close(ch) + }) +} + +// Test recovering from hardware signals (SIGFPE, SIGSEGV). +func recoverSignals() { + recoverMustPanic("divide by zero", func() { + var x int + println(1 / x) + }) + recoverMustPanic("nil pointer dereference", func() { + var p *int + println(*p) + }) +} diff --git a/testdata/recover.txt b/testdata/recover.txt index 87e4ba5d17..693ddf10ac 100644 --- a/testdata/recover.txt +++ b/testdata/recover.txt @@ -28,5 +28,28 @@ recovered: panic 2 defer panic recovered from deferred call: deferred panic +# indirect recover +indirect recover returned: indirect panic + # runtime.Goexit Goexit deferred function, recover is nil: true + +# repanic +inner, repanicking +outer recovered: repanic value + +# recover runtime errors + recovered: index + recovered: index from helper + recovered: slice + recovered: type assert + recovered: empty interface type assert + +# recover from nil map and closed channel + recovered: nil map + recovered: closed chan + recovered: close nil chan + +# recover from hardware signals + recovered: divide by zero + recovered: nil pointer dereference