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 target package code rodata data bss
hifive1b examples/echo 4533 323 0 2268 hifive1b examples/echo 4526 346 0 2268
microbit examples/serial 3002 382 8 2264 microbit examples/serial 2993 391 8 2264
wioterminal examples/pininterrupt 8331 1717 148 7496 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) 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.Run("ldflags", func(t *testing.T) {
t.Parallel() t.Parallel()
opts := optionsFromTarget("", sema) 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. // There is only one goroutine so the task struct can be a global.
var mainTask Task var mainTask Task
//go:linkname runtimePanic runtime.runtimePanic //go:linkname runtimePanicSchedulerDisabled runtime.runtimePanicSchedulerDisabled
func runtimePanic(str string) func runtimePanicSchedulerDisabled()
func Pause() { func Pause() {
runtimePanic("scheduler is disabled") runtimePanicSchedulerDisabled()
} }
func Current() *Task { func Current() *Task {
@@ -22,13 +22,13 @@ func Current() *Task {
//go:noinline //go:noinline
func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) { func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
// The compiler will error if this is reachable. // The compiler will error if this is reachable.
runtimePanic("scheduler is disabled") runtimePanicSchedulerDisabled()
} }
type state struct{} type state struct{}
func (t *Task) Resume() { func (t *Task) Resume() {
runtimePanic("scheduler is disabled") runtimePanicSchedulerDisabled()
} }
// OnSystemStack returns whether the caller is running on the system stack. // OnSystemStack returns whether the caller is running on the system stack.
@@ -39,7 +39,7 @@ func OnSystemStack() bool {
func SystemStack() uintptr { func SystemStack() uintptr {
// System stack is the current stack, so this shouldn't be called. // System stack is the current stack, so this shouldn't be called.
runtimePanic("scheduler is disabled") runtimePanicSchedulerDisabled()
return 0 // unreachable return 0 // unreachable
} }
+1 -1
View File
@@ -4,5 +4,5 @@ package task
//go:export tinygo_task_exit //go:export tinygo_task_exit
func taskExit() { 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. // Note: we cannot currently recover from this panic.
// There's some state in the select statement especially that would be // There's some state in the select statement especially that would be
// corrupted if we allowed recovering from this panic. // 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 // 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). // closed while sending).
if t.DataUint32() == chanOperationClosed { if t.DataUint32() == chanOperationClosed {
// Oops, this channel was closed while sending! // 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) { func chanClose(ch *channel) {
if ch == nil { if ch == nil {
// Not allowed by the language spec. // Not allowed by the language spec.
runtimePanic("close of nil channel") runtimePanic(errCloseNilChannel)
} }
mask := interrupt.Disable() mask := interrupt.Disable()
@@ -359,7 +359,7 @@ func chanClose(ch *channel) {
// Not allowed by the language spec. // Not allowed by the language spec.
ch.lock.Unlock() ch.lock.Unlock()
interrupt.Restore(mask) interrupt.Restore(mask)
runtimePanic("close of closed channel") runtimePanic(errCloseClosedChannel)
} }
// Collect waiters and wake them after unlocking. // 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) Error() string { return string(e) }
func (e plainError) RuntimeError() {} 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 heapptr += size
if heapptr < addr { if heapptr < addr {
// The allocation size overflowed the heap pointer. // The allocation size overflowed the heap pointer.
runtimePanic("out of memory") runtimePanic(errOutOfMemory)
} }
for heapptr > heapEnd { for heapptr > heapEnd {
// Try to increase the heap and check again. // 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 return hash
default: default:
runtimePanic("comparing un-comparable type") runtimePanic(errUncomparable)
return 0 // unreachable return 0 // unreachable
} }
} }
+2 -2
View File
@@ -77,7 +77,7 @@ func reflectValueEqual(x, y reflectlite.Value) bool {
case reflectlite.Interface: case reflectlite.Interface:
return reflectValueEqual(x.Elem(), y.Elem()) return reflectValueEqual(x.Elem(), y.Elem())
default: default:
runtimePanic("comparing un-comparable type") runtimePanic(errUncomparable)
return false // unreachable return false // unreachable
} }
} }
@@ -86,7 +86,7 @@ func reflectValueEqual(x, y reflectlite.Value) bool {
// returns false. // returns false.
func interfaceTypeAssert(ok bool) { func interfaceTypeAssert(ok bool) {
if !ok { if !ok {
runtimePanic("type assert failed") runtimePanic(errTypeAssert)
} }
} }
+21 -17
View File
@@ -84,11 +84,11 @@ func panicOrGoexit(message interface{}, panicking panicState) {
abort() abort()
} }
// Cause a runtime panic, which is (currently) always a string. // Cause a recoverable runtime panic.
func runtimePanic(msg string) { func runtimePanic(err Error) {
// As long as this function is inined, llvm.returnaddress(0) will return // As long as this function is inlined, llvm.returnaddress(0) will return
// something sensible. // something sensible.
runtimePanicAt(returnAddress(0), msg) runtimePanicAt(returnAddress(0), err)
} }
// runtimeFatal terminates for runtime failures that cannot safely be // runtimeFatal terminates for runtime failures that cannot safely be
@@ -103,7 +103,7 @@ func runtimeFatal(msg string) {
abort() abort()
} }
func runtimePanicAt(addr unsafe.Pointer, msg string) { func runtimePanicAt(addr unsafe.Pointer, err Error) {
if panicStrategy() == tinygo.PanicStrategyTrap { if panicStrategy() == tinygo.PanicStrategyTrap {
trap() trap()
} }
@@ -112,7 +112,7 @@ func runtimePanicAt(addr unsafe.Pointer, msg string) {
if frame != nil { if frame != nil {
// Use the normal panic mechanism so that this runtime error // Use the normal panic mechanism so that this runtime error
// can be recovered with recover(). // can be recovered with recover().
frame.PanicValue = plainError(msg) frame.PanicValue = err
frame.Panicking = panicTrue | (frame.Panicking & panicGoexit) frame.Panicking = panicTrue | (frame.Panicking & panicGoexit)
tinygo_longjmp(frame) tinygo_longjmp(frame)
// unreachable // unreachable
@@ -128,11 +128,15 @@ func runtimePanicAt(addr unsafe.Pointer, msg string) {
} else { } else {
printstring("panic: runtime error: ") printstring("panic: runtime error: ")
} }
printstring(msg) printstring(err.Error())
printnl() printnl()
abort() abort()
} }
func runtimePanicSchedulerDisabled() {
runtimePanicAt(returnAddress(0), errSchedulerDisabled)
}
// Called at the start of a function that includes a deferred call. // Called at the start of a function that includes a deferred call.
// It gets passed in the stack-allocated defer frame and configures it. // 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 // 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. // Panic when trying to dereference a nil pointer.
func nilPanic() { func nilPanic() {
runtimePanicAt(returnAddress(0), "nil pointer dereference") runtimePanicAt(returnAddress(0), errNilPointer)
} }
// Panic when trying to add an entry to a nil map // Panic when trying to add an entry to a nil map
func nilMapPanic() { 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. // Panic when trying to access an array or slice out of bounds.
func lookupPanic() { func lookupPanic() {
runtimePanicAt(returnAddress(0), "index out of range") runtimePanicAt(returnAddress(0), errIndexOutOfRange)
} }
// Panic when trying to slice a slice out of bounds. // Panic when trying to slice a slice out of bounds.
func slicePanic() { 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 // Panic when trying to convert a slice to an array pointer (Go 1.17+) and the
// slice is shorter than the array. // slice is shorter than the array.
func sliceToArrayPointerPanic() { 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+) // 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 // with a len that's too large (which includes if the ptr is nil and len is
// nonzero). // nonzero).
func unsafeSlicePanic() { 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. // Panic when trying to create a new channel that is too big.
func chanMakePanic() { func chanMakePanic() {
runtimePanicAt(returnAddress(0), "new channel is too big") runtimePanicAt(returnAddress(0), errChannelTooBig)
} }
// Panic when a shift value is negative. // Panic when a shift value is negative.
func negativeShiftPanic() { func negativeShiftPanic() {
runtimePanicAt(returnAddress(0), "negative shift") runtimePanicAt(returnAddress(0), errNegativeShift)
} }
// Panic when there is a divide by zero. // Panic when there is a divide by zero.
func divideByZeroPanic() { func divideByZeroPanic() {
runtimePanicAt(returnAddress(0), "divide by zero") runtimePanicAt(returnAddress(0), errDivideByZero)
} }
func blockingPanic() { 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 //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. // 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 // But we also can't just return since the program will likely expect this
// function to never return. So we panic instead. // 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 // 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 sig := tinygo_caught_signal
switch sig { switch sig {
case sig_SIGSEGV, sig_SIGBUS: case sig_SIGSEGV, sig_SIGBUS:
runtimePanic("nil pointer dereference") runtimePanic(errNilPointer)
case sig_SIGFPE: case sig_SIGFPE:
runtimePanic("divide by zero") runtimePanic(errDivideByZero)
default: default:
runtimeFatal("signal") runtimeFatal("signal")
} }
@@ -382,7 +382,7 @@ func signal_enable(s uint32) {
if s >= 32 { if s >= 32 {
// TODO: to support higher signal numbers, we need to turn // TODO: to support higher signal numbers, we need to turn
// receivedSignals into a uint32 array. // 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 // This is intentonally a non-atomic store. This is safe, since hasSignals
@@ -399,7 +399,7 @@ func signal_ignore(s uint32) {
if s >= 32 { if s >= 32 {
// TODO: to support higher signal numbers, we need to turn // TODO: to support higher signal numbers, we need to turn
// receivedSignals into a uint32 array. // receivedSignals into a uint32 array.
runtimePanicAt(returnAddress(0), "unsupported signal number") runtimePanicAt(returnAddress(0), errUnsupportedSignal)
} }
tinygo_signal_ignore(s) tinygo_signal_ignore(s)
} }
@@ -409,7 +409,7 @@ func signal_disable(s uint32) {
if s >= 32 { if s >= 32 {
// TODO: to support higher signal numbers, we need to turn // TODO: to support higher signal numbers, we need to turn
// receivedSignals into a uint32 array. // receivedSignals into a uint32 array.
runtimePanicAt(returnAddress(0), "unsupported signal number") runtimePanicAt(returnAddress(0), errUnsupportedSignal)
} }
tinygo_signal_disable(s) tinygo_signal_disable(s)
} }
+3 -3
View File
@@ -66,9 +66,9 @@ var initializeCalled bool
func wasmExportCheckRun() { func wasmExportCheckRun() {
switch { switch {
case !initializeCalled: case !initializeCalled:
runtimePanic("//go:wasmexport function called before runtime initialization") runtimePanic(errWasmBeforeInit)
case mainExited: case mainExited:
runtimePanic("//go:wasmexport function called after main.main returned") runtimePanic(errWasmAfterMain)
} }
} }
@@ -81,6 +81,6 @@ func wasmExportCheckRun() {
func wasmExportRun(done *bool) { func wasmExportRun(done *bool) {
scheduler(true) scheduler(true)
if !*done { 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) { func tinygo_sigpanic_windows(exceptionCode int32) {
switch uint32(exceptionCode) { switch uint32(exceptionCode) {
case _EXCEPTION_ACCESS_VIOLATION, _EXCEPTION_IN_PAGE_ERROR: case _EXCEPTION_ACCESS_VIOLATION, _EXCEPTION_IN_PAGE_ERROR:
runtimePanic("nil pointer dereference") runtimePanic(errNilPointer)
case _EXCEPTION_INT_DIVIDE_BY_ZERO: case _EXCEPTION_INT_DIVIDE_BY_ZERO:
runtimePanic("divide by zero") runtimePanic(errDivideByZero)
case _EXCEPTION_INT_OVERFLOW: case _EXCEPTION_INT_OVERFLOW:
runtimePanic("integer overflow") runtimePanic(errIntegerOverflow)
default: default:
runtimeFatal("unknown exception") runtimeFatal("unknown exception")
} }
+3 -3
View File
@@ -60,15 +60,15 @@ func NumCPU() int {
} }
func addTimer(tim *timerNode) { func addTimer(tim *timerNode) {
runtimePanic("timers not supported without a scheduler") runtimePanic(errTimersUnsupported)
} }
func reAddTimer(tn *timerNode) { func reAddTimer(tn *timerNode) {
runtimePanic("timers not supported without a scheduler") runtimePanic(errTimersUnsupported)
} }
func removeTimer(tim *timer) *timerNode { func removeTimer(tim *timer) *timerNode {
runtimePanic("timers not supported without a scheduler") runtimePanic(errTimersUnsupported)
return nil return nil
} }
+17
View File
@@ -313,12 +313,29 @@ func recoverMustPanic(name string, f func()) {
f() 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. // Test recovering from nil map assignment and closed channel send.
func recoverNilMapAndChan() { func recoverNilMapAndChan() {
recoverMustPanic("nil map", func() { recoverMustPanic("nil map", func() {
var m map[string]int var m map[string]int
m["x"] = 1 m["x"] = 1
}) })
recoverRuntimeErrorValue(func() {
var m map[string]int
m["x"] = 1
})
recoverMustPanic("closed chan", func() { recoverMustPanic("closed chan", func() {
ch := make(chan int) ch := make(chan int)
close(ch) close(ch)
+1
View File
@@ -52,6 +52,7 @@ outer recovered: repanic value
# recover from nil map and closed channel # recover from nil map and closed channel
recovered: nil map recovered: nil map
recovered runtime error: assignment to entry in nil map
recovered: closed chan recovered: closed chan
recovered: close nil chan recovered: close nil chan