From b95c9b758e60bb53a05a968e1e1657644663e569 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:07:11 -0700 Subject: [PATCH] compiler: avoid runtime panic string heap allocs Lower constant-string calls to runtime.runtimePanic and runtime.runtimePanicAt so they pass a runtime.plainError interface value to runtimePanicAtMsg. This uses static interface backing storage for recoverable runtime panics instead of allocating a string header at run time. Keep dynamic runtimePanic(msg) as the exact fallback. It constructs the runtime.Error value only when a recover frame is present, so trap/abort paths do not eagerly box the string. --- builder/sizes_test.go | 6 +++--- compiler/compiler.go | 16 ++++++++++++++++ main_test.go | 17 +++++++++++++++++ src/runtime/panic.go | 15 +++++++++++++-- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/builder/sizes_test.go b/builder/sizes_test.go index e7c2b763ba..9b07c67cf6 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", 3699, 297, 0, 2252}, - {"microbit", "examples/serial", 2736, 356, 8, 2248}, - {"wioterminal", "examples/pininterrupt", 7960, 1652, 132, 7480}, + {"hifive1b", "examples/echo", 3760, 352, 0, 2252}, + {"microbit", "examples/serial", 2761, 395, 8, 2248}, + {"wioterminal", "examples/pininterrupt", 7990, 1710, 132, 7480}, // TODO: also check wasm. Right now this is difficult, because // wasm binaries are run through wasm-opt and therefore the diff --git a/compiler/compiler.go b/compiler/compiler.go index 2b3419ed48..eaf35591bf 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -2032,6 +2032,22 @@ func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error) for _, param := range instr.Args { params = append(params, b.getValue(param, getPos(instr))) } + if fn := instr.StaticCallee(); fn != nil { + switch b.getFunctionInfo(fn).linkName { + case "runtime.runtimePanic": + if len(params) == 1 && params[0].IsConstant() { + errType := b.program.ImportedPackage("runtime").Members["plainError"].(*ssa.Type).Type() + err := b.createMakeInterface(params[0], errType, instr.Pos()) + return b.createRuntimeCall("runtimePanicValue", []llvm.Value{err, params[0]}, ""), nil + } + case "runtime.runtimePanicAt": + if len(params) == 2 && params[1].IsConstant() { + errType := b.program.ImportedPackage("runtime").Members["plainError"].(*ssa.Type).Type() + err := b.createMakeInterface(params[1], errType, instr.Pos()) + return b.createRuntimeCall("runtimePanicAtMsg", []llvm.Value{params[0], err, params[1]}, ""), nil + } + } + } // Try to call the function directly for trivially static calls. var callee, context llvm.Value diff --git a/main_test.go b/main_test.go index 0da9e5b6b5..d74abf5290 100644 --- a/main_test.go +++ b/main_test.go @@ -148,6 +148,23 @@ 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("ldflags", func(t *testing.T) { t.Parallel() opts := optionsFromTarget("", sema) diff --git a/src/runtime/panic.go b/src/runtime/panic.go index f28cb75a47..ab13ffc60f 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -84,12 +84,20 @@ func panicOrGoexit(message interface{}, panicking panicState) { // 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 + // As long as this function is inlined, llvm.returnaddress(0) will return // something sensible. runtimePanicAt(returnAddress(0), msg) } func runtimePanicAt(addr unsafe.Pointer, msg string) { + runtimePanicAtMsg(addr, nil, msg) +} + +func runtimePanicValue(err interface{}, msg string) { + runtimePanicAtMsg(returnAddress(0), err, msg) +} + +func runtimePanicAtMsg(addr unsafe.Pointer, err interface{}, msg string) { if panicStrategy() == tinygo.PanicStrategyTrap { trap() } @@ -98,7 +106,10 @@ 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) + if err == nil { + err = plainError(msg) + } + frame.PanicValue = err frame.Panicking = panicTrue tinygo_longjmp(frame) // unreachable