compiler: implement recover() built-in function

This commit is contained in:
Ayke van Laethem
2021-11-29 14:54:34 +01:00
committed by Ron Evans
parent 79ba6a50c3
commit 8d6b210c09
31 changed files with 868 additions and 191 deletions
@@ -21,3 +21,13 @@ tinygo_scanCurrentStack:
// were only pushed to be discoverable by the GC.
addl $28, %esp
retl
.section .text.tinygo_longjmp
.global tinygo_longjmp
tinygo_longjmp:
// Note: the code we jump to assumes eax is set to a non-zero value if we
// jump from here.
movl 8(%esp), %eax // jumpPC (stash in volatile register)
movl 4(%esp), %esp // jumpSP
jmpl *%eax
@@ -28,6 +28,22 @@ _tinygo_scanCurrentStack:
addq $56, %rsp
retq
#ifdef __ELF__
.section .text.tinygo_longjmp
.global tinygo_longjmp
tinygo_longjmp:
#else // Darwin
.global _tinygo_longjmp
_tinygo_longjmp:
#endif
// Note: the code we jump to assumes rax is non-zero so we have to load it
// with some value here.
movq $1, %rax
movq %rdi, %rsp // jumpSP
jmpq *%rsi // jumpPC
#ifdef __MACH__ // Darwin
// allow these symbols to stripped as dead code
.subsections_via_symbols
@@ -20,3 +20,12 @@ tinygo_scanCurrentStack:
// were only pushed to be discoverable by the GC.
addq $72, %rsp
retq
.section .text.tinygo_longjmp,"ax"
.global tinygo_longjmp
tinygo_longjmp:
// Note: the code we jump to assumes rax is non-zero so we have to load it
// with some value here.
movq $1, %rax
movq %rcx, %rsp // jumpSP
jmpq *%rdx // jumpPC
@@ -31,3 +31,16 @@ tinygo_scanCurrentStack:
pop {pc}
.cfi_endproc
.size tinygo_scanCurrentStack, .-tinygo_scanCurrentStack
.section .text.tinygo_longjmp
.global tinygo_longjmp
.type tinygo_longjmp, %function
tinygo_longjmp:
.cfi_startproc
// Note: the code we jump to assumes r0 is set to a non-zero value if we
// jump from here (which is conveniently already the case).
mov sp, r0 // jumpSP
mov pc, r1 // jumpPC
.cfi_endproc
.size tinygo_longjmp, .-tinygo_longjmp
@@ -30,3 +30,18 @@ tinygo_scanCurrentStack:
// Restore stack state and return.
ldp x29, x30, [sp], #96
ret
#ifdef __MACH__
.global _tinygo_longjmp
_tinygo_longjmp:
#else
.section .text.tinygo_longjmp
.global tinygo_longjmp
.type tinygo_longjmp, %function
tinygo_longjmp:
#endif
// Note: the code we jump to assumes x0 is set to a non-zero value if we
// jump from here (which is conveniently already the case).
mov sp, x0 // jumpSP
br x1 // jumpPC
@@ -40,3 +40,12 @@ tinygo_scanCurrentStack:
// Return to the caller.
ret
.section .text.tinygo_longjmp
.global tinygo_longjmp
tinygo_longjmp:
// Note: the code we jump to assumes a0 is non-zero, which is already the
// case because that's jumpSP (the stack pointer).
mv sp, a0 // jumpSP
jr a1 // jumpPC
+76 -4
View File
@@ -1,12 +1,35 @@
package runtime
import (
"internal/task"
"unsafe"
)
// trap is a compiler hint that this function cannot be executed. It is
// translated into either a trap instruction or a call to abort().
//export llvm.trap
func trap()
// Inline assembly stub. It is essentially C longjmp but modified a bit for the
// purposes of TinyGo. It restores the stack pointer and jumps to the given pc.
//export tinygo_longjmp
func tinygo_longjmp(sp, pc unsafe.Pointer)
// Compiler intrinsic.
// Returns whether recover is supported on the current architecture.
func supportsRecover() bool
// Builtin function panic(msg), used as a compiler intrinsic.
func _panic(message interface{}) {
if supportsRecover() {
frame := task.Current().DeferFrame
if frame != nil {
frame.PanicValue = message
frame.Panicking = true
tinygo_longjmp(frame.JumpSP, frame.JumpPC)
// unreachable
}
}
printstring("panic: ")
printitf(message)
printnl()
@@ -20,10 +43,59 @@ func runtimePanic(msg string) {
abort()
}
// Try to recover a panicking goroutine.
func _recover() interface{} {
// Deferred functions are currently not executed during panic, so there is
// no way this can return anything besides nil.
// Called at the start of a function that includes a deferred call.
// It gets passed in the stack-allocated defer frame and configures it.
// Note that the frame is not zeroed yet, so we need to initialize all values
// that will be used.
//go:inline
//go:nobounds
func setupDeferFrame(frame *task.DeferFrame, jumpSP unsafe.Pointer) {
currentTask := task.Current()
frame.Previous = currentTask.DeferFrame
frame.JumpSP = jumpSP
frame.Panicking = false
currentTask.DeferFrame = frame
}
// Called right before the return instruction. It pops the defer frame from the
// linked list of defer frames. It also re-raises a panic if the goroutine is
// still panicking.
//go:inline
//go:nobounds
func destroyDeferFrame(frame *task.DeferFrame) {
task.Current().DeferFrame = frame.Previous
if frame.Panicking {
// We're still panicking!
// Re-raise the panic now.
_panic(frame.PanicValue)
}
}
// _recover is the built-in recover() function. It tries to recover a currently
// 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.
func _recover(useParentFrame bool) interface{} {
if !supportsRecover() {
// Compiling without stack unwinding support, so make this a no-op.
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 := task.Current().DeferFrame
if useParentFrame {
// Don't recover panic from the current frame (which can't be panicking
// already), but instead from the previous frame.
frame = frame.Previous
}
if frame != nil && frame.Panicking {
// Only the first call to recover returns the panic value. It also stops
// the panicking sequence, hence setting panicking to false.
frame.Panicking = false
return frame.PanicValue
}
// Not panicking, so return a nil interface.
return nil
}