runtime: avoid heap allocs for plain errors

This commit is contained in:
Jake Bailey
2026-07-27 09:08:47 -07:00
committed by Ron Evans
parent 1d2bc0fd64
commit 206d9ae8e0
17 changed files with 132 additions and 50 deletions
+4 -4
View File
@@ -207,7 +207,7 @@ func (ch *channel) trySend(value unsafe.Pointer) (sent bool, wake *task.Task) {
// Note: we cannot currently recover from this panic.
// There's some state in the select statement especially that would be
// corrupted if we allowed recovering from this panic.
runtimePanic("send on closed channel")
runtimePanic(errSendOnClosedChannel)
}
// There is no value in the buffer and we have a receiver available. Copy
@@ -266,7 +266,7 @@ func chanSend(ch *channel, value unsafe.Pointer, op *channelOp) {
// closed while sending).
if t.DataUint32() == chanOperationClosed {
// Oops, this channel was closed while sending!
runtimePanic("send on closed channel")
runtimePanic(errSendOnClosedChannel)
}
}
@@ -349,7 +349,7 @@ func chanRecv(ch *channel, value unsafe.Pointer, op *channelOp) bool {
func chanClose(ch *channel) {
if ch == nil {
// Not allowed by the language spec.
runtimePanic("close of nil channel")
runtimePanic(errCloseNilChannel)
}
mask := interrupt.Disable()
@@ -359,7 +359,7 @@ func chanClose(ch *channel) {
// Not allowed by the language spec.
ch.lock.Unlock()
interrupt.Restore(mask)
runtimePanic("close of closed channel")
runtimePanic(errCloseClosedChannel)
}
// Collect waiters and wake them after unlocking.
+27
View File
@@ -12,3 +12,30 @@ type plainError string
func (e plainError) Error() string { return string(e) }
func (e plainError) RuntimeError() {}
const (
errNilPointer = plainError("nil pointer dereference")
errNilMap = plainError("assignment to entry in nil map")
errIndexOutOfRange = plainError("index out of range")
errSliceOutOfRange = plainError("slice out of range")
errSliceToArray = plainError("slice smaller than array")
errUnsafeSliceLength = plainError("unsafe.Slice/String: len out of range")
errChannelTooBig = plainError("new channel is too big")
errNegativeShift = plainError("negative shift")
errDivideByZero = plainError("divide by zero")
errBlockingExported = plainError("trying to do blocking operation in exported function")
errTimersUnsupported = plainError("timers not supported without a scheduler")
errIntegerOverflow = plainError("integer overflow")
errUnsupportedSignal = plainError("unsupported signal number")
errUnsupportedExit = plainError("unsupported: syscall.Exit")
errSendOnClosedChannel = plainError("send on closed channel")
errCloseNilChannel = plainError("close of nil channel")
errCloseClosedChannel = plainError("close of closed channel")
errWasmBeforeInit = plainError("//go:wasmexport function called before runtime initialization")
errWasmAfterMain = plainError("//go:wasmexport function called after main.main returned")
errWasmDidNotFinish = plainError("//go:wasmexport function did not finish")
errUncomparable = plainError("comparing un-comparable type")
errTypeAssert = plainError("type assert failed")
errSchedulerDisabled = plainError("scheduler is disabled")
errOutOfMemory = plainError("out of memory")
)
+1 -1
View File
@@ -49,7 +49,7 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
heapptr += size
if heapptr < addr {
// The allocation size overflowed the heap pointer.
runtimePanic("out of memory")
runtimePanic(errOutOfMemory)
}
for heapptr > heapEnd {
// Try to increase the heap and check again.
+1 -1
View File
@@ -832,7 +832,7 @@ func hashmapInterfaceHash(itf interface{}, seed uintptr) uint32 {
}
return hash
default:
runtimePanic("comparing un-comparable type")
runtimePanic(errUncomparable)
return 0 // unreachable
}
}
+2 -2
View File
@@ -77,7 +77,7 @@ func reflectValueEqual(x, y reflectlite.Value) bool {
case reflectlite.Interface:
return reflectValueEqual(x.Elem(), y.Elem())
default:
runtimePanic("comparing un-comparable type")
runtimePanic(errUncomparable)
return false // unreachable
}
}
@@ -86,7 +86,7 @@ func reflectValueEqual(x, y reflectlite.Value) bool {
// returns false.
func interfaceTypeAssert(ok bool) {
if !ok {
runtimePanic("type assert failed")
runtimePanic(errTypeAssert)
}
}
+21 -17
View File
@@ -84,11 +84,11 @@ func panicOrGoexit(message interface{}, panicking panicState) {
abort()
}
// Cause a runtime panic, which is (currently) always a string.
func runtimePanic(msg string) {
// As long as this function is inined, llvm.returnaddress(0) will return
// Cause a recoverable runtime panic.
func runtimePanic(err Error) {
// As long as this function is inlined, llvm.returnaddress(0) will return
// something sensible.
runtimePanicAt(returnAddress(0), msg)
runtimePanicAt(returnAddress(0), err)
}
// runtimeFatal terminates for runtime failures that cannot safely be
@@ -103,7 +103,7 @@ func runtimeFatal(msg string) {
abort()
}
func runtimePanicAt(addr unsafe.Pointer, msg string) {
func runtimePanicAt(addr unsafe.Pointer, err Error) {
if panicStrategy() == tinygo.PanicStrategyTrap {
trap()
}
@@ -112,7 +112,7 @@ func runtimePanicAt(addr unsafe.Pointer, msg string) {
if frame != nil {
// Use the normal panic mechanism so that this runtime error
// can be recovered with recover().
frame.PanicValue = plainError(msg)
frame.PanicValue = err
frame.Panicking = panicTrue | (frame.Panicking & panicGoexit)
tinygo_longjmp(frame)
// unreachable
@@ -128,11 +128,15 @@ func runtimePanicAt(addr unsafe.Pointer, msg string) {
} else {
printstring("panic: runtime error: ")
}
printstring(msg)
printstring(err.Error())
printnl()
abort()
}
func runtimePanicSchedulerDisabled() {
runtimePanicAt(returnAddress(0), errSchedulerDisabled)
}
// 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
@@ -220,54 +224,54 @@ func _recover(useParentFrame bool) interface{} {
// Panic when trying to dereference a nil pointer.
func nilPanic() {
runtimePanicAt(returnAddress(0), "nil pointer dereference")
runtimePanicAt(returnAddress(0), errNilPointer)
}
// Panic when trying to add an entry to a nil map
func nilMapPanic() {
runtimePanicAt(returnAddress(0), "assignment to entry in nil map")
runtimePanicAt(returnAddress(0), errNilMap)
}
// Panic when trying to access an array or slice out of bounds.
func lookupPanic() {
runtimePanicAt(returnAddress(0), "index out of range")
runtimePanicAt(returnAddress(0), errIndexOutOfRange)
}
// Panic when trying to slice a slice out of bounds.
func slicePanic() {
runtimePanicAt(returnAddress(0), "slice out of range")
runtimePanicAt(returnAddress(0), errSliceOutOfRange)
}
// Panic when trying to convert a slice to an array pointer (Go 1.17+) and the
// slice is shorter than the array.
func sliceToArrayPointerPanic() {
runtimePanicAt(returnAddress(0), "slice smaller than array")
runtimePanicAt(returnAddress(0), errSliceToArray)
}
// Panic when calling unsafe.Slice() (Go 1.17+) or unsafe.String() (Go 1.20+)
// with a len that's too large (which includes if the ptr is nil and len is
// nonzero).
func unsafeSlicePanic() {
runtimePanicAt(returnAddress(0), "unsafe.Slice/String: len out of range")
runtimePanicAt(returnAddress(0), errUnsafeSliceLength)
}
// Panic when trying to create a new channel that is too big.
func chanMakePanic() {
runtimePanicAt(returnAddress(0), "new channel is too big")
runtimePanicAt(returnAddress(0), errChannelTooBig)
}
// Panic when a shift value is negative.
func negativeShiftPanic() {
runtimePanicAt(returnAddress(0), "negative shift")
runtimePanicAt(returnAddress(0), errNegativeShift)
}
// Panic when there is a divide by zero.
func divideByZeroPanic() {
runtimePanicAt(returnAddress(0), "divide by zero")
runtimePanicAt(returnAddress(0), errDivideByZero)
}
func blockingPanic() {
runtimePanicAt(returnAddress(0), "trying to do blocking operation in exported function")
runtimePanicAt(returnAddress(0), errBlockingExported)
}
//go:linkname fips_fatal crypto/internal/fips140.fatal
+1 -1
View File
@@ -34,7 +34,7 @@ func syscall_Exit(code int) {
// Because this is the "unknown" target we can't call an exit function.
// But we also can't just return since the program will likely expect this
// function to never return. So we panic instead.
runtimePanic("unsupported: syscall.Exit")
runtimePanic(errUnsupportedExit)
}
// There is not yet any support for any form of parallelism on WebAssembly, so these
+5 -5
View File
@@ -151,9 +151,9 @@ func tinygo_sigpanic() {
sig := tinygo_caught_signal
switch sig {
case sig_SIGSEGV, sig_SIGBUS:
runtimePanic("nil pointer dereference")
runtimePanic(errNilPointer)
case sig_SIGFPE:
runtimePanic("divide by zero")
runtimePanic(errDivideByZero)
default:
runtimeFatal("signal")
}
@@ -382,7 +382,7 @@ func signal_enable(s uint32) {
if s >= 32 {
// TODO: to support higher signal numbers, we need to turn
// receivedSignals into a uint32 array.
runtimePanicAt(returnAddress(0), "unsupported signal number")
runtimePanicAt(returnAddress(0), errUnsupportedSignal)
}
// This is intentonally a non-atomic store. This is safe, since hasSignals
@@ -399,7 +399,7 @@ func signal_ignore(s uint32) {
if s >= 32 {
// TODO: to support higher signal numbers, we need to turn
// receivedSignals into a uint32 array.
runtimePanicAt(returnAddress(0), "unsupported signal number")
runtimePanicAt(returnAddress(0), errUnsupportedSignal)
}
tinygo_signal_ignore(s)
}
@@ -409,7 +409,7 @@ func signal_disable(s uint32) {
if s >= 32 {
// TODO: to support higher signal numbers, we need to turn
// receivedSignals into a uint32 array.
runtimePanicAt(returnAddress(0), "unsupported signal number")
runtimePanicAt(returnAddress(0), errUnsupportedSignal)
}
tinygo_signal_disable(s)
}
+3 -3
View File
@@ -66,9 +66,9 @@ var initializeCalled bool
func wasmExportCheckRun() {
switch {
case !initializeCalled:
runtimePanic("//go:wasmexport function called before runtime initialization")
runtimePanic(errWasmBeforeInit)
case mainExited:
runtimePanic("//go:wasmexport function called after main.main returned")
runtimePanic(errWasmAfterMain)
}
}
@@ -81,6 +81,6 @@ func wasmExportCheckRun() {
func wasmExportRun(done *bool) {
scheduler(true)
if !*done {
runtimePanic("//go:wasmexport function did not finish")
runtimePanic(errWasmDidNotFinish)
}
}
+3 -3
View File
@@ -315,11 +315,11 @@ func tinygo_init_exception_handler()
func tinygo_sigpanic_windows(exceptionCode int32) {
switch uint32(exceptionCode) {
case _EXCEPTION_ACCESS_VIOLATION, _EXCEPTION_IN_PAGE_ERROR:
runtimePanic("nil pointer dereference")
runtimePanic(errNilPointer)
case _EXCEPTION_INT_DIVIDE_BY_ZERO:
runtimePanic("divide by zero")
runtimePanic(errDivideByZero)
case _EXCEPTION_INT_OVERFLOW:
runtimePanic("integer overflow")
runtimePanic(errIntegerOverflow)
default:
runtimeFatal("unknown exception")
}
+3 -3
View File
@@ -60,15 +60,15 @@ func NumCPU() int {
}
func addTimer(tim *timerNode) {
runtimePanic("timers not supported without a scheduler")
runtimePanic(errTimersUnsupported)
}
func reAddTimer(tn *timerNode) {
runtimePanic("timers not supported without a scheduler")
runtimePanic(errTimersUnsupported)
}
func removeTimer(tim *timer) *timerNode {
runtimePanic("timers not supported without a scheduler")
runtimePanic(errTimersUnsupported)
return nil
}