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
+3 -3
View File
@@ -1,4 +1,4 @@
target package code rodata data bss
hifive1b examples/echo 4533 323 0 2268
microbit examples/serial 3002 382 8 2264
wioterminal examples/pininterrupt 8331 1717 148 7496
hifive1b examples/echo 4526 346 0 2268
microbit examples/serial 2993 391 8 2264
wioterminal examples/pininterrupt 8309 1739 148 7496
+33
View File
@@ -168,6 +168,39 @@ func TestBuild(t *testing.T) {
runTestWithConfig("print.go", t, opts, nil, nil)
})
t.Run("gc=none-runtime-panic", func(t *testing.T) {
t.Parallel()
opts := optionsFromTarget("cortex-m-qemu", sema)
opts.GC = "none"
opts.Scheduler = "none"
config, err := builder.NewConfig(&opts)
if err != nil {
t.Fatal(err)
}
err = Build("testdata/trivialpanic.go", t.TempDir()+"/trivialpanic", config)
if err != nil {
w := &bytes.Buffer{}
diagnostics.CreateDiagnostics(err).WriteTo(w, "")
t.Fatal(w.String())
}
})
t.Run("scheduler=none-uefi", func(t *testing.T) {
t.Parallel()
opts := optionsFromTarget("uefi-amd64", sema)
opts.Scheduler = "none"
config, err := builder.NewConfig(&opts)
if err != nil {
t.Fatal(err)
}
err = Build("testdata/trivialpanic.go", t.TempDir()+"/trivialpanic", config)
if err != nil {
w := &bytes.Buffer{}
diagnostics.CreateDiagnostics(err).WriteTo(w, "")
t.Fatal(w.String())
}
})
t.Run("ldflags", func(t *testing.T) {
t.Parallel()
opts := optionsFromTarget("", sema)
+6 -6
View File
@@ -7,11 +7,11 @@ import "unsafe"
// There is only one goroutine so the task struct can be a global.
var mainTask Task
//go:linkname runtimePanic runtime.runtimePanic
func runtimePanic(str string)
//go:linkname runtimePanicSchedulerDisabled runtime.runtimePanicSchedulerDisabled
func runtimePanicSchedulerDisabled()
func Pause() {
runtimePanic("scheduler is disabled")
runtimePanicSchedulerDisabled()
}
func Current() *Task {
@@ -22,13 +22,13 @@ func Current() *Task {
//go:noinline
func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
// The compiler will error if this is reachable.
runtimePanic("scheduler is disabled")
runtimePanicSchedulerDisabled()
}
type state struct{}
func (t *Task) Resume() {
runtimePanic("scheduler is disabled")
runtimePanicSchedulerDisabled()
}
// OnSystemStack returns whether the caller is running on the system stack.
@@ -39,7 +39,7 @@ func OnSystemStack() bool {
func SystemStack() uintptr {
// System stack is the current stack, so this shouldn't be called.
runtimePanic("scheduler is disabled")
runtimePanicSchedulerDisabled()
return 0 // unreachable
}
+1 -1
View File
@@ -4,5 +4,5 @@ package task
//go:export tinygo_task_exit
func taskExit() {
runtimePanic("scheduler is disabled")
runtimePanicSchedulerDisabled()
}
+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
}
+17
View File
@@ -313,12 +313,29 @@ func recoverMustPanic(name string, f func()) {
f()
}
func recoverRuntimeErrorValue(f func()) {
defer func() {
r := recover()
err, ok := r.(runtime.Error)
if ok {
println(" recovered runtime error:", err.Error())
} else {
println(" failed runtime error:", r)
}
}()
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
})
recoverRuntimeErrorValue(func() {
var m map[string]int
m["x"] = 1
})
recoverMustPanic("closed chan", func() {
ch := make(chan int)
close(ch)
+1
View File
@@ -52,6 +52,7 @@ outer recovered: repanic value
# recover from nil map and closed channel
recovered: nil map
recovered runtime error: assignment to entry in nil map
recovered: closed chan
recovered: close nil chan