From 1496ad4b807db40bb2d15bd96c613759a8349bf3 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:45:25 -0700 Subject: [PATCH 1/6] compiler: support panic recovery without exceptions Add unwind modes for propagating panic and Goexit without relying on exception handling. Keep setjmp unwinding on existing native targets and use Asyncify automatically when the Asyncify scheduler is selected. Stop Asyncify panic unwinding at the nearest defer frame, then run its deferred calls. Allow ordinary scheduler suspension to pass through the same frame, preserving aggregate results across rewind and nested or concurrent task execution. Add -panic-unwind=auto|explicit. Auto leaves targets without setjmp or Asyncify unchanged, while explicit opts wasm32, riscv64, and Xtensa into return-based unwinding. The trap panic strategy still traps panics immediately, but unwind support remains available for Goexit. Enable recovery, Goexit, and testing package coverage on WebAssembly and WASI targets that use Asyncify. --- GNUmakefile | 3 +- builder/build.go | 1 + builder/config.go | 19 +- builder/sizes_test.go | 6 +- compileopts/config.go | 37 ++ compileopts/options.go | 11 + compileopts/options_test.go | 20 ++ compileopts/target.go | 6 + compileopts/target_test.go | 46 +++ compiler/asserts.go | 6 +- compiler/calls.go | 377 ++++++++++++++++++++- compiler/compiler.go | 100 +++--- compiler/compiler_test.go | 1 + compiler/defer.go | 55 +-- compiler/interface.go | 6 +- compiler/map.go | 12 +- compiler/testdata/basic.ll | 20 +- compiler/testdata/func.ll | 5 +- compiler/testdata/generics.ll | 10 +- compiler/testdata/go1.20.ll | 5 +- compiler/testdata/large.ll | 294 ++++++++++------ compiler/testdata/slice.ll | 55 ++- compiler/testdata/string.ll | 10 +- main.go | 2 + main_test.go | 62 +++- src/internal/task/task_asyncify.go | 12 + src/runtime/panic.go | 80 +++-- src/runtime/panic_unwind_asyncify.go | 15 + src/runtime/panic_unwind_explicit.go | 9 + src/runtime/panic_unwind_none.go | 7 + src/runtime/panic_unwind_return.go | 19 ++ src/runtime/panic_unwind_setjmp.go | 8 + src/runtime/panic_unwind_signal_cores.go | 15 + src/runtime/panic_unwind_signal_unicore.go | 17 + testdata/goexit.go | 3 + testdata/recover-explicit.go | 18 + testdata/recover-explicit.txt | 2 + testdata/recover.go | 16 + testdata/recover.txt | 4 + testdata/testing-wasm.txt | 2 + testdata/testing.go | 19 +- tests/testing/pass/pass_test.go | 63 ++++ transform/optimizer.go | 4 + transform/testdata/unwind.ll | 53 +++ transform/testdata/unwind.out.ll | 57 ++++ transform/transform_test.go | 1 + transform/unwind.go | 53 +++ transform/unwind_test.go | 20 ++ 48 files changed, 1406 insertions(+), 260 deletions(-) create mode 100644 src/runtime/panic_unwind_asyncify.go create mode 100644 src/runtime/panic_unwind_explicit.go create mode 100644 src/runtime/panic_unwind_none.go create mode 100644 src/runtime/panic_unwind_return.go create mode 100644 src/runtime/panic_unwind_setjmp.go create mode 100644 src/runtime/panic_unwind_signal_cores.go create mode 100644 src/runtime/panic_unwind_signal_unicore.go create mode 100644 testdata/recover-explicit.go create mode 100644 testdata/recover-explicit.txt create mode 100644 transform/testdata/unwind.ll create mode 100644 transform/testdata/unwind.out.ll create mode 100644 transform/unwind.go create mode 100644 transform/unwind_test.go diff --git a/GNUmakefile b/GNUmakefile index 627640a520..cb80b6625a 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -510,7 +510,7 @@ TEST_PACKAGES_HOST := $(TEST_PACKAGES_FAST) $(TEST_PACKAGES_WINDOWS) TEST_IOFS := false endif -TEST_SKIP_FLAG := -skip='TestExtraMethods|TestParseAndBytesRoundTrip/P256/Generic|TestAsValidation' +TEST_SKIP_FLAG := -skip='TestExtraMethods|TestAsValidation' TEST_ADDITIONAL_FLAGS ?= # Test known-working standard library packages. @@ -518,7 +518,6 @@ TEST_ADDITIONAL_FLAGS ?= .PHONY: tinygo-test tinygo-test: @# TestExtraMethods: used by many crypto packages and uses reflect.Type.Method which is not implemented. - @# TestParseAndBytesRoundTrip/P256/Generic: needs Goexit to run defers on wasm. $(TINYGO) test $(TEST_ADDITIONAL_FLAGS) $(TEST_SKIP_FLAG) $(filter-out encoding/xml,$(TEST_PACKAGES_HOST)) $(TEST_PACKAGES_SLOW) ifeq ($(TEST_ENCODING_XML),true) $(TINYGO) test $(TEST_ADDITIONAL_FLAGS) $(TEST_SKIP_FLAG) -stack-size=16MB encoding/xml diff --git a/builder/build.go b/builder/build.go index 104e0f3866..da320ac17a 100644 --- a/builder/build.go +++ b/builder/build.go @@ -216,6 +216,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe Debug: !config.Options.SkipDWARF, // emit DWARF except when -internal-nodwarf is passed Nobounds: config.Options.Nobounds, PanicStrategy: config.PanicStrategy(), + PanicUnwind: config.PanicUnwind(), } // Load the target machine, which is the LLVM object that contains all diff --git a/builder/config.go b/builder/config.go index 4dcc2786e8..8aa2363336 100644 --- a/builder/config.go +++ b/builder/config.go @@ -1,6 +1,7 @@ package builder import ( + "errors" "fmt" "runtime" @@ -54,10 +55,24 @@ func NewConfig(options *compileopts.Options) (*compileopts.Config, error) { return nil, fmt.Errorf("cannot compile with Go toolchain version go%d.%d (TinyGo was built using toolchain version %s)", gorootMajor, gorootMinor, runtime.Version()) } - return &compileopts.Config{ + config := &compileopts.Config{ Options: options, Target: spec, GoMinorVersion: gorootMinor, TestConfig: options.TestConfig, - }, nil + } + requestedPanicUnwind := options.PanicUnwind + if requestedPanicUnwind == "" { + requestedPanicUnwind = spec.PanicUnwind + } + if requestedPanicUnwind == "explicit" && config.Scheduler() == "asyncify" { + return nil, errors.New("explicit panic unwinding cannot be used with the asyncify scheduler") + } + if config.PanicUnwind() == "explicit" && !config.SupportsExplicitUnwind() { + return nil, fmt.Errorf("explicit panic unwinding is not supported on %s", config.Triple()) + } + if config.PanicUnwind() == "explicit" && config.Scheduler() == "threads" { + return nil, errors.New("explicit panic unwinding is not supported with the threads scheduler") + } + return config, nil } diff --git a/builder/sizes_test.go b/builder/sizes_test.go index 639c32b5c2..40e5b5f4e7 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", 4313, 323, 0, 2260}, - {"microbit", "examples/serial", 2838, 382, 8, 2256}, - {"wioterminal", "examples/pininterrupt", 8027, 1665, 132, 7488}, + {"hifive1b", "examples/echo", 4301, 323, 0, 2260}, + {"microbit", "examples/serial", 2834, 382, 8, 2256}, + {"wioterminal", "examples/pininterrupt", 7991, 1665, 132, 7488}, // TODO: also check wasm. Right now this is difficult, because // wasm binaries are run through wasm-opt and therefore the diff --git a/compileopts/config.go b/compileopts/config.go index 7786cd2178..f20672c476 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -113,6 +113,7 @@ func (c *Config) BuildTags() []string { "osusergo", // to get os/user to work "math_big_pure_go", // to get math/big to work "gc." + c.GC(), "scheduler." + c.Scheduler(), // used inside the runtime package + "tinygo.unwind." + c.PanicUnwind(), "serial." + c.Serial()}...) // used inside the machine package switch c.Scheduler() { case "threads", "cores": @@ -202,6 +203,42 @@ func (c *Config) PanicStrategy() string { return c.Options.PanicStrategy } +// PanicUnwind returns the mechanism used to unwind panics and Goexit. Asyncify +// provides unwinding whenever that scheduler is selected. Explicit +// return-based unwinding must be requested by the command line or target +// specification. +func (c *Config) PanicUnwind() string { + if c.Scheduler() == "asyncify" { + return "asyncify" + } + requested := c.Target.PanicUnwind + if c.Options.PanicUnwind != "" { + requested = c.Options.PanicUnwind + } + if requested == "explicit" { + return "explicit" + } + arch, _, _ := strings.Cut(c.Triple(), "-") + switch arch { + case "wasm32", "xtensa": + return "none" + default: + return "setjmp" + } +} + +// SupportsExplicitUnwind reports whether the target can use return-based +// panic unwinding. +func (c *Config) SupportsExplicitUnwind() bool { + arch, _, _ := strings.Cut(c.Triple(), "-") + switch arch { + case "wasm32", "riscv64", "xtensa": + return true + default: + return false + } +} + // AutomaticStackSize returns whether goroutine stack sizes should be determined // automatically at compile time, if possible. If it is false, no attempt is // made. diff --git a/compileopts/options.go b/compileopts/options.go index cb5f24d5c0..9b06611c55 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -15,6 +15,7 @@ var ( validSerialOptions = []string{"none", "uart", "usb", "rtt"} validPrintSizeOptions = []string{"none", "short", "full", "html"} validPanicStrategyOptions = []string{"print", "trap"} + validPanicUnwindOptions = []string{"auto", "explicit"} validOptOptions = []string{"none", "0", "1", "2", "s", "z"} ) @@ -32,6 +33,7 @@ type Options struct { Opt string GC string PanicStrategy string + PanicUnwind string Scheduler string StackSize uint64 // goroutine stack size (if none could be automatically determined) Serial string @@ -120,6 +122,15 @@ func (o *Options) Verify() error { } } + if o.PanicUnwind != "" { + valid := slices.Contains(validPanicUnwindOptions, o.PanicUnwind) + if !valid { + return fmt.Errorf(`invalid panic-unwind option '%s': valid values are %s`, + o.PanicUnwind, + strings.Join(validPanicUnwindOptions, ", ")) + } + } + if o.Opt != "" { if !slices.Contains(validOptOptions, o.Opt) { return fmt.Errorf("invalid -opt=%s: valid values are %s", o.Opt, strings.Join(validOptOptions, ", ")) diff --git a/compileopts/options_test.go b/compileopts/options_test.go index dd098e6c4a..094da03593 100644 --- a/compileopts/options_test.go +++ b/compileopts/options_test.go @@ -13,6 +13,7 @@ func TestVerifyOptions(t *testing.T) { expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify, threads, cores`) expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full, html`) expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`) + expectedPanicUnwindError := errors.New(`invalid panic-unwind option 'incorrect': valid values are auto, explicit`) testCases := []struct { name string @@ -117,6 +118,25 @@ func TestVerifyOptions(t *testing.T) { PanicStrategy: "trap", }, }, + { + name: "InvalidPanicUnwindOption", + opts: compileopts.Options{ + PanicUnwind: "incorrect", + }, + expectedError: expectedPanicUnwindError, + }, + { + name: "PanicUnwindOptionAuto", + opts: compileopts.Options{ + PanicUnwind: "auto", + }, + }, + { + name: "PanicUnwindOptionExplicit", + opts: compileopts.Options{ + PanicUnwind: "explicit", + }, + }, } for _, tc := range testCases { diff --git a/compileopts/target.go b/compileopts/target.go index 92b011b830..f1e9504f87 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -35,6 +35,7 @@ type TargetSpec struct { BuildTags []string `json:"build-tags,omitempty"` BuildMode string `json:"buildmode,omitempty"` // default build mode (if nothing specified) GC string `json:"gc,omitempty"` + PanicUnwind string `json:"panic-unwind,omitempty"` Scheduler string `json:"scheduler,omitempty"` Serial string `json:"serial,omitempty"` // which serial output to use (uart, usb, none) Linker string `json:"linker,omitempty"` @@ -224,6 +225,11 @@ func LoadTarget(options *Options) (*TargetSpec, error) { if err != nil { return nil, fmt.Errorf("%s : %w", options.Target, err) } + switch spec.PanicUnwind { + case "", "auto", "explicit": + default: + return nil, fmt.Errorf("%s: invalid panic-unwind option %q", options.Target, spec.PanicUnwind) + } if spec.Scheduler == "asyncify" { spec.ExtraFiles = append(spec.ExtraFiles, "src/internal/task/task_asyncify_wasm.S") diff --git a/compileopts/target_test.go b/compileopts/target_test.go index 315ccf4e49..b6e2882f0c 100644 --- a/compileopts/target_test.go +++ b/compileopts/target_test.go @@ -161,3 +161,49 @@ func TestConfigLinkerFlavor(t *testing.T) { }) } } + +func TestConfigPanicUnwind(t *testing.T) { + tests := []struct { + name string + options Options + target TargetSpec + want string + }{ + { + name: "native defaults to setjmp", + target: TargetSpec{Triple: "x86_64-unknown-linux"}, + want: "setjmp", + }, + { + name: "riscv64 defaults to setjmp", + target: TargetSpec{Triple: "riscv64-unknown-unknown"}, + want: "setjmp", + }, + { + name: "explicit command line opt in", + options: Options{PanicUnwind: "explicit"}, + target: TargetSpec{Triple: "riscv64-unknown-unknown"}, + want: "explicit", + }, + { + name: "auto overrides target opt in", + options: Options{PanicUnwind: "auto"}, + target: TargetSpec{Triple: "riscv64-unknown-unknown", PanicUnwind: "explicit"}, + want: "setjmp", + }, + { + name: "asyncify enables unwinding", + options: Options{Scheduler: "asyncify"}, + target: TargetSpec{Triple: "wasm32-unknown-unknown", PanicUnwind: "auto"}, + want: "asyncify", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + config := Config{Options: &tc.options, Target: &tc.target} + if got := config.PanicUnwind(); got != tc.want { + t.Fatalf("got %q, want %q", got, tc.want) + } + }) + } +} diff --git a/compiler/asserts.go b/compiler/asserts.go index 7e3a8b1504..4cacd13aff 100644 --- a/compiler/asserts.go +++ b/compiler/asserts.go @@ -263,11 +263,13 @@ func (b *builder) getRuntimeAssertBlock(blockPrefix, assertFunc string) llvm.Bas block := b.ctx.AddBasicBlock(b.llvmFn, blockPrefix+".throw") b.runtimeAssertBlocks[assertFunc] = block b.SetInsertPointAtEnd(block) + b.inFaultBlock = true if b.hasDeferFrame() { b.createFaultCheckpoint() } - b.createRuntimeCall(assertFunc, nil, "") - b.CreateUnreachable() + b.createRuntimeInvoke(assertFunc, nil, "") + b.createUnwindReturnOrUnreachable() + b.inFaultBlock = false b.SetInsertPointAtEnd(savedBlock) return block } diff --git a/compiler/calls.go b/compiler/calls.go index 257973320e..cd2f2646a9 100644 --- a/compiler/calls.go +++ b/compiler/calls.go @@ -1,6 +1,7 @@ package compiler import ( + "go/token" "go/types" "strconv" @@ -43,20 +44,27 @@ const ( // createRuntimeCallCommon creates a runtime call. Use createRuntimeCall or // createRuntimeInvoke instead. func (b *builder) createRuntimeCallCommon(fnName string, args []llvm.Value, name string, isInvoke bool) llvm.Value { - member := b.program.ImportedPackage("runtime").Members[fnName] + fnType, llvmFn := b.getRuntimeFunction(fnName) + args = append(args, llvm.Undef(b.dataPtrType)) // unused context parameter + if isInvoke { + // chanSend is the only panic-capable runtime operation that can also + // suspend the task. + return b.createInvokeWithSuspend(fnType, llvmFn, args, name, fnName == "chanSend") + } + return b.createCall(fnType, llvmFn, args, name) +} + +func (b *builder) getRuntimeFunction(name string) (llvm.Type, llvm.Value) { + member := b.program.ImportedPackage("runtime").Members[name] if member == nil { - panic("unknown runtime call: " + fnName) + panic("unknown runtime call: " + name) } fn := member.(*ssa.Function) fnType, llvmFn := b.getFunction(fn) if llvmFn.IsNil() { panic("trying to call non-existent function: " + fn.RelString(nil)) } - args = append(args, llvm.Undef(b.dataPtrType)) // unused context parameter - if isInvoke { - return b.createInvoke(fnType, llvmFn, args, name) - } - return b.createCall(fnType, llvmFn, args, name) + return fnType, llvmFn } // createRuntimeCall creates a new call to runtime. with the given @@ -77,11 +85,7 @@ func (b *builder) createRuntimeInvoke(fnName string, args []llvm.Value, name str // createCall creates a call to the given function with the arguments possibly // expanded. func (b *builder) createCall(fnType llvm.Type, fn llvm.Value, args []llvm.Value, name string) llvm.Value { - expanded := make([]llvm.Value, 0, len(args)) - for _, arg := range args { - fragments := b.expandFormalParam(arg) - expanded = append(expanded, fragments...) - } + expanded := b.expandFormalParams(args) call := b.CreateCall(fnType, fn, expanded, name) if !fn.IsAFunction().IsNil() { if cc := fn.FunctionCallConv(); cc != llvm.CCallConv { @@ -93,15 +97,364 @@ func (b *builder) createCall(fnType llvm.Type, fn llvm.Value, args []llvm.Value, return call } +func (b *builder) expandFormalParams(args []llvm.Value) []llvm.Value { + expanded := make([]llvm.Value, 0, len(args)) + for _, arg := range args { + fragments := b.expandFormalParam(arg) + expanded = append(expanded, fragments...) + } + return expanded +} + // createInvoke is like createCall but continues execution at the landing pad if // the call resulted in a panic. func (b *builder) createInvoke(fnType llvm.Type, fn llvm.Value, args []llvm.Value, name string) llvm.Value { + return b.createInvokeWithSuspend(fnType, fn, args, name, false) +} + +func (b *builder) createInvokeWithSuspend(fnType llvm.Type, fn llvm.Value, args []llvm.Value, name string, maySuspend bool) llvm.Value { + if b.usesReturnUnwind() { + var call llvm.Value + if b.usesAsyncifyUnwind() && b.hasDeferFrame() { + if maySuspend { + call = b.createAsyncifySuspendInvoke(fnType, fn, args, name) + } else { + call = b.createAsyncifyNoSuspendInvoke(fnType, fn, args, name) + } + } else { + call = b.createCall(fnType, fn, args, name) + } + if b.needsPostCallUnwindCheck() { + b.createUnwindCheck(true) + } + + return call + } if b.hasDeferFrame() { b.createInvokeCheckpoint() } return b.createCall(fnType, fn, args, name) } +func (b *builder) createAsyncifyNoSuspendInvoke(fnType llvm.Type, fn llvm.Value, args []llvm.Value, name string) llvm.Value { + expanded := b.expandFormalParams(args) + paramTypes := append([]llvm.Type(nil), fnType.ParamTypes()...) + direct := !fn.IsAFunction().IsNil() + if !direct { + paramTypes = append([]llvm.Type{fn.Type()}, paramTypes...) + } + wrapperType := llvm.FunctionType(fnType.ReturnType(), paramTypes, false) + wrapperName := b.llvmFn.Name() + ".asyncifycatch." + strconv.Itoa(b.asyncifyCatchIndex) + b.asyncifyCatchIndex++ + wrapper := llvm.AddFunction(b.mod, wrapperName, wrapperType) + wrapper.SetLinkage(llvm.InternalLinkage) + wrapper.AddFunctionAttr(b.ctx.CreateEnumAttribute(llvm.AttributeKindID("noinline"), 0)) + + builder := b.ctx.NewBuilder() + entry := b.ctx.AddBasicBlock(wrapper, "entry") + builder.SetInsertPointAtEnd(entry) + target := fn + paramOffset := 0 + if !direct { + target = wrapper.Param(0) + paramOffset = 1 + } + callArgs := make([]llvm.Value, len(fnType.ParamTypes())) + for i := range callArgs { + callArgs[i] = wrapper.Param(i + paramOffset) + } + result := builder.CreateCall(fnType, target, callArgs, "") + unwindType, unwindLLVMFn := b.getRuntimeFunction("unwindPending") + unwinding := builder.CreateCall(unwindType, unwindLLVMFn, []llvm.Value{llvm.Undef(b.dataPtrType)}, "") + b.emitAsyncifyCatchReturn(builder, fnType, result, entry, unwinding) + builder.Dispose() + + wrapperArgs := expanded + if !direct { + wrapperArgs = append([]llvm.Value{fn}, expanded...) + } + return b.CreateCall(wrapperType, wrapper, wrapperArgs, name) +} + +func (b *builder) createAsyncifySuspendInvoke(fnType llvm.Type, fn llvm.Value, args []llvm.Value, name string) llvm.Value { + // Binaryen does not instrument a function containing stop_unwind, so a + // suspend-capable call needs two wrappers: + // + // defer caller (instrumented) + // | + // | volatile indirect call + // v + // panic catcher (not instrumented; contains stop_unwind) + // | + // v + // target trampoline (instrumented) + // | + // | indirect call + // v + // real target + // + // A scheduler unwind crosses both wrappers. A panic unwind stops in the + // catcher, then the caller branches to its defer landing pad. + expanded := b.expandFormalParams(args) + paramTypes := append([]llvm.Type{fn.Type()}, fnType.ParamTypes()...) + wrapperType := llvm.FunctionType(fnType.ReturnType(), paramTypes, false) + wrapperArgs := append([]llvm.Value{fn}, expanded...) + if catchGlobal, ok := b.asyncifyCatchers[wrapperType]; ok { + catchPointer := b.CreateLoad(catchGlobal.GlobalValueType(), catchGlobal, "") + catchPointer.SetVolatile(true) + return b.CreateCall(wrapperType, catchPointer, wrapperArgs, name) + } + wrapperName := b.llvmFn.Name() + ".asyncifysuspendcatch." + strconv.Itoa(b.asyncifyCatchIndex) + b.asyncifyCatchIndex++ + + targetType := wrapperType + var resultGlobal llvm.Value + if fnType.ReturnType().TypeKind() != llvm.VoidTypeKind { + targetType = llvm.FunctionType(b.ctx.VoidType(), paramTypes, false) + resultGlobal = llvm.AddGlobal(b.mod, b.dataPtrType, wrapperName+".result.ptr") + resultGlobal.SetLinkage(llvm.InternalLinkage) + resultGlobal.SetInitializer(llvm.ConstNull(b.dataPtrType)) + } + targetWrapper := llvm.AddFunction(b.mod, wrapperName+".target", targetType) + targetWrapper.SetLinkage(llvm.InternalLinkage) + targetWrapper.AddFunctionAttr(b.ctx.CreateEnumAttribute(llvm.AttributeKindID("noinline"), 0)) + builder := b.ctx.NewBuilder() + entry := b.ctx.AddBasicBlock(targetWrapper, "entry") + builder.SetInsertPointAtEnd(entry) + target := targetWrapper.Param(0) + callArgs := make([]llvm.Value, len(fnType.ParamTypes())) + for i := range callArgs { + callArgs[i] = targetWrapper.Param(i + 1) + } + result := builder.CreateCall(fnType, target, callArgs, "") + if fnType.ReturnType().TypeKind() == llvm.VoidTypeKind { + builder.CreateRetVoid() + } else { + resultPointer := builder.CreateLoad(b.dataPtrType, resultGlobal, "") + resultPointer.SetVolatile(true) + builder.CreateStore(result, resultPointer) + builder.CreateRetVoid() + } + builder.Dispose() + + catchWrapper := llvm.AddFunction(b.mod, wrapperName+".paniccatch", wrapperType) + catchWrapper.SetLinkage(llvm.InternalLinkage) + catchWrapper.AddFunctionAttr(b.ctx.CreateEnumAttribute(llvm.AttributeKindID("noinline"), 0)) + builder = b.ctx.NewBuilder() + entry = b.ctx.AddBasicBlock(catchWrapper, "entry") + builder.SetInsertPointAtEnd(entry) + catchArgs := make([]llvm.Value, len(paramTypes)) + for i := range catchArgs { + catchArgs[i] = catchWrapper.Param(i) + } + if fnType.ReturnType().TypeKind() == llvm.VoidTypeKind { + result = builder.CreateCall(targetType, targetWrapper, catchArgs, "") + } else { + // Asyncify can restore a stale hidden result pointer during rewind. Use + // a volatile slot to select the current catcher's storage, restoring its + // previous value before the unwind can reach the scheduler. This also + // makes recursive use safe. + resultStorage := builder.CreateAlloca(fnType.ReturnType(), "") + previousResultPointer := builder.CreateLoad(b.dataPtrType, resultGlobal, "") + previousResultPointer.SetVolatile(true) + storeResultPointer := builder.CreateStore(resultStorage, resultGlobal) + storeResultPointer.SetVolatile(true) + builder.CreateCall(targetType, targetWrapper, catchArgs, "") + restoreResultPointer := builder.CreateStore(previousResultPointer, resultGlobal) + restoreResultPointer.SetVolatile(true) + result = builder.CreateLoad(fnType.ReturnType(), resultStorage, "") + } + unwindType, unwindLLVMFn := b.getRuntimeFunction("unwindPending") + // This call must stay opaque to AddUnwindAssumptions. This catcher observes + // the signal set by its target, so assuming a clear signal on entry would + // let LLVM remove the check. + unwindGlobal := llvm.AddGlobal(b.mod, unwindLLVMFn.Type(), wrapperName+".unwind.ptr") + unwindGlobal.SetLinkage(llvm.InternalLinkage) + unwindGlobal.SetInitializer(unwindLLVMFn) + unwindPointer := builder.CreateLoad(unwindLLVMFn.Type(), unwindGlobal, "") + unwindPointer.SetVolatile(true) + unwinding := builder.CreateCall(unwindType, unwindPointer, []llvm.Value{llvm.Undef(b.dataPtrType)}, "") + b.emitAsyncifyCatchReturn(builder, fnType, result, entry, unwinding) + builder.Dispose() + + // Keep the caller-to-catcher edge opaque so Binaryen instruments the + // indirect call instead of treating it as a call into bottommost runtime. + catchGlobal := llvm.AddGlobal(b.mod, catchWrapper.Type(), wrapperName+".paniccatch.ptr") + catchGlobal.SetLinkage(llvm.InternalLinkage) + catchGlobal.SetInitializer(catchWrapper) + b.asyncifyCatchers[wrapperType] = catchGlobal + catchPointer := b.CreateLoad(catchWrapper.Type(), catchGlobal, "") + catchPointer.SetVolatile(true) + return b.CreateCall(wrapperType, catchPointer, wrapperArgs, name) +} + +func (b *builder) emitAsyncifyCatchReturn(builder llvm.Builder, fnType llvm.Type, result llvm.Value, entry llvm.BasicBlock, unwinding llvm.Value) { + wrapper := entry.Parent() + stopBlock := b.ctx.AddBasicBlock(wrapper, "unwind.stop") + returnBlock := b.ctx.AddBasicBlock(wrapper, "return") + builder.CreateCondBr(unwinding, stopBlock, returnBlock) + + builder.SetInsertPointAtEnd(stopBlock) + stopType, stopFn := b.getRuntimeFunction("asyncifyStopUnwindImport") + builder.CreateCall(stopType, stopFn, nil, "") + builder.CreateBr(returnBlock) + + builder.SetInsertPointAtEnd(returnBlock) + if fnType.ReturnType().TypeKind() == llvm.VoidTypeKind { + builder.CreateRetVoid() + return + } + phi := builder.CreatePHI(fnType.ReturnType(), "") + phi.AddIncoming([]llvm.Value{result, result}, []llvm.BasicBlock{entry, stopBlock}) + builder.CreateRet(phi) +} + +type suspendState uint8 + +const ( + suspendUnknown suspendState = iota + suspendVisiting + suspendNo + suspendYes +) + +func (b *builder) functionMaySuspend(fn *ssa.Function) bool { + // Calls through unknown targets are conservatively treated as suspending. + // Cycles are also considered suspending so recursive call graphs cannot + // hide a path to task.Pause. + switch b.maySuspend[fn] { + case suspendVisiting: + return true + case suspendNo: + return false + case suspendYes: + return true + } + + b.maySuspend[fn] = suspendVisiting + if fn.Pkg != nil && fn.Pkg.Pkg.Path() == "internal/task" && fn.Name() == "Pause" { + b.maySuspend[fn] = suspendYes + return true + } + if len(fn.Blocks) == 0 { + b.maySuspend[fn] = suspendYes + return true + } + for _, block := range fn.Blocks { + if block == nil { + continue + } + for _, instruction := range block.Instrs { + switch instruction := instruction.(type) { + case *ssa.Send, *ssa.Select, *ssa.Next: + b.maySuspend[fn] = suspendYes + return true + case *ssa.UnOp: + if instruction.Op == token.ARROW { + b.maySuspend[fn] = suspendYes + return true + } + } + call, ok := instruction.(ssa.CallInstruction) + if !ok { + continue + } + common := call.Common() + if _, ok := common.Value.(*ssa.Builtin); ok { + continue + } + callee := common.StaticCallee() + if callee == nil || b.functionMaySuspend(callee) { + b.maySuspend[fn] = suspendYes + return true + } + } + } + b.maySuspend[fn] = suspendNo + return false +} + +func (b *builder) callMaySuspend(call *ssa.CallCommon) bool { + callee := call.StaticCallee() + return callee == nil || b.functionMaySuspend(callee) +} + +func (b *builder) inFunctionBody() bool { + block := b.GetInsertBlock() + return b.loweringBody && !b.llvmFn.IsNil() && !block.IsNil() && block.Parent() == b.llvmFn +} + +func (b *builder) needsPostCallUnwindCheck() bool { + if !b.inFunctionBody() || b.runningDefers || b.isUnwindRuntime() { + return false + } + // Explicit unwinding propagates through every caller. Asyncify already + // unwinds ordinary callers itself; only a defer frame stops it and needs a + // check that branches to the landing pad. + return !b.usesAsyncifyUnwind() || b.hasDeferFrame() +} + +func (b *builder) isUnwindRuntime() bool { + if !b.usesReturnUnwind() { + return false + } + if b.fn.Pkg == nil { + return false + } + if b.fn.Pkg.Pkg.Path() != "runtime" { + return false + } + switch b.fn.Name() { + case "startUnwind", "currentDeferFrame", "unwindPending", "clearUnwind", + "getUnwindSignal", "setUnwindSignal": + return true + default: + return false + } +} + +// When catch is true, route the unwind to this function's defer landing pad. +func (b *builder) createUnwindCheck(catch bool) { + unwind := b.createRuntimeCall("unwindPending", nil, "unwind") + continueBB := b.insertBasicBlock("unwind.continue") + if catch && b.hasDeferFrame() { + b.CreateCondBr(unwind, b.landingpad, continueBB) + } else { + b.CreateCondBr(unwind, b.unwindReturnBlock(), continueBB) + } + + b.SetInsertPointAtEnd(continueBB) + if !b.inFaultBlock { + b.currentBlockInfo.exit = continueBB + } +} + +func (b *builder) unwindReturnBlock() llvm.BasicBlock { + if !b.unwindReturn.IsNil() { + return b.unwindReturn + } + + savedBlock := b.GetInsertBlock() + b.unwindReturn = b.ctx.AddBasicBlock(b.llvmFn, "unwind.return") + b.SetInsertPointAtEnd(b.unwindReturn) + returnType := b.llvmFn.GlobalValueType().ReturnType() + if returnType.TypeKind() == llvm.VoidTypeKind { + b.CreateRetVoid() + } else { + b.CreateRet(llvm.Undef(returnType)) + } + b.SetInsertPointAtEnd(savedBlock) + return b.unwindReturn +} + +func (b *builder) createUnwindReturnOrUnreachable() { + if b.usesAsyncifyUnwind() { + b.CreateBr(b.unwindReturnBlock()) + } else { + b.CreateUnreachable() + } +} + // Expand an argument type to a list that can be used in a function call // parameter list. func (c *compilerContext) expandFormalParamType(t llvm.Type, name string, goType types.Type) []paramInfo { diff --git a/compiler/compiler.go b/compiler/compiler.go index 19410d49ea..ba251acf44 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -60,6 +60,7 @@ type Config struct { Debug bool // Whether to emit debug information in the LLVM module. Nobounds bool // Whether to skip bounds checks PanicStrategy string + PanicUnwind string } // compilerContext contains function-independent data that should still be @@ -87,6 +88,8 @@ type compilerContext struct { program *ssa.Program diagnostics []error functionInfos map[*ssa.Function]functionInfo + maySuspend map[*ssa.Function]suspendState + asyncifyCatchers map[llvm.Type]llvm.Value astComments map[string]*ast.CommentGroup embedGlobals map[string][]*loader.EmbedFile pkg *types.Package @@ -100,14 +103,16 @@ type compilerContext struct { // importantly with a newly created LLVM context and module. func newCompilerContext(moduleName string, machine llvm.TargetMachine, config *Config, dumpSSA bool) *compilerContext { c := &compilerContext{ - Config: config, - DumpSSA: dumpSSA, - difiles: make(map[string]llvm.Metadata), - ditypes: make(map[types.Type]llvm.Metadata), - machine: machine, - targetData: machine.CreateTargetData(), - functionInfos: map[*ssa.Function]functionInfo{}, - astComments: map[string]*ast.CommentGroup{}, + Config: config, + DumpSSA: dumpSSA, + difiles: make(map[string]llvm.Metadata), + ditypes: make(map[types.Type]llvm.Metadata), + machine: machine, + targetData: machine.CreateTargetData(), + functionInfos: map[*ssa.Function]functionInfo{}, + maySuspend: map[*ssa.Function]suspendState{}, + asyncifyCatchers: map[llvm.Type]llvm.Value{}, + astComments: map[string]*ast.CommentGroup{}, } c.ctx = llvm.NewContext() @@ -150,36 +155,41 @@ func (c *compilerContext) dispose() { type builder struct { *compilerContext llvm.Builder - fn *ssa.Function - llvmFnType llvm.Type - llvmFn llvm.Value - info functionInfo - locals map[ssa.Value]llvm.Value // local variables - indirectValues map[ssa.Value]llvm.Value - indirectReturn llvm.Value - blockInfo []blockInfo - currentBlock *ssa.BasicBlock - currentBlockInfo *blockInfo - tarjanStack []uint - tarjanIndex uint - phis []phiNode - deferPtr llvm.Value - deferFrame llvm.Value - stackChainAlloca llvm.Value - landingpad llvm.BasicBlock - difunc llvm.Metadata - dilocals map[*types.Var]llvm.Metadata - initInlinedAt llvm.Metadata // fake inlinedAt position - initPseudoFuncs map[string]llvm.Metadata // fake "inlined" functions for proper init debug locations - allDeferFuncs []any - deferFuncs map[*ssa.Function]int - deferInvokeFuncs map[string]int - deferClosureFuncs map[*ssa.Function]int - deferExprFuncs map[ssa.Value]int - selectRecvBuf map[*ssa.Select]llvm.Value - deferBuiltinFuncs map[ssa.Value]deferBuiltin - runDefersBlock []llvm.BasicBlock - afterDefersBlock []llvm.BasicBlock + fn *ssa.Function + llvmFnType llvm.Type + llvmFn llvm.Value + info functionInfo + locals map[ssa.Value]llvm.Value // local variables + indirectValues map[ssa.Value]llvm.Value + indirectReturn llvm.Value + blockInfo []blockInfo + currentBlock *ssa.BasicBlock + currentBlockInfo *blockInfo + tarjanStack []uint + tarjanIndex uint + phis []phiNode + deferPtr llvm.Value + deferFrame llvm.Value + stackChainAlloca llvm.Value + landingpad llvm.BasicBlock + unwindReturn llvm.BasicBlock + asyncifyCatchIndex int + difunc llvm.Metadata + dilocals map[*types.Var]llvm.Metadata + initInlinedAt llvm.Metadata // fake inlinedAt position + initPseudoFuncs map[string]llvm.Metadata // fake "inlined" functions for proper init debug locations + allDeferFuncs []any + deferFuncs map[*ssa.Function]int + deferInvokeFuncs map[string]int + deferClosureFuncs map[*ssa.Function]int + deferExprFuncs map[ssa.Value]int + selectRecvBuf map[*ssa.Select]llvm.Value + deferBuiltinFuncs map[ssa.Value]deferBuiltin + runningDefers bool + loweringBody bool + inFaultBlock bool + runDefersBlock []llvm.BasicBlock + afterDefersBlock []llvm.BasicBlock runtimeAssertBlocks map[string]llvm.BasicBlock interfaceAssertBlock llvm.BasicBlock @@ -1379,6 +1389,7 @@ func (b *builder) createFunction() { b.createFunctionStart(false) // Fill blocks with instructions. + b.loweringBody = true for _, block := range b.fn.DomPreorder() { if b.DumpSSA { fmt.Printf("%d: %s:\n", block.Index, block.Comment) @@ -1424,6 +1435,7 @@ func (b *builder) createFunction() { b.CreateRetVoid() } } + b.loweringBody = false // The rundefers instruction needs to be created after all defer // instructions have been created. Otherwise it won't handle all defer @@ -1572,10 +1584,13 @@ func (b *builder) createInstruction(instr ssa.Instruction) { case *ssa.Panic: value := b.getValue(instr.X, getPos(instr)) b.createRuntimeInvoke("_panic", []llvm.Value{value}, "") - b.CreateUnreachable() + b.createUnwindReturnOrUnreachable() case *ssa.Return: if b.hasDeferFrame() { b.createRuntimeCall("destroyDeferFrame", []llvm.Value{b.deferFrame}, "") + if b.usesReturnUnwind() { + b.createUnwindCheck(false) + } } b.createReturn(instr.Results, getPos(instr)) case *ssa.RunDefers: @@ -2333,11 +2348,12 @@ func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error) } if !exported { + maySuspend := b.callMaySuspend(instr) if resultType, indirectResult := b.hasIndirectResult(instr.Signature()); indirectResult { result := b.createIndirectStorage(resultType, "call.result") params = append([]llvm.Value{result}, params...) params = append(params, context) - b.createInvoke(calleeType, callee, params, "") + b.createInvokeWithSuspend(calleeType, callee, params, "", maySuspend) return result, nil } // This function takes a context parameter. @@ -2345,7 +2361,7 @@ func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error) params = append(params, context) } - return b.createInvoke(calleeType, callee, params, ""), nil + return b.createInvokeWithSuspend(calleeType, callee, params, "", b.callMaySuspend(instr)), nil } // getValue returns the LLVM value of a constant, function value, global, or @@ -3231,7 +3247,7 @@ func (b *builder) createBinOp(op token.Token, typ, ytyp types.Type, x, y llvm.Va result = b.CreateICmp(llvm.IntEQ, typecodeX, typecodeY, "") } else { // Fall back to a full interface comparison. - result = b.createRuntimeCall("interfaceEqual", []llvm.Value{x, y}, "") + result = b.createRuntimeInvoke("interfaceEqual", []llvm.Value{x, y}, "") } if op == token.NEQ { result = b.CreateNot(result, "") diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 462a9bb8df..7feebbc857 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -392,6 +392,7 @@ func testCompilePackage(t *testing.T, options *compileopts.Options, file string) AutomaticStackSize: config.AutomaticStackSize(), DefaultStackSize: config.StackSize(), NeedsStackObjects: config.NeedsStackObjects(), + PanicUnwind: config.PanicUnwind(), } machine, err := NewTargetMachine(compilerConfig) if err != nil { diff --git a/compiler/defer.go b/compiler/defer.go index f8078f6b52..28acf399d3 100644 --- a/compiler/defer.go +++ b/compiler/defer.go @@ -23,30 +23,27 @@ import ( "tinygo.org/x/go-llvm" ) -// supportsRecover returns whether the compiler supports the recover() builtin -// for the current architecture. +// supportsRecover reports whether the selected unwind mode supports recover. func (b *builder) supportsRecover() bool { - switch b.archFamily() { - case "wasm32": - // Probably needs to be implemented using the exception handling - // proposal of WebAssembly: - // https://github.com/WebAssembly/exception-handling - return false - case "xtensa": - // TODO: add support for these architectures - return false - default: - return true - } + return b.PanicUnwind != "none" +} + +func (b *builder) usesExplicitUnwind() bool { + return b.PanicUnwind == "explicit" +} + +func (b *builder) usesAsyncifyUnwind() bool { + return b.PanicUnwind == "asyncify" +} + +func (b *builder) usesReturnUnwind() bool { + return b.usesExplicitUnwind() || b.usesAsyncifyUnwind() } // hasDeferFrame returns whether the current function needs to catch panics and // run defers. func (b *builder) hasDeferFrame() bool { - if b.fn.Recover == nil { - return false - } - return b.supportsRecover() + return b.fn.Recover != nil && b.supportsRecover() } // deferInitFunc sets up this function for future deferred calls. It must be @@ -94,6 +91,9 @@ func (b *builder) deferInitFunc() { // destroyDeferFrame. func (b *builder) createLandingPad() { b.SetInsertPointAtEnd(b.landingpad) + if b.usesReturnUnwind() { + b.createRuntimeCall("clearUnwind", nil, "") + } // Add debug info, if needed. // The location used is the closing bracket of the function. @@ -233,7 +233,6 @@ li a0, 0 } constraints = "={a0},{a1},~{a1},~{a2},~{a3},~{a4},~{a5},~{a6},~{a7},~{s0},~{s1},~{s2},~{s3},~{s4},~{s5},~{s6},~{s7},~{s8},~{s9},~{s10},~{s11},~{t0},~{t1},~{t2},~{t3},~{t4},~{t5},~{t6},~{ra},~{f0},~{f1},~{f2},~{f3},~{f4},~{f5},~{f6},~{f7},~{f8},~{f9},~{f10},~{f11},~{f12},~{f13},~{f14},~{f15},~{f16},~{f17},~{f18},~{f19},~{f20},~{f21},~{f22},~{f23},~{f24},~{f25},~{f26},~{f27},~{f28},~{f29},~{f30},~{f31},~{memory}" default: - // This case should have been handled by b.supportsRecover(). b.addError(b.fn.Pos(), "unknown architecture for defer: "+b.archFamily()) } asmType := llvm.FunctionType(resultType, []llvm.Type{b.dataPtrType}, false) @@ -260,6 +259,9 @@ func (b *builder) createInvokeCheckpoint() { // not update currentBlockInfo.exit because the fault block is a dead-end that // does not participate in phi node resolution. func (b *builder) createFaultCheckpoint() { + if b.usesReturnUnwind() { + return + } isZero := b.createCheckpoint(b.deferFrame) continueBB := b.insertBasicBlock("") b.CreateCondBr(isZero, continueBB, b.landingpad) @@ -553,6 +555,7 @@ func (b *builder) createDefer(instr *ssa.Defer) { // createRunDefers emits code to run all deferred functions. func (b *builder) createRunDefers() { deferType := b.getLLVMRuntimeType("_defer") + b.runningDefers = true // Add a loop like the following: // for stack != nil { @@ -658,7 +661,7 @@ func (b *builder) createRunDefers() { } forwardParams = b.prependIndirectResult(callback.Signature(), false, forwardParams, "defer.result") - b.createCall(fnType, fnPtr, forwardParams, "") + b.createInvokeWithSuspend(fnType, fnPtr, forwardParams, "", true) case *ssa.Function: // Direct call. @@ -683,7 +686,7 @@ func (b *builder) createRunDefers() { // Call real function. fnType, fn := b.getFunction(callback) - b.createInvoke(fnType, fn, forwardParams, "") + b.createInvokeWithSuspend(fnType, fn, forwardParams, "", b.functionMaySuspend(callback)) case *ssa.MakeClosure: // Get the real defer struct type and cast to it. @@ -699,7 +702,7 @@ func (b *builder) createRunDefers() { // Call deferred function. fnType, llvmFn := b.getFunction(fn) forwardParams = b.prependIndirectResult(fn.Signature, false, forwardParams, "defer.result") - b.createCall(fnType, llvmFn, forwardParams, "") + b.createInvokeWithSuspend(fnType, llvmFn, forwardParams, "", b.functionMaySuspend(fn)) case *ssa.Builtin: db := b.deferBuiltinFuncs[callback] @@ -725,6 +728,13 @@ func (b *builder) createRunDefers() { panic("unknown deferred function type") } + if b.usesReturnUnwind() { + // A panic in a deferred call has reached its target frame. Keep + // running the remaining defers; destroyDeferFrame will propagate + // the panic afterwards if it was not recovered. + b.createRuntimeCall("clearUnwind", nil, "") + } + // Branch back to the start of the loop. b.CreateBr(loophead) } @@ -738,4 +748,5 @@ func (b *builder) createRunDefers() { // End of loop. b.SetInsertPointAtEnd(end) + b.runningDefers = false } diff --git a/compiler/interface.go b/compiler/interface.go index 84f91cc448..ff60877bdd 100644 --- a/compiler/interface.go +++ b/compiler/interface.go @@ -1163,11 +1163,13 @@ func (b *builder) getInterfaceAssertBlock() llvm.BasicBlock { block := b.ctx.AddBasicBlock(b.llvmFn, "typeassert.throw") b.interfaceAssertBlock = block b.SetInsertPointAtEnd(block) + b.inFaultBlock = true if b.hasDeferFrame() { b.createFaultCheckpoint() } - b.createRuntimeCall("interfaceTypeAssert", []llvm.Value{llvm.ConstInt(b.ctx.Int1Type(), 0, false)}, "") - b.CreateUnreachable() + b.createRuntimeInvoke("interfaceTypeAssert", []llvm.Value{llvm.ConstInt(b.ctx.Int1Type(), 0, false)}, "") + b.createUnwindReturnOrUnreachable() + b.inFaultBlock = false b.SetInsertPointAtEnd(savedBlock) return block } diff --git a/compiler/map.go b/compiler/map.go index 3481ae90ac..9011b8592b 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -106,7 +106,11 @@ func (b *builder) createMapLookup(keyType, valueType types.Type, m llvm.Value, k if !hashmapIsBinaryKey(keyType) { fnName = "hashmapGenericGet" } - commaOkValue = b.createRuntimeCall(fnName, params, "") + if fnName == "hashmapGenericGet" { + commaOkValue = b.createRuntimeInvoke(fnName, params, "") + } else { + commaOkValue = b.createRuntimeCall(fnName, params, "") + } b.endValueStorage(mapKey) } @@ -155,7 +159,11 @@ func (b *builder) createMapDelete(keyType types.Type, m, key llvm.Value, pos tok fnName = "hashmapGenericDelete" } params := []llvm.Value{m, keyAlloca} - b.createRuntimeCall(fnName, params, "") + if fnName == "hashmapGenericDelete" { + b.createRuntimeInvoke(fnName, params, "") + } else { + b.createRuntimeCall(fnName, params, "") + } b.emitLifetimeEnd(keyAlloca, keySize) return nil } diff --git a/compiler/testdata/basic.ll b/compiler/testdata/basic.ll index 0eae6bbf29..d14e3860f6 100644 --- a/compiler/testdata/basic.ll +++ b/compiler/testdata/basic.ll @@ -48,7 +48,10 @@ divbyzero.next: ; preds = %entry divbyzero.throw: ; preds = %entry call void @runtime.divideByZeroPanic(ptr undef) #2 - unreachable + br label %unwind.return + +unwind.return: ; preds = %divbyzero.throw + ret i32 undef } declare void @runtime.divideByZeroPanic(ptr) #0 @@ -65,7 +68,10 @@ divbyzero.next: ; preds = %entry divbyzero.throw: ; preds = %entry call void @runtime.divideByZeroPanic(ptr undef) #2 - unreachable + br label %unwind.return + +unwind.return: ; preds = %divbyzero.throw + ret i32 undef } ; Function Attrs: nounwind @@ -84,7 +90,10 @@ divbyzero.next: ; preds = %entry divbyzero.throw: ; preds = %entry call void @runtime.divideByZeroPanic(ptr undef) #2 - unreachable + br label %unwind.return + +unwind.return: ; preds = %divbyzero.throw + ret i32 undef } ; Function Attrs: nounwind @@ -99,7 +108,10 @@ divbyzero.next: ; preds = %entry divbyzero.throw: ; preds = %entry call void @runtime.divideByZeroPanic(ptr undef) #2 - unreachable + br label %unwind.return + +unwind.return: ; preds = %divbyzero.throw + ret i32 undef } ; Function Attrs: nounwind diff --git a/compiler/testdata/func.ll b/compiler/testdata/func.ll index fd708789ed..5626e3155f 100644 --- a/compiler/testdata/func.ll +++ b/compiler/testdata/func.ll @@ -23,7 +23,10 @@ fpcall.next: ; preds = %entry fpcall.throw: ; preds = %entry call void @runtime.nilPanic(ptr undef) #2 - unreachable + br label %unwind.return + +unwind.return: ; preds = %fpcall.throw + ret void } declare void @runtime.nilPanic(ptr) #0 diff --git a/compiler/testdata/generics.ll b/compiler/testdata/generics.ll index 5b9f4ee987..1c6c716f4c 100644 --- a/compiler/testdata/generics.ll +++ b/compiler/testdata/generics.ll @@ -105,7 +105,10 @@ store.next4: ; preds = %store.next ret %"main.Point[float32]" %10 deref.throw: ; preds = %store.next, %deref.next3, %deref.next2, %deref.next1, %deref.next, %entry - unreachable + br label %unwind.return + +unwind.return: ; preds = %deref.throw + ret %"main.Point[float32]" undef } ; Function Attrs: allockind("alloc,zeroed") allocsize(0) @@ -168,7 +171,10 @@ store.next4: ; preds = %store.next ret %"main.Point[int]" %10 deref.throw: ; preds = %store.next, %deref.next3, %deref.next2, %deref.next1, %deref.next, %entry - unreachable + br label %unwind.return + +unwind.return: ; preds = %deref.throw + ret %"main.Point[int]" undef } declare void @main.checkBool(i1, ptr) #0 diff --git a/compiler/testdata/go1.20.ll b/compiler/testdata/go1.20.ll index 3ef92b634a..edf6cdb4b5 100644 --- a/compiler/testdata/go1.20.ll +++ b/compiler/testdata/go1.20.ll @@ -41,7 +41,10 @@ unsafe.String.next: ; preds = %entry unsafe.String.throw: ; preds = %entry call void @runtime.unsafeSlicePanic(ptr undef) #2 - unreachable + br label %unwind.return + +unwind.return: ; preds = %unsafe.String.throw + ret %runtime._string undef } declare void @runtime.unsafeSlicePanic(ptr) #0 diff --git a/compiler/testdata/large.ll b/compiler/testdata/large.ll index 27da846060..dfb9502e2e 100644 --- a/compiler/testdata/large.ll +++ b/compiler/testdata/large.ll @@ -4,6 +4,8 @@ target datalayout = "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-i128:128-n32:64-S128-n target triple = "wasm32-unknown-wasi" %runtime._string = type { ptr, i32 } +%runtime.deferFrame = type { ptr, ptr, [0 x ptr], ptr, i8, %runtime._interface, ptr } +%runtime._interface = type { ptr, ptr } %runtime.channelOp = type { ptr, ptr, i32, ptr } %runtime.chanSelectState = type { ptr, ptr } @@ -37,8 +39,8 @@ declare void @llvm.memcpy.p0.p0.i32(ptr noalias writeonly captures(none), ptr no define hidden i8 @"(main.largeReceiver).readLargeValue"(ptr readonly dereferenceable_or_null(1025) %receiver, ptr readonly dereferenceable_or_null(1025) %value, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %value1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %value1, ptr nonnull %stackalloc, ptr undef) #9 + %value1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %value1, ptr nonnull %stackalloc, ptr undef) #12 call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %value1, ptr noundef nonnull align 1 dereferenceable(1025) %value, i32 1025, i1 false) %0 = getelementptr inbounds nuw i8, ptr %value1, i32 1024 %1 = load i8, ptr %0, align 1 @@ -52,12 +54,12 @@ declare noalias nonnull ptr @runtime.alloc(i32, ptr, ptr) #3 define hidden void @main.makeLargeValue(ptr dereferenceable_or_null(1025) %return, i8 %value, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %result, ptr nonnull %stackalloc, ptr undef) #9 + %result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %result, ptr nonnull %stackalloc, ptr undef) #12 %0 = getelementptr inbounds nuw i8, ptr %result, i32 1024 store i8 %value, ptr %0, align 1 - %1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %1, ptr nonnull %stackalloc, ptr undef) #9 + %1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %1, ptr nonnull %stackalloc, ptr undef) #12 call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %1, ptr noundef nonnull align 1 dereferenceable(1025) %result, i32 1025, i1 false) call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %return, ptr noundef nonnull align 1 dereferenceable(1025) %1, i32 1025, i1 false) ret void @@ -74,8 +76,8 @@ entry: define hidden i8 @main.passZeroLargeValue(ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %"main.largeValue{}:main.largeValue" = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %"main.largeValue{}:main.largeValue", ptr nonnull %stackalloc, ptr undef) #9 + %"main.largeValue{}:main.largeValue" = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %"main.largeValue{}:main.largeValue", ptr nonnull %stackalloc, ptr undef) #12 store [1025 x i8] zeroinitializer, ptr %"main.largeValue{}:main.largeValue", align 1 %0 = call i8 @main.readLargeValue(ptr nonnull %"main.largeValue{}:main.largeValue", ptr undef) ret i8 %0 @@ -85,8 +87,8 @@ entry: define hidden i8 @main.readLargeValue(ptr readonly dereferenceable_or_null(1025) %value, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %value1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %value1, ptr nonnull %stackalloc, ptr undef) #9 + %value1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %value1, ptr nonnull %stackalloc, ptr undef) #12 call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %value1, ptr noundef nonnull align 1 dereferenceable(1025) %value, i32 1025, i1 false) %0 = getelementptr inbounds nuw i8, ptr %value1, i32 1024 %1 = load i8, ptr %0, align 1 @@ -97,8 +99,8 @@ entry: define hidden i8 @main.useLargeValue(ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #9 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #12 call void @main.makeLargeValue(ptr nonnull %call.result, i8 42, ptr undef) %0 = call i8 @main.readLargeValue(ptr nonnull %call.result, ptr undef) ret i8 %0 @@ -108,19 +110,22 @@ entry: define hidden i8 @main.useLargeFunctionValue(ptr %fn.context, ptr %fn.funcptr, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #9 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #12 call void @main.makeLargeValue(ptr nonnull %call.result, i8 42, ptr undef) %0 = icmp eq ptr %fn.funcptr, null br i1 %0, label %fpcall.throw, label %fpcall.next fpcall.next: ; preds = %entry - %1 = call i8 %fn.funcptr(ptr nonnull %call.result, ptr %fn.context) #9 + %1 = call i8 %fn.funcptr(ptr nonnull %call.result, ptr %fn.context) #12 ret i8 %1 fpcall.throw: ; preds = %entry - call void @runtime.nilPanic(ptr undef) #9 - unreachable + call void @runtime.nilPanic(ptr undef) #12 + br label %unwind.return + +unwind.return: ; preds = %fpcall.throw + ret i8 undef } declare void @runtime.nilPanic(ptr) #0 @@ -129,10 +134,10 @@ declare void @runtime.nilPanic(ptr) #0 define hidden i8 @main.useLargeInterface(ptr %value.typecode, ptr %value.value, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #9 - call void @"interface:{main.makeLargeValue:func:{}{named:main.largeValue},main.readLargeValue:func:{named:main.largeValue}{basic:uint8}}.makeLargeValue$invoke"(ptr nonnull %call.result, ptr %value.value, ptr %value.typecode, ptr undef) #9 - %0 = call i8 @"interface:{main.makeLargeValue:func:{}{named:main.largeValue},main.readLargeValue:func:{named:main.largeValue}{basic:uint8}}.readLargeValue$invoke"(ptr %value.value, ptr nonnull %call.result, ptr %value.typecode, ptr undef) #9 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #12 + call void @"interface:{main.makeLargeValue:func:{}{named:main.largeValue},main.readLargeValue:func:{named:main.largeValue}{basic:uint8}}.makeLargeValue$invoke"(ptr nonnull %call.result, ptr %value.value, ptr %value.typecode, ptr undef) #12 + %0 = call i8 @"interface:{main.makeLargeValue:func:{}{named:main.largeValue},main.readLargeValue:func:{named:main.largeValue}{basic:uint8}}.readLargeValue$invoke"(ptr %value.value, ptr nonnull %call.result, ptr %value.typecode, ptr undef) #12 ret i8 %0 } @@ -144,42 +149,51 @@ declare i8 @"interface:{main.makeLargeValue:func:{}{named:main.largeValue},main. define hidden void @main.deferLargeValue(ptr readonly dereferenceable_or_null(1025) %value, ptr %context) unnamed_addr #1 { entry: %defer.alloca = alloca { i32, ptr, ptr }, align 8 - %deferPtr = alloca ptr, align 4 - store ptr null, ptr %deferPtr, align 4 + %deferframe.buf = alloca %runtime.deferFrame, align 8 + %deferPtr = getelementptr inbounds nuw i8, ptr %deferframe.buf, i32 24 + %0 = call ptr @llvm.stacksave.p0() + call void @runtime.setupDeferFrame(ptr nonnull %deferframe.buf, ptr %0, ptr undef) #12 %stackalloc = alloca i8, align 1 - call void @runtime.trackPointer(ptr nonnull %defer.alloca, ptr nonnull %stackalloc, ptr undef) #9 + %defer.next = load ptr, ptr %deferPtr, align 4 + call void @runtime.trackPointer(ptr nonnull %defer.alloca, ptr nonnull %stackalloc, ptr undef) #12 store i32 0, ptr %defer.alloca, align 4 - %defer.alloca.repack1 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 4 - store ptr null, ptr %defer.alloca.repack1, align 4 - %defer.alloca.repack3 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 8 - store ptr %value, ptr %defer.alloca.repack3, align 4 + %defer.alloca.repack15 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 4 + store ptr %defer.next, ptr %defer.alloca.repack15, align 4 + %defer.alloca.repack17 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 8 + store ptr %value, ptr %defer.alloca.repack17, align 4 store ptr %defer.alloca, ptr %deferPtr, align 4 br label %rundefers.block rundefers.after: ; preds = %rundefers.end + call void @runtime.destroyDeferFrame(ptr nonnull %deferframe.buf, ptr undef) #12 + %unwind = call i1 @runtime.unwindPending(ptr undef) #12 + br i1 %unwind, label %unwind.return, label %unwind.continue + +unwind.continue: ; preds = %rundefers.after ret void rundefers.block: ; preds = %entry br label %rundefers.loophead rundefers.loophead: ; preds = %rundefers.callback0, %rundefers.block - %0 = load ptr, ptr %deferPtr, align 4 - %stackIsNil = icmp eq ptr %0, null + %1 = load ptr, ptr %deferPtr, align 4 + %stackIsNil = icmp eq ptr %1, null br i1 %stackIsNil, label %rundefers.end, label %rundefers.loop rundefers.loop: ; preds = %rundefers.loophead - %stack.next.gep = getelementptr inbounds nuw i8, ptr %0, i32 4 + %stack.next.gep = getelementptr inbounds nuw i8, ptr %1, i32 4 %stack.next = load ptr, ptr %stack.next.gep, align 4 store ptr %stack.next, ptr %deferPtr, align 4 - %callback = load i32, ptr %0, align 4 + %callback = load i32, ptr %1, align 4 switch i32 %callback, label %rundefers.default [ i32 0, label %rundefers.callback0 ] rundefers.callback0: ; preds = %rundefers.loop - %gep = getelementptr inbounds nuw i8, ptr %0, i32 8 + %gep = getelementptr inbounds nuw i8, ptr %1, i32 8 %param = load ptr, ptr %gep, align 4 - %1 = call i8 @main.readLargeValue(ptr %param, ptr undef) + %2 = call i8 @main.deferLargeValue.asyncifycatch.0(ptr %param, ptr undef) #12 + call void @runtime.clearUnwind(ptr undef) #12 br label %rundefers.loophead rundefers.default: ; preds = %rundefers.loop @@ -188,28 +202,97 @@ rundefers.default: ; preds = %rundefers.loop rundefers.end: ; preds = %rundefers.loophead br label %rundefers.after -recover: ; No predecessors! +recover: ; preds = %rundefers.end3 + br i1 poison, label %unwind.return, label %unwind.continue2 + +unwind.continue2: ; preds = %recover + ret void + +lpad: ; No predecessors! + br label %rundefers.loophead6 + +rundefers.loophead6: ; preds = %rundefers.callback012, %lpad + br i1 poison, label %rundefers.end3, label %rundefers.loop5 + +rundefers.loop5: ; preds = %rundefers.loophead6 + switch i32 poison, label %rundefers.default4 [ + i32 0, label %rundefers.callback012 + ] + +rundefers.callback012: ; preds = %rundefers.loop5 + br label %rundefers.loophead6 + +rundefers.default4: ; preds = %rundefers.loop5 + unreachable + +rundefers.end3: ; preds = %rundefers.loophead6 + br label %recover + +unwind.return: ; preds = %recover, %rundefers.after ret void } +; Function Attrs: nocallback nofree nosync nounwind willreturn +declare ptr @llvm.stacksave.p0() #6 + +declare void @runtime.setupDeferFrame(ptr dereferenceable_or_null(28), ptr, ptr) #0 + +declare void @runtime.destroyDeferFrame(ptr dereferenceable_or_null(28), ptr) #0 + +declare i1 @runtime.unwindPending(ptr) #0 + +; Function Attrs: noinline +define internal i8 @main.deferLargeValue.asyncifycatch.0(ptr %0, ptr %1) #7 { +entry: + %2 = call i8 @main.readLargeValue(ptr %0, ptr %1) + %3 = call i1 @runtime.unwindPending(ptr undef) + br i1 %3, label %unwind.stop, label %return + +unwind.stop: ; preds = %entry + call void @runtime.asyncifyStopUnwindImport() + br label %return + +return: ; preds = %unwind.stop, %entry + ret i8 %2 +} + +declare void @runtime.asyncifyStopUnwindImport() #8 + +declare void @runtime.clearUnwind(ptr) #0 + +; Function Attrs: noinline +define internal i8 @main.deferLargeValue.asyncifycatch.1(ptr %0, ptr %1) #7 { +entry: + %2 = call i8 @main.readLargeValue(ptr %0, ptr %1) + %3 = call i1 @runtime.unwindPending(ptr undef) + br i1 %3, label %unwind.stop, label %return + +unwind.stop: ; preds = %entry + call void @runtime.asyncifyStopUnwindImport() + br label %return + +return: ; preds = %unwind.stop, %entry + ret i8 %2 +} + ; Function Attrs: nounwind define hidden void @main.goLargeValue(ptr readonly dereferenceable_or_null(1025) %value, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %go.param = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %go.param, ptr nonnull %stackalloc, ptr undef) #9 + %go.param = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %go.param, ptr nonnull %stackalloc, ptr undef) #12 call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %go.param, ptr noundef nonnull align 1 dereferenceable(1025) %value, i32 1025, i1 false) - call void @"internal/task.start"(i32 ptrtoint (ptr @"main.readLargeValue$gowrapper" to i32), ptr nonnull %go.param, i32 65536, ptr undef) #9 + call void @"internal/task.start"(i32 ptrtoint (ptr @"main.readLargeValue$gowrapper" to i32), ptr nonnull %go.param, i32 65536, ptr undef) #12 ret void } declare void @runtime.deadlock(ptr) #0 ; Function Attrs: nounwind -define linkonce_odr void @"main.readLargeValue$gowrapper"(ptr %0) unnamed_addr #6 { +define linkonce_odr void @"main.readLargeValue$gowrapper"(ptr %0) unnamed_addr #9 { entry: %1 = call i8 @main.readLargeValue(ptr %0, ptr undef) - call void @runtime.deadlock(ptr undef) #9 + call void @runtime.deadlock(ptr undef) #12 unreachable } @@ -219,8 +302,8 @@ declare void @"internal/task.start"(i32, ptr, i32, ptr) #0 define hidden void @main.makeLargeResults(ptr dereferenceable_or_null(1026) %return, i8 %value, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #9 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #12 call void @main.makeLargeValue(ptr nonnull %call.result, i8 %value, ptr undef) call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %return, ptr noundef nonnull align 1 dereferenceable(1025) %call.result, i32 1025, i1 false) %0 = getelementptr inbounds nuw i8, ptr %return, i32 1025 @@ -232,12 +315,12 @@ entry: define hidden void @main.makeTwoLargeResults(ptr dereferenceable_or_null(2050) %return, i8 %value, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #9 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #12 call void @main.makeLargeValue(ptr nonnull %call.result, i8 %value, ptr undef) %0 = add i8 %value, 1 - %call.result1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %call.result1, ptr nonnull %stackalloc, ptr undef) #9 + %call.result1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %call.result1, ptr nonnull %stackalloc, ptr undef) #12 call void @main.makeLargeValue(ptr nonnull %call.result1, i8 %0, ptr undef) call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %return, ptr noundef nonnull align 1 dereferenceable(1025) %call.result, i32 1025, i1 false) %1 = getelementptr inbounds nuw i8, ptr %return, i32 1025 @@ -249,13 +332,13 @@ entry: define hidden void @main.makeMixedLargeResults(ptr dereferenceable_or_null(2051) %return, i8 %value, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #9 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #12 call void @main.makeLargeValue(ptr nonnull %call.result, i8 %value, ptr undef) %0 = add i8 %value, 1 %1 = add i8 %value, 2 - %call.result1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %call.result1, ptr nonnull %stackalloc, ptr undef) #9 + %call.result1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %call.result1, ptr nonnull %stackalloc, ptr undef) #12 call void @main.makeLargeValue(ptr nonnull %call.result1, i8 %1, ptr undef) call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %return, ptr noundef nonnull align 1 dereferenceable(1025) %call.result, i32 1025, i1 false) %2 = getelementptr inbounds nuw i8, ptr %return, i32 1025 @@ -269,14 +352,14 @@ entry: define hidden void @main.chooseLargeValue(ptr dereferenceable_or_null(1025) %return, i1 %flag, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #9 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #12 call void @main.makeLargeValue(ptr nonnull %call.result, i8 1, ptr undef) br i1 %flag, label %if.then, label %if.done if.then: ; preds = %entry - %call.result1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %call.result1, ptr nonnull %stackalloc, ptr undef) #9 + %call.result1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %call.result1, ptr nonnull %stackalloc, ptr undef) #12 call void @main.makeLargeValue(ptr nonnull %call.result1, i8 42, ptr undef) br label %if.done @@ -290,40 +373,43 @@ if.done: ; preds = %if.then, %entry define hidden void @main.makePointerLargeValue(ptr dereferenceable_or_null(1032) %return, ptr dereferenceable_or_null(1) %value, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %complit = call align 4 dereferenceable(1032) ptr @runtime.alloc(i32 1032, ptr nonnull @"runtime/gc.layout:258-000000000000000000000000000000000000000000000000000000000000000002", ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %complit, ptr nonnull %stackalloc, ptr undef) #9 + %complit = call align 4 dereferenceable(1032) ptr @runtime.alloc(i32 1032, ptr nonnull @"runtime/gc.layout:258-000000000000000000000000000000000000000000000000000000000000000002", ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %complit, ptr nonnull %stackalloc, ptr undef) #12 br i1 false, label %store.throw, label %store.next store.next: ; preds = %entry %0 = getelementptr inbounds nuw i8, ptr %complit, i32 1028 store ptr %value, ptr %0, align 4 - %1 = call align 4 dereferenceable(1032) ptr @runtime.alloc(i32 1032, ptr nonnull @"runtime/gc.layout:258-000000000000000000000000000000000000000000000000000000000000000002", ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %1, ptr nonnull %stackalloc, ptr undef) #9 + %1 = call align 4 dereferenceable(1032) ptr @runtime.alloc(i32 1032, ptr nonnull @"runtime/gc.layout:258-000000000000000000000000000000000000000000000000000000000000000002", ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %1, ptr nonnull %stackalloc, ptr undef) #12 call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 4 dereferenceable(1032) %1, ptr noundef nonnull align 4 dereferenceable(1032) %complit, i32 1032, i1 false) call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1032) %return, ptr noundef nonnull align 4 dereferenceable(1032) %1, i32 1032, i1 false) ret void store.throw: ; preds = %entry - unreachable + br label %unwind.return + +unwind.return: ; preds = %store.throw + ret void } ; Function Attrs: nounwind define hidden i8 @main.assertLargeValue(ptr %value.typecode, ptr %value.value, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %large = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %large, ptr nonnull %stackalloc, ptr undef) #9 - %typecode = call i1 @runtime.typeAssert(ptr %value.typecode, ptr nonnull @"reflect/types.typeid:named:main.largeValue", ptr undef) #9 - %typeassert.result = call align 1 dereferenceable(1026) ptr @runtime.alloc(i32 1026, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %typeassert.result, ptr nonnull %stackalloc, ptr undef) #9 + %large = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %large, ptr nonnull %stackalloc, ptr undef) #12 + %typecode = call i1 @runtime.typeAssert(ptr %value.typecode, ptr nonnull @"reflect/types.typeid:named:main.largeValue", ptr undef) #12 + %typeassert.result = call align 1 dereferenceable(1026) ptr @runtime.alloc(i32 1026, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %typeassert.result, ptr nonnull %stackalloc, ptr undef) #12 call void @llvm.memset.p0.i32(ptr noundef nonnull align 1 dereferenceable(1026) %typeassert.result, i8 0, i32 1026, i1 false) br i1 %typecode, label %typeassert.ok, label %typeassert.next typeassert.next: ; preds = %typeassert.ok, %entry %0 = getelementptr inbounds nuw i8, ptr %typeassert.result, i32 1025 store i1 %typecode, ptr %0, align 1 - %t2 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %t2, ptr nonnull %stackalloc, ptr undef) #9 + %t2 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %t2, ptr nonnull %stackalloc, ptr undef) #12 call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %t2, ptr noundef nonnull align 1 dereferenceable(1025) %typeassert.result, i32 1025, i1 false) call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %large, ptr noundef nonnull align 1 dereferenceable(1025) %t2, i32 1025, i1 false) br i1 %typecode, label %if.done, label %if.then @@ -344,24 +430,24 @@ if.then: ; preds = %typeassert.next declare i1 @runtime.typeAssert(ptr, ptr dereferenceable_or_null(1), ptr) #0 ; Function Attrs: nocallback nofree nounwind willreturn memory(argmem: write) -declare void @llvm.memset.p0.i32(ptr writeonly captures(none), i8, i32, i1 immarg) #7 +declare void @llvm.memset.p0.i32(ptr writeonly captures(none), i8, i32, i1 immarg) #10 ; Function Attrs: nounwind define hidden i8 @main.useLargeMap(ptr readonly dereferenceable_or_null(1025) %key, ptr readonly dereferenceable_or_null(1025) %value, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 - %0 = call ptr @runtime.hashmapMakeGeneric(i32 1025, i32 1025, i32 1, ptr null, ptr nonnull @runtime.hash32, ptr null, ptr nonnull @runtime.memequal, ptr undef) #9 - call void @runtime.trackPointer(ptr %0, ptr nonnull %stackalloc, ptr undef) #9 - call void @runtime.hashmapBinarySet(ptr %0, ptr %key, ptr %value, ptr undef) #9 - %result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %result, ptr nonnull %stackalloc, ptr undef) #9 - %hashmap.result = call align 1 dereferenceable(1026) ptr @runtime.alloc(i32 1026, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %hashmap.result, ptr nonnull %stackalloc, ptr undef) #9 - %1 = call i1 @runtime.hashmapBinaryGet(ptr %0, ptr %key, ptr nonnull %hashmap.result, i32 1025, ptr undef) #9 + %0 = call ptr @runtime.hashmapMakeGeneric(i32 1025, i32 1025, i32 1, ptr null, ptr nonnull @runtime.hash32, ptr null, ptr nonnull @runtime.memequal, ptr undef) #12 + call void @runtime.trackPointer(ptr %0, ptr nonnull %stackalloc, ptr undef) #12 + call void @runtime.hashmapBinarySet(ptr %0, ptr %key, ptr %value, ptr undef) #12 + %result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %result, ptr nonnull %stackalloc, ptr undef) #12 + %hashmap.result = call align 1 dereferenceable(1026) ptr @runtime.alloc(i32 1026, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %hashmap.result, ptr nonnull %stackalloc, ptr undef) #12 + %1 = call i1 @runtime.hashmapBinaryGet(ptr %0, ptr %key, ptr nonnull %hashmap.result, i32 1025, ptr undef) #12 %2 = getelementptr inbounds nuw i8, ptr %hashmap.result, i32 1025 store i1 %1, ptr %2, align 1 - %t3 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %t3, ptr nonnull %stackalloc, ptr undef) #9 + %t3 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %t3, ptr nonnull %stackalloc, ptr undef) #12 call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %t3, ptr noundef nonnull align 1 dereferenceable(1025) %hashmap.result, i32 1025, i1 false) call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %result, ptr noundef nonnull align 1 dereferenceable(1025) %t3, i32 1025, i1 false) %3 = getelementptr inbounds nuw i8, ptr %hashmap.result, i32 1025 @@ -394,19 +480,19 @@ entry: %chan.op = alloca %runtime.channelOp, align 8 %stackalloc = alloca i8, align 1 call void @llvm.lifetime.start.p0(ptr nonnull %chan.op) - call void @runtime.chanSend(ptr %ch, ptr %value, ptr nonnull %chan.op, ptr undef) #9 + call void @runtime.chanSend(ptr %ch, ptr %value, ptr nonnull %chan.op, ptr undef) #12 call void @llvm.lifetime.end.p0(ptr nonnull %chan.op) - %result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %result, ptr nonnull %stackalloc, ptr undef) #9 - %chan.result = call align 1 dereferenceable(1026) ptr @runtime.alloc(i32 1026, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %chan.result, ptr nonnull %stackalloc, ptr undef) #9 + %result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %result, ptr nonnull %stackalloc, ptr undef) #12 + %chan.result = call align 1 dereferenceable(1026) ptr @runtime.alloc(i32 1026, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %chan.result, ptr nonnull %stackalloc, ptr undef) #12 call void @llvm.lifetime.start.p0(ptr nonnull %chan.op1) - %0 = call i1 @runtime.chanRecv(ptr %ch, ptr nonnull %chan.result, ptr nonnull %chan.op1, ptr undef) #9 + %0 = call i1 @runtime.chanRecv(ptr %ch, ptr nonnull %chan.result, ptr nonnull %chan.op1, ptr undef) #12 %1 = getelementptr inbounds nuw i8, ptr %chan.result, i32 1025 store i1 %0, ptr %1, align 1 call void @llvm.lifetime.end.p0(ptr nonnull %chan.op1) - %t2 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %t2, ptr nonnull %stackalloc, ptr undef) #9 + %t2 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %t2, ptr nonnull %stackalloc, ptr undef) #12 call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %t2, ptr noundef nonnull align 1 dereferenceable(1025) %chan.result, i32 1025, i1 false) call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %result, ptr noundef nonnull align 1 dereferenceable(1025) %t2, i32 1025, i1 false) %2 = getelementptr inbounds nuw i8, ptr %chan.result, i32 1025 @@ -423,12 +509,12 @@ if.then: ; preds = %entry } ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) -declare void @llvm.lifetime.start.p0(ptr captures(none)) #8 +declare void @llvm.lifetime.start.p0(ptr captures(none)) #11 declare void @runtime.chanSend(ptr dereferenceable_or_null(36), ptr, ptr dereferenceable_or_null(16), ptr) #0 ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) -declare void @llvm.lifetime.end.p0(ptr captures(none)) #8 +declare void @llvm.lifetime.end.p0(ptr captures(none)) #11 declare i1 @runtime.chanRecv(ptr dereferenceable_or_null(36), ptr, ptr dereferenceable_or_null(16), ptr) #0 @@ -449,10 +535,10 @@ entry: %.repack5 = getelementptr inbounds nuw i8, ptr %select.states.alloca, i32 12 store ptr null, ptr %.repack5, align 4 call void @llvm.lifetime.start.p0(ptr nonnull %select.block.alloca) - %select.result = call { i32, i1 } @runtime.chanSelect(ptr nonnull %select.recvbuf.alloca, ptr nonnull %select.states.alloca, i32 2, i32 2, ptr nonnull %select.block.alloca, i32 2, i32 2, ptr undef) #9 + %select.result = call { i32, i1 } @runtime.chanSelect(ptr nonnull %select.recvbuf.alloca, ptr nonnull %select.states.alloca, i32 2, i32 2, ptr nonnull %select.block.alloca, i32 2, i32 2, ptr undef) #12 call void @llvm.lifetime.end.p0(ptr nonnull %select.block.alloca) call void @llvm.lifetime.end.p0(ptr nonnull %select.states.alloca) - call void @runtime.trackPointer(ptr nonnull %select.recvbuf.alloca, ptr nonnull %stackalloc, ptr undef) #9 + call void @runtime.trackPointer(ptr nonnull %select.recvbuf.alloca, ptr nonnull %stackalloc, ptr undef) #12 %1 = extractvalue { i32, i1 } %select.result, 0 %2 = icmp eq i32 %1, 0 br i1 %2, label %select.body, label %select.next @@ -465,10 +551,10 @@ select.next: ; preds = %entry br i1 %3, label %select.body1, label %select.next2 select.body1: ; preds = %select.next - %result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %result, ptr nonnull %stackalloc, ptr undef) #9 - %select.received = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull %select.received, ptr nonnull %stackalloc, ptr undef) #9 + %result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %result, ptr nonnull %stackalloc, ptr undef) #12 + %select.received = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %select.received, ptr nonnull %stackalloc, ptr undef) #12 call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %select.received, ptr noundef nonnull align 1 dereferenceable(1025) %select.recvbuf.alloca, i32 1025, i1 false) call void @llvm.memcpy.p0.p0.i32(ptr noundef nonnull align 1 dereferenceable(1025) %result, ptr noundef nonnull align 1 dereferenceable(1025) %select.received, i32 1025, i1 false) %4 = getelementptr inbounds nuw i8, ptr %result, i32 1024 @@ -476,10 +562,13 @@ select.body1: ; preds = %select.next ret i8 %5 select.next2: ; preds = %select.next - call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:basic:string", ptr nonnull %stackalloc, ptr undef) #9 - call void @runtime.trackPointer(ptr nonnull @"main$pack", ptr nonnull %stackalloc, ptr undef) #9 - call void @runtime._panic(ptr nonnull @"reflect/types.type:basic:string", ptr nonnull @"main$pack", ptr undef) #9 - unreachable + call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:basic:string", ptr nonnull %stackalloc, ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull @"main$pack", ptr nonnull %stackalloc, ptr undef) #12 + call void @runtime._panic(ptr nonnull @"reflect/types.type:basic:string", ptr nonnull @"main$pack", ptr undef) #12 + br label %unwind.return + +unwind.return: ; preds = %select.next2 + ret i8 undef } declare { i32, i1 } @runtime.chanSelect(ptr, ptr, i32, i32, ptr, i32, i32, ptr) #0 @@ -492,7 +581,10 @@ attributes #2 = { nocallback nofree nounwind willreturn memory(argmem: readwrite attributes #3 = { allockind("alloc,zeroed") allocsize(0) "alloc-family"="runtime.alloc" "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" } attributes #4 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" "tinygo-indirect-result"="true" "tinygo-invoke"="main.$methods.makeLargeValue:func:{}{named:main.largeValue}" "tinygo-methods"="main.$methods.makeLargeValue:func:{}{named:main.largeValue}; main.$methods.readLargeValue:func:{named:main.largeValue}{basic:uint8}" } attributes #5 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" "tinygo-invoke"="main.$methods.readLargeValue:func:{named:main.largeValue}{basic:uint8}" "tinygo-methods"="main.$methods.makeLargeValue:func:{}{named:main.largeValue}; main.$methods.readLargeValue:func:{named:main.largeValue}{basic:uint8}" } -attributes #6 = { nounwind "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" "tinygo-gowrapper"="main.readLargeValue" } -attributes #7 = { nocallback nofree nounwind willreturn memory(argmem: write) } -attributes #8 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) } -attributes #9 = { nounwind } +attributes #6 = { nocallback nofree nosync nounwind willreturn } +attributes #7 = { noinline } +attributes #8 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" "wasm-import-module"="asyncify" "wasm-import-name"="stop_unwind" } +attributes #9 = { nounwind "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" "tinygo-gowrapper"="main.readLargeValue" } +attributes #10 = { nocallback nofree nounwind willreturn memory(argmem: write) } +attributes #11 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) } +attributes #12 = { nounwind } diff --git a/compiler/testdata/slice.ll b/compiler/testdata/slice.ll index a0756b54b6..9fdf794f09 100644 --- a/compiler/testdata/slice.ll +++ b/compiler/testdata/slice.ll @@ -36,7 +36,10 @@ lookup.next: ; preds = %entry lookup.throw: ; preds = %entry call void @runtime.lookupPanic(ptr undef) #5 - unreachable + br label %unwind.return + +unwind.return: ; preds = %lookup.throw + ret i32 undef } declare void @runtime.lookupPanic(ptr) #0 @@ -115,7 +118,10 @@ slice.next: ; preds = %entry slice.throw: ; preds = %entry call void @runtime.slicePanic(ptr undef) #5 - unreachable + br label %unwind.return + +unwind.return: ; preds = %slice.throw + ret { ptr, i32, i32 } undef } declare void @runtime.slicePanic(ptr) #0 @@ -138,7 +144,10 @@ slice.next: ; preds = %entry slice.throw: ; preds = %entry call void @runtime.slicePanic(ptr undef) #5 - unreachable + br label %unwind.return + +unwind.return: ; preds = %slice.throw + ret { ptr, i32, i32 } undef } ; Function Attrs: nounwind @@ -159,7 +168,10 @@ slice.next: ; preds = %entry slice.throw: ; preds = %entry call void @runtime.slicePanic(ptr undef) #5 - unreachable + br label %unwind.return + +unwind.return: ; preds = %slice.throw + ret { ptr, i32, i32 } undef } ; Function Attrs: nounwind @@ -180,7 +192,10 @@ slice.next: ; preds = %entry slice.throw: ; preds = %entry call void @runtime.slicePanic(ptr undef) #5 - unreachable + br label %unwind.return + +unwind.return: ; preds = %slice.throw + ret { ptr, i32, i32 } undef } ; Function Attrs: nounwind @@ -213,7 +228,10 @@ slicetoarray.next: ; preds = %entry slicetoarray.throw: ; preds = %entry call void @runtime.sliceToArrayPointerPanic(ptr undef) #5 - unreachable + br label %unwind.return + +unwind.return: ; preds = %slicetoarray.throw + ret ptr undef } declare void @runtime.sliceToArrayPointerPanic(ptr) #0 @@ -230,7 +248,10 @@ slicetoarray.next: ; preds = %entry ret ptr %makeslice slicetoarray.throw: ; preds = %entry - unreachable + br label %unwind.return + +unwind.return: ; preds = %slicetoarray.throw + ret ptr undef } ; Function Attrs: nounwind @@ -253,7 +274,10 @@ unsafe.Slice.next: ; preds = %entry unsafe.Slice.throw: ; preds = %entry call void @runtime.unsafeSlicePanic(ptr undef) #5 - unreachable + br label %unwind.return + +unwind.return: ; preds = %unsafe.Slice.throw + ret { ptr, i32, i32 } undef } declare void @runtime.unsafeSlicePanic(ptr) #0 @@ -277,7 +301,10 @@ unsafe.Slice.next: ; preds = %entry unsafe.Slice.throw: ; preds = %entry call void @runtime.unsafeSlicePanic(ptr undef) #5 - unreachable + br label %unwind.return + +unwind.return: ; preds = %unsafe.Slice.throw + ret { ptr, i32, i32 } undef } ; Function Attrs: nounwind @@ -301,7 +328,10 @@ unsafe.Slice.next: ; preds = %entry unsafe.Slice.throw: ; preds = %entry call void @runtime.unsafeSlicePanic(ptr undef) #5 - unreachable + br label %unwind.return + +unwind.return: ; preds = %unsafe.Slice.throw + ret { ptr, i32, i32 } undef } ; Function Attrs: nounwind @@ -325,7 +355,10 @@ unsafe.Slice.next: ; preds = %entry unsafe.Slice.throw: ; preds = %entry call void @runtime.unsafeSlicePanic(ptr undef) #5 - unreachable + br label %unwind.return + +unwind.return: ; preds = %unsafe.Slice.throw + ret { ptr, i32, i32 } undef } attributes #0 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" } diff --git a/compiler/testdata/string.ll b/compiler/testdata/string.ll index dd9986ed3d..53ee6f891d 100644 --- a/compiler/testdata/string.ll +++ b/compiler/testdata/string.ll @@ -46,7 +46,10 @@ lookup.next: ; preds = %entry lookup.throw: ; preds = %entry call void @runtime.lookupPanic(ptr undef) #2 - unreachable + br label %unwind.return + +unwind.return: ; preds = %lookup.throw + ret i8 undef } declare void @runtime.lookupPanic(ptr) #0 @@ -91,7 +94,10 @@ lookup.next: ; preds = %entry lookup.throw: ; preds = %entry call void @runtime.lookupPanic(ptr undef) #2 - unreachable + br label %unwind.return + +unwind.return: ; preds = %lookup.throw + ret i8 undef } attributes #0 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" } diff --git a/main.go b/main.go index 6751b2669c..c627a2dde1 100644 --- a/main.go +++ b/main.go @@ -1762,6 +1762,7 @@ func main() { opt := flag.String("opt", "z", "optimization level: 0, 1, 2, s, z") gc := flag.String("gc", "", "garbage collector to use (none, leaking, conservative, custom, precise, boehm)") panicStrategy := flag.String("panic", "print", "panic strategy (print, trap)") + panicUnwind := flag.String("panic-unwind", "", "panic unwind strategy (auto, explicit)") scheduler := flag.String("scheduler", "", "which scheduler to use (none, tasks, cores, threads, asyncify)") serial := flag.String("serial", "", "which serial output to use (none, uart, usb, rtt)") work := flag.Bool("work", false, "print the name of the temporary build directory and do not delete this directory on exit") @@ -1901,6 +1902,7 @@ func main() { Opt: *opt, GC: *gc, PanicStrategy: *panicStrategy, + PanicUnwind: *panicUnwind, Scheduler: *scheduler, Serial: *serial, Work: *work, diff --git a/main_test.go b/main_test.go index 2dfd1683f6..2a9e32d96e 100644 --- a/main_test.go +++ b/main_test.go @@ -408,11 +408,17 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) { runTest("rand.go", options, t, nil, nil) }) } - if !isWebAssembly { - // The recover() builtin isn't supported yet on WebAssembly and Windows. - t.Run("recover.go", func(t *testing.T) { + t.Run("recover.go", func(t *testing.T) { + t.Parallel() + runTest("recover.go", options, t, nil, nil) + }) + if isWebAssembly { + t.Run("recover-explicit.go", func(t *testing.T) { t.Parallel() - runTest("recover.go", options, t, nil, nil) + options := compileopts.Options(options) + options.Scheduler = "none" + options.PanicUnwind = "explicit" + runTest("recover-explicit.go", options, t, nil, nil) }) } } @@ -598,7 +604,7 @@ func TestWebAssembly(t *testing.T) { for _, tc := range []testCase{ // Test whether there really are no imports when using -panic=trap. This // tests the bugfix for https://github.com/tinygo-org/tinygo/issues/4161. - {name: "panic-default", target: "wasip1", imports: []string{"wasi_snapshot_preview1.fd_write", "wasi_snapshot_preview1.random_get"}}, + {name: "panic-default", target: "wasip1", imports: []string{"wasi_snapshot_preview1.fd_write", "wasi_snapshot_preview1.proc_exit", "wasi_snapshot_preview1.random_get"}}, {name: "panic-trap", target: "wasm-unknown", panicStrategy: "trap", imports: []string{}}, } { t.Run(tc.name, func(t *testing.T) { @@ -1020,6 +1026,52 @@ func TestGoexitCrash(t *testing.T) { } }) } + + for _, tc := range []struct { + name string + arg string + panicStrategy string + want string + }{ + {"wasip1-deadlock", "deadlock", "", "deadlocked: no event source"}, + {"wasip1-goexit-panic-trap", "defer", "trap", "defer ran"}, + } { + t.Run(tc.name, func(t *testing.T) { + options := optionsFromTarget("wasip1", sema) + options.PanicStrategy = tc.panicStrategy + config, err := builder.NewConfig(&options) + if err != nil { + t.Fatal(err) + } + + result, err := builder.Build("testdata/goexit.go", ".wasm", t.TempDir(), config) + if err != nil { + t.Fatal("failed to build binary:", err) + } + data, err := os.ReadFile(result.Binary) + if err != nil { + t.Fatal("failed to read binary:", err) + } + + output := &bytes.Buffer{} + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigInterpreter()) + defer r.Close(ctx) + wasi_snapshot_preview1.MustInstantiate(ctx, r) + moduleConfig := wazero.NewModuleConfig(). + WithArgs(result.Binary, tc.arg). + WithStdout(output). + WithStderr(output) + _, err = r.InstantiateWithConfig(ctx, data, moduleConfig) + if err == nil { + t.Fatal("program unexpectedly exited successfully") + } + if !strings.Contains(output.String(), tc.want) { + t.Fatalf("output does not contain %q:\n%s", tc.want, output.String()) + } + }) + } } func TestRuntimeFatal(t *testing.T) { diff --git a/src/internal/task/task_asyncify.go b/src/internal/task/task_asyncify.go index d7d9a6de42..a3d8fa999a 100644 --- a/src/internal/task/task_asyncify.go +++ b/src/internal/task/task_asyncify.go @@ -83,6 +83,10 @@ func (s *state) initialize(fn uintptr, args unsafe.Pointer, stackSize uintptr) { // currentTask is the current running task, or nil if currently in the scheduler. var currentTask *Task +// Panic unwinding is synchronous: it either reaches a defer frame or aborts +// without switching tasks. A single transient header is therefore sufficient. +var panicStackState stackState + // Current returns the current active task. func Current() *Task { return currentTask @@ -98,6 +102,14 @@ func Pause() { currentTask.state.unwind() } +// PanicUnwind starts an Asyncify unwind without pausing the task. A defer +// frame stops the unwind before it reaches the scheduler. +func PanicUnwind() { + panicStackState.asyncifysp = currentTask.state.asyncifysp + panicStackState.csp = currentTask.state.csp + panicStackState.unwind() +} + //export tinygo_unwind func (*stackState) unwind() diff --git a/src/runtime/panic.go b/src/runtime/panic.go index ff4bb8af25..3a837a7659 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -20,7 +20,7 @@ func trap() func tinygo_longjmp(frame *deferFrame) // Compiler intrinsic. -// Returns whether recover is supported on the current architecture. +// Reports whether recover and Goexit unwinding are supported. func supportsRecover() bool // Compile intrinsic. @@ -38,7 +38,7 @@ type deferFrame struct { JumpPC unsafe.Pointer // pc to return to ExtraRegs [deferExtraRegs]unsafe.Pointer // extra registers (depending on the architecture) Previous *deferFrame // previous recover buffer pointer - Panicking panicState // panic/Goexit state + PanicState panicState // panic/Goexit and unwind state PanicValue interface{} // panic value, might be nil for panic(nil) for example DeferPtr unsafe.Pointer // head of the stack-allocated defer list } @@ -48,6 +48,7 @@ type panicState uint8 const ( panicTrue panicState = 1 << iota panicGoexit + panicUnwinding ) // Builtin function panic(msg), used as a compiler intrinsic. @@ -55,23 +56,33 @@ func _panic(message interface{}) { panicOrGoexit(message, panicTrue) } +func startPanicUnwind(message interface{}, state panicState) bool { + // Note: recover is not supported inside interrupts. + // (This could be supported, like defer, but we currently don't). + if !supportsRecover() || interrupt.In() { + return false + } + frame := currentDeferFrame() + if frame == nil { + return false + } + if state&panicTrue != 0 { + // Preserve a suspended Goexit so it can resume if this panic is + // recovered. + state |= frame.PanicState & panicGoexit + } + frame.PanicValue = message + frame.PanicState = state + return startUnwind(frame) +} + func panicOrGoexit(message interface{}, panicking panicState) { + // Goexit must run deferred calls regardless of the panic strategy. if panicking != panicGoexit && panicStrategy() == tinygo.PanicStrategyTrap { trap() } - // Note: recover is not supported inside interrupts. - // (This could be supported, like defer, but we currently don't). - if supportsRecover() && !interrupt.In() { - frame := (*deferFrame)(task.Current().DeferFrame) - if frame != nil { - frame.PanicValue = message - if panicking&panicTrue != 0 { - panicking |= frame.Panicking & panicGoexit - } - frame.Panicking = panicking - tinygo_longjmp(frame) - // unreachable - } + if startPanicUnwind(message, panicking) { + return } if panicking == panicGoexit { // Call to Goexit() instead of a panic. @@ -107,16 +118,10 @@ func runtimePanicAt(addr unsafe.Pointer, msg string) { if panicStrategy() == tinygo.PanicStrategyTrap { trap() } - if supportsRecover() && !interrupt.In() { - frame := (*deferFrame)(task.Current().DeferFrame) - if frame != nil { - // Use the normal panic mechanism so that this runtime error - // can be recovered with recover(). - frame.PanicValue = plainError(msg) - frame.Panicking = panicTrue | (frame.Panicking & panicGoexit) - tinygo_longjmp(frame) - // unreachable - } + // Use the normal panic mechanism so that this runtime error + // can be recovered with recover(). + if startPanicUnwind(plainError(msg), panicTrue) { + return } if hasReturnAddr { // Note: the string "panic: runtime error at " is also used in @@ -133,6 +138,16 @@ func runtimePanicAt(addr unsafe.Pointer, msg string) { abort() } +//go:inline +//go:nobounds +func currentDeferFrame() *deferFrame { + currentTask := task.Current() + if currentTask == nil { + return nil + } + return (*deferFrame)(currentTask.DeferFrame) +} + // 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 @@ -150,7 +165,7 @@ func setupDeferFrame(frame *deferFrame, jumpSP unsafe.Pointer) { currentTask := task.Current() frame.Previous = (*deferFrame)(currentTask.DeferFrame) frame.JumpSP = jumpSP - frame.Panicking = 0 + frame.PanicState = 0 frame.DeferPtr = nil currentTask.DeferFrame = unsafe.Pointer(frame) } @@ -163,12 +178,12 @@ func setupDeferFrame(frame *deferFrame, jumpSP unsafe.Pointer) { //go:nobounds func destroyDeferFrame(frame *deferFrame) { task.Current().DeferFrame = unsafe.Pointer(frame.Previous) - if frame.Panicking&panicTrue != 0 { + if frame.PanicState&panicTrue != 0 { // We're still panicking! // Re-raise the panic now. panicOrGoexit(frame.PanicValue, panicTrue) } - if frame.Panicking&panicGoexit != 0 { + if frame.PanicState&panicGoexit != 0 { // A deferred function panicked during Goexit, and that panic was // recovered. Continue the original Goexit instead of returning. panicOrGoexit(nil, panicGoexit) @@ -193,8 +208,7 @@ func _recover(useParentFrame bool) interface{} { if !supportsRecover() || interrupt.In() { // Either we're compiling without stack unwinding support, or we're // inside an interrupt where panic/recover is not supported. Either way, - // make this a no-op since panic() won't do any long jumps to a deferred - // function. + // panic() won't unwind to a deferred function. return nil } frame := (*deferFrame)(task.Current().DeferFrame) @@ -203,15 +217,15 @@ func _recover(useParentFrame bool) interface{} { // already), but instead from the previous frame. frame = frame.Previous } - if frame != nil && frame.Panicking != 0 { - if frame.Panicking&panicTrue == 0 { + if frame != nil && frame.PanicState&(panicTrue|panicGoexit) != 0 { + if frame.PanicState&panicTrue == 0 { // Special value that indicates we're exiting the goroutine using // Goexit(). Therefore, make this recover call a no-op. return nil } // Only the first call to recover returns the panic value. It also stops // the panicking sequence, hence setting panicking to false. - frame.Panicking &^= panicTrue + frame.PanicState &^= panicTrue return frame.PanicValue } // Not panicking, so return a nil interface. diff --git a/src/runtime/panic_unwind_asyncify.go b/src/runtime/panic_unwind_asyncify.go new file mode 100644 index 0000000000..0be2d6cb2a --- /dev/null +++ b/src/runtime/panic_unwind_asyncify.go @@ -0,0 +1,15 @@ +//go:build tinygo.unwind.asyncify + +package runtime + +import "internal/task" + +//go:wasmimport asyncify stop_unwind +func asyncifyStopUnwindImport() + +func startUnwind(frame *deferFrame) bool { + frame.PanicState |= panicUnwinding + setUnwindSignal(true) + task.PanicUnwind() + return true +} diff --git a/src/runtime/panic_unwind_explicit.go b/src/runtime/panic_unwind_explicit.go new file mode 100644 index 0000000000..231d4ae9c6 --- /dev/null +++ b/src/runtime/panic_unwind_explicit.go @@ -0,0 +1,9 @@ +//go:build tinygo.unwind.explicit + +package runtime + +func startUnwind(frame *deferFrame) bool { + frame.PanicState |= panicUnwinding + setUnwindSignal(true) + return true +} diff --git a/src/runtime/panic_unwind_none.go b/src/runtime/panic_unwind_none.go new file mode 100644 index 0000000000..3ccb5f5833 --- /dev/null +++ b/src/runtime/panic_unwind_none.go @@ -0,0 +1,7 @@ +//go:build tinygo.unwind.none + +package runtime + +func startUnwind(frame *deferFrame) bool { + return false +} diff --git a/src/runtime/panic_unwind_return.go b/src/runtime/panic_unwind_return.go new file mode 100644 index 0000000000..66d5cc8067 --- /dev/null +++ b/src/runtime/panic_unwind_return.go @@ -0,0 +1,19 @@ +//go:build tinygo.unwind.explicit || tinygo.unwind.asyncify + +package runtime + +//go:inline +//go:nobounds +func unwindPending() bool { + return getUnwindSignal() +} + +//go:inline +//go:nobounds +func clearUnwind() { + setUnwindSignal(false) + frame := currentDeferFrame() + if frame != nil { + frame.PanicState &^= panicUnwinding + } +} diff --git a/src/runtime/panic_unwind_setjmp.go b/src/runtime/panic_unwind_setjmp.go new file mode 100644 index 0000000000..f91aa4ccf3 --- /dev/null +++ b/src/runtime/panic_unwind_setjmp.go @@ -0,0 +1,8 @@ +//go:build tinygo.unwind.setjmp + +package runtime + +func startUnwind(frame *deferFrame) bool { + tinygo_longjmp(frame) + return false +} diff --git a/src/runtime/panic_unwind_signal_cores.go b/src/runtime/panic_unwind_signal_cores.go new file mode 100644 index 0000000000..d6435d65d5 --- /dev/null +++ b/src/runtime/panic_unwind_signal_cores.go @@ -0,0 +1,15 @@ +//go:build tinygo.unwind.explicit && scheduler.cores + +package runtime + +var unwindPendingSignal [numCPU]bool + +//go:inline +func getUnwindSignal() bool { + return unwindPendingSignal[currentCPU()] +} + +//go:inline +func setUnwindSignal(unwinding bool) { + unwindPendingSignal[currentCPU()] = unwinding +} diff --git a/src/runtime/panic_unwind_signal_unicore.go b/src/runtime/panic_unwind_signal_unicore.go new file mode 100644 index 0000000000..c688f30bde --- /dev/null +++ b/src/runtime/panic_unwind_signal_unicore.go @@ -0,0 +1,17 @@ +//go:build (tinygo.unwind.explicit || tinygo.unwind.asyncify) && !scheduler.cores && !scheduler.threads + +package runtime + +// The signal is only set while returning synchronously to the defer frame +// recorded in PanicState, and is cleared before deferred calls can schedule. +var unwindPendingSignal bool + +//go:inline +func getUnwindSignal() bool { + return unwindPendingSignal +} + +//go:inline +func setUnwindSignal(unwinding bool) { + unwindPendingSignal = unwinding +} diff --git a/testdata/goexit.go b/testdata/goexit.go index fd6c0d2cd6..09c7417a6c 100644 --- a/testdata/goexit.go +++ b/testdata/goexit.go @@ -10,6 +10,9 @@ func main() { switch os.Args[1] { case "main": runtime.Goexit() + case "defer": + defer println("defer ran") + runtime.Goexit() case "deadlock": f := func() { for i := 0; i < 10; i++ { diff --git a/testdata/recover-explicit.go b/testdata/recover-explicit.go new file mode 100644 index 0000000000..5d32a256f1 --- /dev/null +++ b/testdata/recover-explicit.go @@ -0,0 +1,18 @@ +package main + +func main() { + catch() + println("done") +} + +func catch() { + defer func() { + println("recovered:", recover() == "panic") + }() + call() + println("unreachable after call") +} + +func call() { + panic("panic") +} diff --git a/testdata/recover-explicit.txt b/testdata/recover-explicit.txt new file mode 100644 index 0000000000..68d90d4d9a --- /dev/null +++ b/testdata/recover-explicit.txt @@ -0,0 +1,2 @@ +recovered: true +done diff --git a/testdata/recover.go b/testdata/recover.go index bb90f0da62..f5ff7ffdec 100644 --- a/testdata/recover.go +++ b/testdata/recover.go @@ -6,6 +6,7 @@ import ( ) var wg sync.WaitGroup +var panicMap = map[interface{}]int{} func main() { println("# simple recover") @@ -274,6 +275,21 @@ func recoverRuntimeError() { _ = x.(string) }) recoverEmptyInterfaceTypeAssert() + recoverMustPanic("interface compare", func() { + var x interface{} = []int{} + _ = x == x + }) + recoverMustPanic("map key", func() { + panicMap[[]int{}] = 1 + }) + recoverMustPanic("map lookup key", func() { + m := map[interface{}]int{} + _ = m[[]int{}] + }) + recoverMustPanic("map delete key", func() { + m := map[interface{}]int{} + delete(m, []int{}) + }) } //go:noinline diff --git a/testdata/recover.txt b/testdata/recover.txt index e61cf25fe6..3769df4991 100644 --- a/testdata/recover.txt +++ b/testdata/recover.txt @@ -49,6 +49,10 @@ outer recovered: repanic value recovered: slice recovered: type assert recovered: empty interface type assert + recovered: interface compare + recovered: map key + recovered: map lookup key + recovered: map delete key # recover from nil map and closed channel recovered: nil map diff --git a/testdata/testing-wasm.txt b/testdata/testing-wasm.txt index 6a8ee05918..1d61154dc0 100644 --- a/testdata/testing-wasm.txt +++ b/testdata/testing-wasm.txt @@ -11,6 +11,8 @@ c failed after failed + --- FAIL: TestBar/Bar4 (0.00s) + fatal log Bar end --- FAIL: TestAllLowercase (0.00s) --- FAIL: TestAllLowercase/BETA (0.00s) diff --git a/testdata/testing.go b/testdata/testing.go index 2e56d8f577..f8447fa56d 100644 --- a/testdata/testing.go +++ b/testdata/testing.go @@ -6,7 +6,6 @@ import ( "errors" "flag" "io" - "runtime" "strings" "testing" ) @@ -26,16 +25,14 @@ func TestBar(t *testing.T) { t.Log("after failed") }) t.Run("Bar3", func(t *testing.T) {}) - if runtime.GOARCH != "wasm" { - t.Run("Bar4", func(t *testing.T) { - t.Fatal("fatal") - t.Log("after fatal") - }) - t.Run("Bar5", func(t *testing.T) { - t.SkipNow() - t.Error("after skip") - }) - } + t.Run("Bar4", func(t *testing.T) { + t.Fatal("fatal") + t.Log("after fatal") + }) + t.Run("Bar5", func(t *testing.T) { + t.SkipNow() + t.Error("after skip") + }) t.Log("log Bar end") } diff --git a/tests/testing/pass/pass_test.go b/tests/testing/pass/pass_test.go index 3dd229385d..4d545bed7f 100644 --- a/tests/testing/pass/pass_test.go +++ b/tests/testing/pass/pass_test.go @@ -5,3 +5,66 @@ import "testing" func TestPass(t *testing.T) { // This test passes. } + +func TestDeferredSuspend(t *testing.T) { + ready := make(chan struct{}) + release := make(chan struct{}) + finished := make(chan struct{}) + go func() { + defer func() { + close(ready) + <-release + close(finished) + }() + }() + <-ready + close(release) + <-finished +} + +func TestBlockingSendWithDefer(t *testing.T) { + ready := make(chan struct{}) + values := make(chan int) + finished := make(chan struct{}) + go func() { + defer close(finished) + close(ready) + values <- 1 + }() + <-ready + if value := <-values; value != 1 { + t.Fatalf("unexpected value: %d", value) + } + <-finished +} + +func TestConcurrentAggregateSuspend(t *testing.T) { + ready := make(chan int) + release := make(chan int) + results := make(chan [2]int) + for id := 1; id <= 2; id++ { + go func() { + defer func() {}() + first, second := suspendedPair(id, ready, release) + results <- [2]int{first, second} + }() + } + <-ready + <-ready + release <- 20 + release <- 10 + for range 2 { + result := <-results + if result[0] != 1 && result[0] != 2 { + t.Fatalf("unexpected first result: %d", result[0]) + } + if result[1] != 10 && result[1] != 20 { + t.Fatalf("unexpected second result: %d", result[1]) + } + } +} + +func suspendedPair(id int, ready chan<- int, release <-chan int) (int, int) { + ready <- id + return id, <-release +} diff --git a/transform/optimizer.go b/transform/optimizer.go index 150a9a77cb..e1d7f8071b 100644 --- a/transform/optimizer.go +++ b/transform/optimizer.go @@ -137,6 +137,10 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error { } } + if speedLevel > 0 && config.PanicUnwind() == "asyncify" { + AddUnwindAssumptions(mod) + } + if config.VerifyIR() { if errs := ircheck.Module(mod); errs != nil { return errs diff --git a/transform/testdata/unwind.ll b/transform/testdata/unwind.ll new file mode 100644 index 0000000000..6ea94741ff --- /dev/null +++ b/transform/testdata/unwind.ll @@ -0,0 +1,53 @@ +target datalayout = "e-p:32:32" +target triple = "wasm32-unknown-unknown" + +@runtime.unwindPendingSignal = internal global i1 false +@value = global i32 0 + +define internal void @safe() { +entry: + store i32 1, ptr @value + ret void +} + +define internal void @panics() { +entry: + store i1 true, ptr @runtime.unwindPendingSignal + ret void +} + +declare void @external() + +define i1 @checkSafe() { +entry: + call void @safe() + %unwind = call i1 @runtime.unwindPending() + ret i1 %unwind +} + +define i1 @checkPanic() { +entry: + call void @panics() + %unwind = call i1 @runtime.unwindPending() + ret i1 %unwind +} + +define i1 @checkExternal() { +entry: + call void @external() + %unwind = call i1 @runtime.unwindPending() + ret i1 %unwind +} + +define i1 @checkIndirect(ptr %fn) { +entry: + call void %fn() + %unwind = call i1 @runtime.unwindPending() + ret i1 %unwind +} + +define i1 @runtime.unwindPending() { +entry: + %unwind = load i1, ptr @runtime.unwindPendingSignal + ret i1 %unwind +} diff --git a/transform/testdata/unwind.out.ll b/transform/testdata/unwind.out.ll new file mode 100644 index 0000000000..8462c65073 --- /dev/null +++ b/transform/testdata/unwind.out.ll @@ -0,0 +1,57 @@ +target datalayout = "e-p:32:32" +target triple = "wasm32-unknown-unknown" + +@runtime.unwindPendingSignal = internal unnamed_addr global i1 false +@value = local_unnamed_addr global i32 0 + +declare void @external() local_unnamed_addr + +define noundef i1 @checkSafe() local_unnamed_addr #0 { +entry: + %unwind.entry = load i1, ptr @runtime.unwindPendingSignal, align 1 + %0 = xor i1 %unwind.entry, true + tail call void @llvm.assume(i1 %0) + store i32 1, ptr @value, align 4 + ret i1 false +} + +define noundef i1 @checkPanic() local_unnamed_addr #0 { +entry: + %unwind.entry = load i1, ptr @runtime.unwindPendingSignal, align 1 + %0 = xor i1 %unwind.entry, true + tail call void @llvm.assume(i1 %0) + store i1 true, ptr @runtime.unwindPendingSignal, align 1 + ret i1 true +} + +define i1 @checkExternal() local_unnamed_addr { +entry: + %unwind.entry = load i1, ptr @runtime.unwindPendingSignal, align 1 + %0 = xor i1 %unwind.entry, true + tail call void @llvm.assume(i1 %0) + tail call void @external() + %unwind.i = load i1, ptr @runtime.unwindPendingSignal, align 1 + ret i1 %unwind.i +} + +define i1 @checkIndirect(ptr nocapture readonly %fn) local_unnamed_addr { +entry: + %unwind.entry = load i1, ptr @runtime.unwindPendingSignal, align 1 + %0 = xor i1 %unwind.entry, true + tail call void @llvm.assume(i1 %0) + tail call void %fn() + %unwind.i = load i1, ptr @runtime.unwindPendingSignal, align 1 + ret i1 %unwind.i +} + +define i1 @runtime.unwindPending() local_unnamed_addr #1 { +entry: + %unwind = load i1, ptr @runtime.unwindPendingSignal, align 1 + ret i1 %unwind +} + +declare void @llvm.assume(i1 noundef) #2 + +attributes #0 = { mustprogress nofree norecurse nosync nounwind willreturn memory(readwrite, argmem: none, inaccessiblemem: write, target_mem0: none, target_mem1: none) } +attributes #1 = { mustprogress nofree norecurse nosync nounwind willreturn memory(read, argmem: none, inaccessiblemem: none, target_mem0: none, target_mem1: none) } +attributes #2 = { mustprogress nocallback nofree nosync nounwind willreturn memory(inaccessiblemem: write) } diff --git a/transform/transform_test.go b/transform/transform_test.go index b79e177639..65caebefae 100644 --- a/transform/transform_test.go +++ b/transform/transform_test.go @@ -199,6 +199,7 @@ func compileGoFileForTesting(t *testing.T, filename string) llvm.Module { AutomaticStackSize: config.AutomaticStackSize(), Debug: true, PanicStrategy: config.PanicStrategy(), + PanicUnwind: config.PanicUnwind(), } machine, err := compiler.NewTargetMachine(compilerConfig) if err != nil { diff --git a/transform/unwind.go b/transform/unwind.go new file mode 100644 index 0000000000..ebcafddce5 --- /dev/null +++ b/transform/unwind.go @@ -0,0 +1,53 @@ +package transform + +import "tinygo.org/x/go-llvm" + +// AddUnwindAssumptions records that normal Go calls only begin while the +// transient Asyncify unwind signal is clear. Asyncify panic unwinding is +// synchronous and cannot be interrupted by another Go entry. Checks are not +// emitted while deferred calls run, and landing pads clear the signal before +// invoking deferred code. LLVM's alias analysis can then remove checks around +// calls that do not modify the signal. +func AddUnwindAssumptions(mod llvm.Module) bool { + signal := mod.NamedGlobal("runtime.unwindPendingSignal") + if signal.IsNil() || + signal.GlobalValueType().TypeKind() != llvm.IntegerTypeKind || + signal.GlobalValueType().IntTypeWidth() != 1 { + return false + } + unwind := mod.NamedFunction("runtime.unwindPending") + if unwind.IsNil() { + return false + } + + functions := make(map[llvm.Value]struct{}) + // Suspension-safe catchers call unwindPending indirectly and are + // intentionally absent from this set. + for _, call := range getUses(unwind) { + if call.IsACallInst().IsNil() || call.CalledValue() != unwind { + continue + } + functions[call.InstructionParent().Parent()] = struct{}{} + } + if len(functions) == 0 { + return false + } + + ctx := mod.Context() + assumeType := llvm.FunctionType(ctx.VoidType(), []llvm.Type{ctx.Int1Type()}, false) + assume := mod.NamedFunction("llvm.assume") + if assume.IsNil() { + assume = llvm.AddFunction(mod, "llvm.assume", assumeType) + } + + builder := ctx.NewBuilder() + defer builder.Dispose() + for fn := range functions { + first := fn.EntryBasicBlock().FirstInstruction() + builder.SetInsertPointBefore(first) + unwinding := builder.CreateLoad(ctx.Int1Type(), signal, "unwind.entry") + notUnwinding := builder.CreateNot(unwinding, "") + builder.CreateCall(assumeType, assume, []llvm.Value{notUnwinding}, "") + } + return true +} diff --git a/transform/unwind_test.go b/transform/unwind_test.go new file mode 100644 index 0000000000..13099057cd --- /dev/null +++ b/transform/unwind_test.go @@ -0,0 +1,20 @@ +package transform_test + +import ( + "testing" + + "github.com/tinygo-org/tinygo/transform" + "tinygo.org/x/go-llvm" +) + +func TestUnwindAssumptions(t *testing.T) { + t.Parallel() + testTransform(t, "testdata/unwind", func(mod llvm.Module) { + transform.AddUnwindAssumptions(mod) + po := llvm.NewPassBuilderOptions() + defer po.Dispose() + if err := mod.RunPasses("thinlto-pre-link", llvm.TargetMachine{}, po); err != nil { + t.Fatal(err) + } + }) +} From d76170da2e1ae054b783b2a9ca7bb2f0989fe41a Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:14:23 -0700 Subject: [PATCH 2/6] GNUmakefile: enable recovered wasm package tests Move the standard library packages that were blocked by missing panic recovery into the portable test set. Keep image excluded from bare wasm because its tests require filesystem fixtures. Also enable crypto/ecdsa in the fast WASI suite now that Goexit runs deferred calls. --- GNUmakefile | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index cb80b6625a..5aa40a9c61 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -337,8 +337,10 @@ TEST_PACKAGES_FAST = \ container/heap \ container/list \ container/ring \ + crypto/des \ crypto/ecdsa \ crypto/elliptic \ + crypto/hmac \ crypto/md5 \ crypto/sha1 \ crypto/sha256 \ @@ -365,6 +367,7 @@ TEST_PACKAGES_FAST = \ hash/crc64 \ hash/fnv \ html \ + image \ internal/itoa \ internal/profile \ math \ @@ -375,10 +378,14 @@ TEST_PACKAGES_FAST = \ os \ path \ reflect \ + regexp/syntax \ + strconv \ sync \ testing \ testing/iotest \ text/scanner \ + text/tabwriter \ + text/template/parse \ unicode \ unicode/utf16 \ unicode/utf8 \ @@ -389,21 +396,14 @@ TEST_PACKAGES_FAST = \ # bytes requires mmap # compress/flate appears to hang on wasi # crypto/aes needs reflect.Type.Method(), not yet implemented -# crypto/des fails on wasi, needs panic()/recover() -# crypto/hmac fails on wasi, it exits with a "slice out of range" panic # debug/plan9obj requires os.ReadAt, which is not yet supported on windows # encoding/xml takes a minute on linux and gives a stack overflow on wasi -# image fails on wasi, needs panic()/recover() # io/ioutil requires os.ReadDir, which is not yet supported on windows or wasi -# mime: fails on wasi, needs panic()/recover() +# mime fails on wasi: bufio.Scanner reports an impossible read count # mime/multipart: needs wasip1 syscall.FDFLAG_NONBLOCK # mime/quotedprintable requires syscall.Faccessat # net/mail: needs wasip1 syscall.FDFLAG_NONBLOCK # net/ntextproto: needs wasip1 syscall.FDFLAG_NONBLOCK -# regexp/syntax: fails on wasip1, needs panic()/recover() -# strconv: fails on wasi, needs panic()/recover() -# text/tabwriter: fails on wasi, needs panic()/recover() -# text/template/parse: fails on wasi, needs panic()/recover() # testing/fstest requires os.ReadDir, which is not yet supported on windows or wasi # Additional standard library packages that pass tests on individual platforms @@ -412,13 +412,10 @@ TEST_PACKAGES_LINUX := \ compress/flate \ context \ crypto/aes \ - crypto/des \ crypto/ecdh \ - crypto/hmac \ debug/dwarf \ debug/plan9obj \ encoding/xml \ - image \ io/ioutil \ mime \ mime/multipart \ @@ -427,25 +424,15 @@ TEST_PACKAGES_LINUX := \ net/mail \ net/textproto \ os/user \ - regexp/syntax \ - strconv \ testing/fstest \ - text/tabwriter \ - text/template/parse + $(nil) TEST_PACKAGES_DARWIN := $(TEST_PACKAGES_LINUX) # os/user requires t.Skip() support TEST_PACKAGES_WINDOWS := \ compress/flate \ - crypto/des \ - crypto/hmac \ - image \ mime \ - regexp/syntax \ - strconv \ - text/tabwriter \ - text/template/parse \ $(nil) @@ -461,6 +448,7 @@ TEST_PACKAGES_NONWASM = \ embed/internal/embedtest \ expvar \ go/format \ + image \ os \ testing \ $(nil) @@ -477,11 +465,11 @@ TEST_PACKAGES_BAREMETAL = $(filter-out $(TEST_PACKAGES_NONBAREMETAL), $(TEST_PAC TEST_PACKAGES_NONBAREMETAL = \ $(TEST_PACKAGES_NONWASM) \ math \ + regexp/syntax \ $(nil) TEST_PACKAGES_FAST_WASI = $(filter-out $(TEST_PACKAGES_NOWASI), $(TEST_PACKAGES_FAST)) TEST_PACKAGES_NOWASI = \ - crypto/ecdsa \ $(nil) # Report platforms on which each standard library package is known to pass tests From abd56c9c83b770f84524b7149fb42ea09304090c Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 24 Jul 2026 07:07:00 -0700 Subject: [PATCH 3/6] test: expand explicit panic unwind coverage Exercise explicit return-based unwinding through nested and deferred panics, repanics, indirect calls, scalar and aggregate results, and runtime-generated faults. The fixture runs with the scheduler disabled on each WebAssembly target. --- testdata/recover-explicit.go | 162 ++++++++++++++++++++++++++++++++-- testdata/recover-explicit.txt | 30 ++++++- 2 files changed, 182 insertions(+), 10 deletions(-) diff --git a/testdata/recover-explicit.go b/testdata/recover-explicit.go index 5d32a256f1..9473fdd397 100644 --- a/testdata/recover-explicit.go +++ b/testdata/recover-explicit.go @@ -1,18 +1,164 @@ package main +type pair struct { + first int + second int +} + +var panicMap = map[any]int{} + func main() { - catch() - println("done") + println("# direct panic") + direct() + + println("\n# results") + println("scalar result:", scalarResult()) + result := aggregateResult() + println("aggregate result:", result.first, result.second) + + println("\n# nested panics") + nestedDefer() + nestedPanic() + panicReplace() + deferPanic() + repanic() + + println("\n# runtime panics") + mustRecover("index", func() { + values := []int{1} + println(values[2]) + }) + mustRecover("index from helper", func() { + println(readOutOfBounds([]byte{1})) + }) + mustRecover("slice", func() { + values := []int{1} + _ = values[:2] + }) + mustRecover("type assertion", func() { + var value any = "string" + println(value.(int)) + }) + mustRecover("interface comparison", func() { + var value any = []int{} + println(value == value) + }) + mustRecover("map assignment", func() { + panicMap[[]int{}] = 1 + }) + mustRecover("map lookup", func() { + _ = panicMap[[]int{}] + }) + mustRecover("map delete", func() { + delete(panicMap, []int{}) + }) + mustRecover("nil map", func() { + var values map[string]int + values["key"] = 1 + }) + mustRecover("divide by zero", func() { + var divisor int + println(1 / divisor) + }) + mustRecover("nil pointer", func() { + var pointer *int + println(*pointer) + }) +} + +func direct() { + defer func() { + println("recovered direct:", recover() == "direct panic") + }() + panicHelper("direct panic") + println("unreachable after direct panic") +} + +//go:noinline +func panicHelper(value any) { + panic(value) +} + +func scalarResult() (result int) { + defer func() { + recover() + }() + result = 3 + panicHelper("scalar result panic") + return +} + +func aggregateResult() (result pair) { + defer func() { + recover() + }() + result = pair{1, 2} + panicHelper("aggregate result panic") + return +} + +func nestedDefer() { + defer func() { + println("recovered nested:", recover() == "nested panic") + }() + func() { + defer println("nested defer ran") + panicHelper("nested panic") + }() +} + +func nestedPanic() { + defer func() { + println("recovered outer:", recover() == "outer panic") + }() + defer func() { + println("recovered inner:", recover() == "inner panic") + panicHelper("outer panic") + }() + panicHelper("inner panic") +} + +func panicReplace() { + defer func() { + println("recovered replacement:", recover() == "replacement panic") + }() + defer func() { + panicHelper("replacement panic") + }() + panicHelper("original panic") +} + +func deferPanic() { + defer func() { + println("recovered deferred:", recover() == "deferred panic") + }() + defer panicHelper("deferred panic") +} + +func repanic() { + defer func() { + println("recovered repanic:", recover() == "repanic") + }() + defer func() { + value := recover() + panicHelper(value) + }() + panicHelper("repanic") } -func catch() { +func mustRecover(name string, fn func()) { defer func() { - println("recovered:", recover() == "panic") + if recover() == nil { + println("failed to recover:", name) + } else { + println("recovered:", name) + } }() - call() - println("unreachable after call") + fn() + println("unreachable after:", name) } -func call() { - panic("panic") +//go:noinline +func readOutOfBounds(values []byte) byte { + return values[2] } diff --git a/testdata/recover-explicit.txt b/testdata/recover-explicit.txt index 68d90d4d9a..34377f0e21 100644 --- a/testdata/recover-explicit.txt +++ b/testdata/recover-explicit.txt @@ -1,2 +1,28 @@ -recovered: true -done +# direct panic +recovered direct: true + +# results +scalar result: 3 +aggregate result: 1 2 + +# nested panics +nested defer ran +recovered nested: true +recovered inner: true +recovered outer: true +recovered replacement: true +recovered deferred: true +recovered repanic: true + +# runtime panics +recovered: index +recovered: index from helper +recovered: slice +recovered: type assertion +recovered: interface comparison +recovered: map assignment +recovered: map lookup +recovered: map delete +recovered: nil map +recovered: divide by zero +recovered: nil pointer From 837f39669eecdbb6dcccf0da59e73cd2c3856abc Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:53:33 -0700 Subject: [PATCH 4/6] compiler: test non-panicking calls with defers --- compiler/testdata/defer-cortex-m-qemu.ll | 112 +++++++++++++++++++++++ compiler/testdata/defer.go | 11 +++ 2 files changed, 123 insertions(+) diff --git a/compiler/testdata/defer-cortex-m-qemu.ll b/compiler/testdata/defer-cortex-m-qemu.ll index 550f817f4e..808bab4731 100644 --- a/compiler/testdata/defer-cortex-m-qemu.ll +++ b/compiler/testdata/defer-cortex-m-qemu.ll @@ -130,6 +130,118 @@ declare void @runtime.printint32(i32, ptr) #1 declare void @runtime.printunlock(ptr) #1 +; Function Attrs: nounwind +define hidden void @main.noPanicCall(ptr %context) unnamed_addr #0 { +entry: + call void @runtime.printlock(ptr undef) #4 + call void @runtime.printint32(i32 1, ptr undef) #4 + call void @runtime.printunlock(ptr undef) #4 + ret void +} + +; Function Attrs: nounwind +define hidden void @main.deferNoPanicCall(ptr %context) unnamed_addr #0 { +entry: + %defer.alloca = alloca { i32, ptr }, align 4 + %deferframe.buf = alloca %runtime.deferFrame, align 4 + %deferPtr = getelementptr inbounds nuw i8, ptr %deferframe.buf, i32 24 + %0 = call ptr @llvm.stacksave.p0() + call void @runtime.setupDeferFrame(ptr nonnull %deferframe.buf, ptr %0, ptr undef) #4 + %defer.next = load ptr, ptr %deferPtr, align 4 + store i32 0, ptr %defer.alloca, align 4 + %defer.alloca.repack15 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 4 + store ptr %defer.next, ptr %defer.alloca.repack15, align 4 + store ptr %defer.alloca, ptr %deferPtr, align 4 + %setjmp = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 + %setjmp.result = icmp eq i32 %setjmp, 0 + br i1 %setjmp.result, label %1, label %lpad + +1: ; preds = %entry + call void @main.noPanicCall(ptr undef) + br label %rundefers.block + +rundefers.after: ; preds = %rundefers.end + call void @runtime.destroyDeferFrame(ptr nonnull %deferframe.buf, ptr undef) #4 + ret void + +rundefers.block: ; preds = %1 + br label %rundefers.loophead + +rundefers.loophead: ; preds = %3, %rundefers.block + %2 = load ptr, ptr %deferPtr, align 4 + %stackIsNil = icmp eq ptr %2, null + br i1 %stackIsNil, label %rundefers.end, label %rundefers.loop + +rundefers.loop: ; preds = %rundefers.loophead + %stack.next.gep = getelementptr inbounds nuw i8, ptr %2, i32 4 + %stack.next = load ptr, ptr %stack.next.gep, align 4 + store ptr %stack.next, ptr %deferPtr, align 4 + %callback = load i32, ptr %2, align 4 + switch i32 %callback, label %rundefers.default [ + i32 0, label %rundefers.callback0 + ] + +rundefers.callback0: ; preds = %rundefers.loop + %setjmp1 = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 + %setjmp.result2 = icmp eq i32 %setjmp1, 0 + br i1 %setjmp.result2, label %3, label %lpad + +3: ; preds = %rundefers.callback0 + call void @"main.deferNoPanicCall$1"(ptr undef) + br label %rundefers.loophead + +rundefers.default: ; preds = %rundefers.loop + unreachable + +rundefers.end: ; preds = %rundefers.loophead + br label %rundefers.after + +recover: ; preds = %rundefers.end3 + call void @runtime.destroyDeferFrame(ptr nonnull %deferframe.buf, ptr undef) #4 + ret void + +lpad: ; preds = %rundefers.callback012, %rundefers.callback0, %entry + br label %rundefers.loophead6 + +rundefers.loophead6: ; preds = %5, %lpad + %4 = load ptr, ptr %deferPtr, align 4 + %stackIsNil7 = icmp eq ptr %4, null + br i1 %stackIsNil7, label %rundefers.end3, label %rundefers.loop5 + +rundefers.loop5: ; preds = %rundefers.loophead6 + %stack.next.gep8 = getelementptr inbounds nuw i8, ptr %4, i32 4 + %stack.next9 = load ptr, ptr %stack.next.gep8, align 4 + store ptr %stack.next9, ptr %deferPtr, align 4 + %callback11 = load i32, ptr %4, align 4 + switch i32 %callback11, label %rundefers.default4 [ + i32 0, label %rundefers.callback012 + ] + +rundefers.callback012: ; preds = %rundefers.loop5 + %setjmp13 = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 + %setjmp.result14 = icmp eq i32 %setjmp13, 0 + br i1 %setjmp.result14, label %5, label %lpad + +5: ; preds = %rundefers.callback012 + call void @"main.deferNoPanicCall$1"(ptr undef) + br label %rundefers.loophead6 + +rundefers.default4: ; preds = %rundefers.loop5 + unreachable + +rundefers.end3: ; preds = %rundefers.loophead6 + br label %recover +} + +; Function Attrs: nounwind +define internal void @"main.deferNoPanicCall$1"(ptr %context) unnamed_addr #0 { +entry: + call void @runtime.printlock(ptr undef) #4 + call void @runtime.printint32(i32 2, ptr undef) #4 + call void @runtime.printunlock(ptr undef) #4 + ret void +} + ; Function Attrs: nounwind define hidden void @main.deferMultiple(ptr %context) unnamed_addr #0 { entry: diff --git a/compiler/testdata/defer.go b/compiler/testdata/defer.go index b93d304991..de35c8ae3a 100644 --- a/compiler/testdata/defer.go +++ b/compiler/testdata/defer.go @@ -9,6 +9,17 @@ func deferSimple() { external() } +func noPanicCall() { + print(1) +} + +func deferNoPanicCall() { + defer func() { + print(2) + }() + noPanicCall() +} + func deferMultiple() { defer func() { print(3) From 98caa2a9080b7b321cd2bdf4c13bb0512324b4ac Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:16:21 -0700 Subject: [PATCH 5/6] compiler: skip unwinding for safe calls --- compiler/calls.go | 180 +++++++++++++++------ compiler/compiler.go | 9 +- compiler/defer.go | 6 +- compiler/testdata/defer-cortex-m-qemu.ll | 191 ++++++++--------------- 4 files changed, 204 insertions(+), 182 deletions(-) diff --git a/compiler/calls.go b/compiler/calls.go index cd2f2646a9..ac537f6792 100644 --- a/compiler/calls.go +++ b/compiler/calls.go @@ -49,7 +49,7 @@ func (b *builder) createRuntimeCallCommon(fnName string, args []llvm.Value, name if isInvoke { // chanSend is the only panic-capable runtime operation that can also // suspend the task. - return b.createInvokeWithSuspend(fnType, llvmFn, args, name, fnName == "chanSend") + return b.createInvokeWithAnalysis(fnType, llvmFn, args, name, true, fnName == "chanSend") } return b.createCall(fnType, llvmFn, args, name) } @@ -106,29 +106,33 @@ func (b *builder) expandFormalParams(args []llvm.Value) []llvm.Value { return expanded } -// createInvoke is like createCall but continues execution at the landing pad if -// the call resulted in a panic. -func (b *builder) createInvoke(fnType llvm.Type, fn llvm.Value, args []llvm.Value, name string) llvm.Value { - return b.createInvokeWithSuspend(fnType, fn, args, name, false) +// createInvoke emits a Go call, adding unwind handling only when the static +// call graph indicates the call can unwind. +func (b *builder) createInvoke(fnType llvm.Type, fn llvm.Value, args []llvm.Value, name string, call *ssa.CallCommon) llvm.Value { + properties := b.callPropertiesFor(call) + return b.createInvokeWithAnalysis(fnType, fn, args, name, properties&callMayUnwind != 0, properties&callMaySuspend != 0) } -func (b *builder) createInvokeWithSuspend(fnType llvm.Type, fn llvm.Value, args []llvm.Value, name string, maySuspend bool) llvm.Value { +func (b *builder) createInvokeWithAnalysis(fnType llvm.Type, fn llvm.Value, args []llvm.Value, name string, mayUnwind, maySuspend bool) llvm.Value { + if !mayUnwind { + return b.createCall(fnType, fn, args, name) + } if b.usesReturnUnwind() { - var call llvm.Value + var result llvm.Value if b.usesAsyncifyUnwind() && b.hasDeferFrame() { if maySuspend { - call = b.createAsyncifySuspendInvoke(fnType, fn, args, name) + result = b.createAsyncifySuspendInvoke(fnType, fn, args, name) } else { - call = b.createAsyncifyNoSuspendInvoke(fnType, fn, args, name) + result = b.createAsyncifyNoSuspendInvoke(fnType, fn, args, name) } } else { - call = b.createCall(fnType, fn, args, name) + result = b.createCall(fnType, fn, args, name) } if b.needsPostCallUnwindCheck() { b.createUnwindCheck(true) } - return call + return result } if b.hasDeferFrame() { b.createInvokeCheckpoint() @@ -309,74 +313,146 @@ func (b *builder) emitAsyncifyCatchReturn(builder llvm.Builder, fnType llvm.Type builder.CreateRet(phi) } -type suspendState uint8 +type callProperties uint8 const ( - suspendUnknown suspendState = iota - suspendVisiting - suspendNo - suspendYes + callMaySuspend callProperties = 1 << iota + callMayUnwind ) -func (b *builder) functionMaySuspend(fn *ssa.Function) bool { - // Calls through unknown targets are conservatively treated as suspending. - // Cycles are also considered suspending so recursive call graphs cannot - // hide a path to task.Pause. - switch b.maySuspend[fn] { - case suspendVisiting: - return true - case suspendNo: - return false - case suspendYes: - return true +type functionCallProperties struct { + properties callProperties + visiting bool + complete bool +} + +func (b *builder) functionCallProperties(fn *ssa.Function) callProperties { + analysis := b.callProperties[fn] + if analysis.complete { + return analysis.properties + } + if analysis.visiting || len(fn.Blocks) == 0 { + return callMaySuspend | callMayUnwind } - b.maySuspend[fn] = suspendVisiting + analysis.visiting = true + b.callProperties[fn] = analysis + + var properties callProperties if fn.Pkg != nil && fn.Pkg.Pkg.Path() == "internal/task" && fn.Name() == "Pause" { - b.maySuspend[fn] = suspendYes - return true - } - if len(fn.Blocks) == 0 { - b.maySuspend[fn] = suspendYes - return true + properties |= callMaySuspend } for _, block := range fn.Blocks { if block == nil { continue } for _, instruction := range block.Instrs { - switch instruction := instruction.(type) { - case *ssa.Send, *ssa.Select, *ssa.Next: - b.maySuspend[fn] = suspendYes - return true - case *ssa.UnOp: - if instruction.Op == token.ARROW { - b.maySuspend[fn] = suspendYes - return true - } - } + properties |= instructionCallProperties(instruction) call, ok := instruction.(ssa.CallInstruction) if !ok { continue } common := call.Common() - if _, ok := common.Value.(*ssa.Builtin); ok { + if builtin, ok := common.Value.(*ssa.Builtin); ok { + switch builtin.Name() { + case "close", "delete", "panic": + properties |= callMayUnwind + } continue } callee := common.StaticCallee() - if callee == nil || b.functionMaySuspend(callee) { - b.maySuspend[fn] = suspendYes - return true + if callee == nil { + properties |= callMaySuspend | callMayUnwind + } else { + properties |= b.functionCallProperties(callee) + } + if properties == callMaySuspend|callMayUnwind { + break } } } - b.maySuspend[fn] = suspendNo - return false + + b.callProperties[fn] = functionCallProperties{ + properties: properties, + complete: true, + } + return properties } -func (b *builder) callMaySuspend(call *ssa.CallCommon) bool { +func (b *builder) callPropertiesFor(call *ssa.CallCommon) callProperties { callee := call.StaticCallee() - return callee == nil || b.functionMaySuspend(callee) + if callee == nil { + return callMaySuspend | callMayUnwind + } + return b.functionCallProperties(callee) +} + +func (b *builder) functionMaySuspend(fn *ssa.Function) bool { + return b.functionCallProperties(fn)&callMaySuspend != 0 +} + +func (b *builder) functionMayUnwind(fn *ssa.Function) bool { + return b.functionCallProperties(fn)&callMayUnwind != 0 +} + +func instructionCallProperties(instruction ssa.Instruction) callProperties { + switch instruction := instruction.(type) { + case *ssa.Send: + return callMaySuspend | callMayUnwind + case *ssa.Next: + return callMaySuspend + case *ssa.Select: + return callMaySuspend | callMayUnwind + case *ssa.FieldAddr, *ssa.Index, *ssa.IndexAddr, *ssa.Lookup, + *ssa.MakeChan, *ssa.MakeSlice, *ssa.MapUpdate, *ssa.Panic, + *ssa.Slice, *ssa.SliceToArrayPointer, *ssa.Store, *ssa.TypeAssert: + return callMayUnwind + case *ssa.BinOp: + if binOpMayUnwind(instruction) { + return callMayUnwind + } + case *ssa.UnOp: + var properties callProperties + if instruction.Op == token.ARROW { + properties |= callMaySuspend + } + if instruction.Op == token.MUL { + properties |= callMayUnwind + } + return properties + } + return 0 +} + +func binOpMayUnwind(instruction *ssa.BinOp) bool { + switch instruction.Op { + case token.QUO, token.REM: + basic, ok := instruction.X.Type().Underlying().(*types.Basic) + return ok && basic.Info()&types.IsInteger != 0 + case token.SHL, token.SHR: + basic, ok := instruction.Y.Type().Underlying().(*types.Basic) + return ok && basic.Info()&types.IsUnsigned == 0 + case token.EQL, token.NEQ: + return typeMayPanicOnCompare(instruction.X.Type()) + default: + return false + } +} + +func typeMayPanicOnCompare(typ types.Type) bool { + switch typ := typ.Underlying().(type) { + case *types.Interface: + return true + case *types.Array: + return typeMayPanicOnCompare(typ.Elem()) + case *types.Struct: + for i := 0; i < typ.NumFields(); i++ { + if typeMayPanicOnCompare(typ.Field(i).Type()) { + return true + } + } + } + return false } func (b *builder) inFunctionBody() bool { diff --git a/compiler/compiler.go b/compiler/compiler.go index ba251acf44..97b90535b2 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -88,7 +88,7 @@ type compilerContext struct { program *ssa.Program diagnostics []error functionInfos map[*ssa.Function]functionInfo - maySuspend map[*ssa.Function]suspendState + callProperties map[*ssa.Function]functionCallProperties asyncifyCatchers map[llvm.Type]llvm.Value astComments map[string]*ast.CommentGroup embedGlobals map[string][]*loader.EmbedFile @@ -110,7 +110,7 @@ func newCompilerContext(moduleName string, machine llvm.TargetMachine, config *C machine: machine, targetData: machine.CreateTargetData(), functionInfos: map[*ssa.Function]functionInfo{}, - maySuspend: map[*ssa.Function]suspendState{}, + callProperties: map[*ssa.Function]functionCallProperties{}, asyncifyCatchers: map[llvm.Type]llvm.Value{}, astComments: map[string]*ast.CommentGroup{}, } @@ -2348,12 +2348,11 @@ func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error) } if !exported { - maySuspend := b.callMaySuspend(instr) if resultType, indirectResult := b.hasIndirectResult(instr.Signature()); indirectResult { result := b.createIndirectStorage(resultType, "call.result") params = append([]llvm.Value{result}, params...) params = append(params, context) - b.createInvokeWithSuspend(calleeType, callee, params, "", maySuspend) + b.createInvoke(calleeType, callee, params, "", instr) return result, nil } // This function takes a context parameter. @@ -2361,7 +2360,7 @@ func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error) params = append(params, context) } - return b.createInvokeWithSuspend(calleeType, callee, params, "", b.callMaySuspend(instr)), nil + return b.createInvoke(calleeType, callee, params, "", instr), nil } // getValue returns the LLVM value of a constant, function value, global, or diff --git a/compiler/defer.go b/compiler/defer.go index 28acf399d3..b7c0c0dbb1 100644 --- a/compiler/defer.go +++ b/compiler/defer.go @@ -661,7 +661,7 @@ func (b *builder) createRunDefers() { } forwardParams = b.prependIndirectResult(callback.Signature(), false, forwardParams, "defer.result") - b.createInvokeWithSuspend(fnType, fnPtr, forwardParams, "", true) + b.createInvokeWithAnalysis(fnType, fnPtr, forwardParams, "", true, true) case *ssa.Function: // Direct call. @@ -686,7 +686,7 @@ func (b *builder) createRunDefers() { // Call real function. fnType, fn := b.getFunction(callback) - b.createInvokeWithSuspend(fnType, fn, forwardParams, "", b.functionMaySuspend(callback)) + b.createInvokeWithAnalysis(fnType, fn, forwardParams, "", b.functionMayUnwind(callback), b.functionMaySuspend(callback)) case *ssa.MakeClosure: // Get the real defer struct type and cast to it. @@ -702,7 +702,7 @@ func (b *builder) createRunDefers() { // Call deferred function. fnType, llvmFn := b.getFunction(fn) forwardParams = b.prependIndirectResult(fn.Signature, false, forwardParams, "defer.result") - b.createInvokeWithSuspend(fnType, llvmFn, forwardParams, "", b.functionMaySuspend(fn)) + b.createInvokeWithAnalysis(fnType, llvmFn, forwardParams, "", b.functionMayUnwind(fn), b.functionMaySuspend(fn)) case *ssa.Builtin: db := b.deferBuiltinFuncs[callback] diff --git a/compiler/testdata/defer-cortex-m-qemu.ll b/compiler/testdata/defer-cortex-m-qemu.ll index 808bab4731..02a6ed965a 100644 --- a/compiler/testdata/defer-cortex-m-qemu.ll +++ b/compiler/testdata/defer-cortex-m-qemu.ll @@ -24,8 +24,8 @@ entry: call void @runtime.setupDeferFrame(ptr nonnull %deferframe.buf, ptr %0, ptr undef) #4 %defer.next = load ptr, ptr %deferPtr, align 4 store i32 0, ptr %defer.alloca, align 4 - %defer.alloca.repack15 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 4 - store ptr %defer.next, ptr %defer.alloca.repack15, align 4 + %defer.alloca.repack11 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 4 + store ptr %defer.next, ptr %defer.alloca.repack11, align 4 store ptr %defer.alloca, ptr %deferPtr, align 4 %setjmp = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 %setjmp.result = icmp eq i32 %setjmp, 0 @@ -42,7 +42,7 @@ rundefers.after: ; preds = %rundefers.end rundefers.block: ; preds = %1 br label %rundefers.loophead -rundefers.loophead: ; preds = %3, %rundefers.block +rundefers.loophead: ; preds = %rundefers.callback0, %rundefers.block %2 = load ptr, ptr %deferPtr, align 4 %stackIsNil = icmp eq ptr %2, null br i1 %stackIsNil, label %rundefers.end, label %rundefers.loop @@ -57,11 +57,6 @@ rundefers.loop: ; preds = %rundefers.loophead ] rundefers.callback0: ; preds = %rundefers.loop - %setjmp1 = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 - %setjmp.result2 = icmp eq i32 %setjmp1, 0 - br i1 %setjmp.result2, label %3, label %lpad - -3: ; preds = %rundefers.callback0 call void @"main.deferSimple$1"(ptr undef) br label %rundefers.loophead @@ -71,40 +66,35 @@ rundefers.default: ; preds = %rundefers.loop rundefers.end: ; preds = %rundefers.loophead br label %rundefers.after -recover: ; preds = %rundefers.end3 +recover: ; preds = %rundefers.end1 call void @runtime.destroyDeferFrame(ptr nonnull %deferframe.buf, ptr undef) #4 ret void -lpad: ; preds = %rundefers.callback012, %rundefers.callback0, %entry - br label %rundefers.loophead6 +lpad: ; preds = %entry + br label %rundefers.loophead4 -rundefers.loophead6: ; preds = %5, %lpad - %4 = load ptr, ptr %deferPtr, align 4 - %stackIsNil7 = icmp eq ptr %4, null - br i1 %stackIsNil7, label %rundefers.end3, label %rundefers.loop5 +rundefers.loophead4: ; preds = %rundefers.callback010, %lpad + %3 = load ptr, ptr %deferPtr, align 4 + %stackIsNil5 = icmp eq ptr %3, null + br i1 %stackIsNil5, label %rundefers.end1, label %rundefers.loop3 -rundefers.loop5: ; preds = %rundefers.loophead6 - %stack.next.gep8 = getelementptr inbounds nuw i8, ptr %4, i32 4 - %stack.next9 = load ptr, ptr %stack.next.gep8, align 4 - store ptr %stack.next9, ptr %deferPtr, align 4 - %callback11 = load i32, ptr %4, align 4 - switch i32 %callback11, label %rundefers.default4 [ - i32 0, label %rundefers.callback012 +rundefers.loop3: ; preds = %rundefers.loophead4 + %stack.next.gep6 = getelementptr inbounds nuw i8, ptr %3, i32 4 + %stack.next7 = load ptr, ptr %stack.next.gep6, align 4 + store ptr %stack.next7, ptr %deferPtr, align 4 + %callback9 = load i32, ptr %3, align 4 + switch i32 %callback9, label %rundefers.default2 [ + i32 0, label %rundefers.callback010 ] -rundefers.callback012: ; preds = %rundefers.loop5 - %setjmp13 = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 - %setjmp.result14 = icmp eq i32 %setjmp13, 0 - br i1 %setjmp.result14, label %5, label %lpad - -5: ; preds = %rundefers.callback012 +rundefers.callback010: ; preds = %rundefers.loop3 call void @"main.deferSimple$1"(ptr undef) - br label %rundefers.loophead6 + br label %rundefers.loophead4 -rundefers.default4: ; preds = %rundefers.loop5 +rundefers.default2: ; preds = %rundefers.loop3 unreachable -rundefers.end3: ; preds = %rundefers.loophead6 +rundefers.end1: ; preds = %rundefers.loophead4 br label %recover } @@ -149,14 +139,9 @@ entry: call void @runtime.setupDeferFrame(ptr nonnull %deferframe.buf, ptr %0, ptr undef) #4 %defer.next = load ptr, ptr %deferPtr, align 4 store i32 0, ptr %defer.alloca, align 4 - %defer.alloca.repack15 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 4 - store ptr %defer.next, ptr %defer.alloca.repack15, align 4 + %defer.alloca.repack11 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 4 + store ptr %defer.next, ptr %defer.alloca.repack11, align 4 store ptr %defer.alloca, ptr %deferPtr, align 4 - %setjmp = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 - %setjmp.result = icmp eq i32 %setjmp, 0 - br i1 %setjmp.result, label %1, label %lpad - -1: ; preds = %entry call void @main.noPanicCall(ptr undef) br label %rundefers.block @@ -164,29 +149,24 @@ rundefers.after: ; preds = %rundefers.end call void @runtime.destroyDeferFrame(ptr nonnull %deferframe.buf, ptr undef) #4 ret void -rundefers.block: ; preds = %1 +rundefers.block: ; preds = %entry br label %rundefers.loophead -rundefers.loophead: ; preds = %3, %rundefers.block - %2 = load ptr, ptr %deferPtr, align 4 - %stackIsNil = icmp eq ptr %2, null +rundefers.loophead: ; preds = %rundefers.callback0, %rundefers.block + %1 = load ptr, ptr %deferPtr, align 4 + %stackIsNil = icmp eq ptr %1, null br i1 %stackIsNil, label %rundefers.end, label %rundefers.loop rundefers.loop: ; preds = %rundefers.loophead - %stack.next.gep = getelementptr inbounds nuw i8, ptr %2, i32 4 + %stack.next.gep = getelementptr inbounds nuw i8, ptr %1, i32 4 %stack.next = load ptr, ptr %stack.next.gep, align 4 store ptr %stack.next, ptr %deferPtr, align 4 - %callback = load i32, ptr %2, align 4 + %callback = load i32, ptr %1, align 4 switch i32 %callback, label %rundefers.default [ i32 0, label %rundefers.callback0 ] rundefers.callback0: ; preds = %rundefers.loop - %setjmp1 = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 - %setjmp.result2 = icmp eq i32 %setjmp1, 0 - br i1 %setjmp.result2, label %3, label %lpad - -3: ; preds = %rundefers.callback0 call void @"main.deferNoPanicCall$1"(ptr undef) br label %rundefers.loophead @@ -196,40 +176,27 @@ rundefers.default: ; preds = %rundefers.loop rundefers.end: ; preds = %rundefers.loophead br label %rundefers.after -recover: ; preds = %rundefers.end3 - call void @runtime.destroyDeferFrame(ptr nonnull %deferframe.buf, ptr undef) #4 +recover: ; preds = %rundefers.end1 ret void -lpad: ; preds = %rundefers.callback012, %rundefers.callback0, %entry - br label %rundefers.loophead6 +lpad: ; No predecessors! + br label %rundefers.loophead4 -rundefers.loophead6: ; preds = %5, %lpad - %4 = load ptr, ptr %deferPtr, align 4 - %stackIsNil7 = icmp eq ptr %4, null - br i1 %stackIsNil7, label %rundefers.end3, label %rundefers.loop5 +rundefers.loophead4: ; preds = %rundefers.callback010, %lpad + br i1 poison, label %rundefers.end1, label %rundefers.loop3 -rundefers.loop5: ; preds = %rundefers.loophead6 - %stack.next.gep8 = getelementptr inbounds nuw i8, ptr %4, i32 4 - %stack.next9 = load ptr, ptr %stack.next.gep8, align 4 - store ptr %stack.next9, ptr %deferPtr, align 4 - %callback11 = load i32, ptr %4, align 4 - switch i32 %callback11, label %rundefers.default4 [ - i32 0, label %rundefers.callback012 +rundefers.loop3: ; preds = %rundefers.loophead4 + switch i32 poison, label %rundefers.default2 [ + i32 0, label %rundefers.callback010 ] -rundefers.callback012: ; preds = %rundefers.loop5 - %setjmp13 = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 - %setjmp.result14 = icmp eq i32 %setjmp13, 0 - br i1 %setjmp.result14, label %5, label %lpad - -5: ; preds = %rundefers.callback012 - call void @"main.deferNoPanicCall$1"(ptr undef) - br label %rundefers.loophead6 +rundefers.callback010: ; preds = %rundefers.loop3 + br label %rundefers.loophead4 -rundefers.default4: ; preds = %rundefers.loop5 +rundefers.default2: ; preds = %rundefers.loop3 unreachable -rundefers.end3: ; preds = %rundefers.loophead6 +rundefers.end1: ; preds = %rundefers.loophead4 br label %recover } @@ -253,12 +220,12 @@ entry: call void @runtime.setupDeferFrame(ptr nonnull %deferframe.buf, ptr %0, ptr undef) #4 %defer.next = load ptr, ptr %deferPtr, align 4 store i32 0, ptr %defer.alloca, align 4 - %defer.alloca.repack22 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 4 - store ptr %defer.next, ptr %defer.alloca.repack22, align 4 + %defer.alloca.repack14 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 4 + store ptr %defer.next, ptr %defer.alloca.repack14, align 4 store ptr %defer.alloca, ptr %deferPtr, align 4 store i32 1, ptr %defer.alloca2, align 4 - %defer.alloca2.repack24 = getelementptr inbounds nuw i8, ptr %defer.alloca2, i32 4 - store ptr %defer.alloca, ptr %defer.alloca2.repack24, align 4 + %defer.alloca2.repack16 = getelementptr inbounds nuw i8, ptr %defer.alloca2, i32 4 + store ptr %defer.alloca, ptr %defer.alloca2.repack16, align 4 store ptr %defer.alloca2, ptr %deferPtr, align 4 %setjmp = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 %setjmp.result = icmp eq i32 %setjmp, 0 @@ -275,7 +242,7 @@ rundefers.after: ; preds = %rundefers.end rundefers.block: ; preds = %1 br label %rundefers.loophead -rundefers.loophead: ; preds = %4, %3, %rundefers.block +rundefers.loophead: ; preds = %rundefers.callback1, %rundefers.callback0, %rundefers.block %2 = load ptr, ptr %deferPtr, align 4 %stackIsNil = icmp eq ptr %2, null br i1 %stackIsNil, label %rundefers.end, label %rundefers.loop @@ -291,20 +258,10 @@ rundefers.loop: ; preds = %rundefers.loophead ] rundefers.callback0: ; preds = %rundefers.loop - %setjmp3 = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 - %setjmp.result4 = icmp eq i32 %setjmp3, 0 - br i1 %setjmp.result4, label %3, label %lpad - -3: ; preds = %rundefers.callback0 call void @"main.deferMultiple$1"(ptr undef) br label %rundefers.loophead rundefers.callback1: ; preds = %rundefers.loop - %setjmp5 = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 - %setjmp.result6 = icmp eq i32 %setjmp5, 0 - br i1 %setjmp.result6, label %4, label %lpad - -4: ; preds = %rundefers.callback1 call void @"main.deferMultiple$2"(ptr undef) br label %rundefers.loophead @@ -314,50 +271,40 @@ rundefers.default: ; preds = %rundefers.loop rundefers.end: ; preds = %rundefers.loophead br label %rundefers.after -recover: ; preds = %rundefers.end7 +recover: ; preds = %rundefers.end3 call void @runtime.destroyDeferFrame(ptr nonnull %deferframe.buf, ptr undef) #4 ret void -lpad: ; preds = %rundefers.callback119, %rundefers.callback016, %rundefers.callback1, %rundefers.callback0, %entry - br label %rundefers.loophead10 - -rundefers.loophead10: ; preds = %7, %6, %lpad - %5 = load ptr, ptr %deferPtr, align 4 - %stackIsNil11 = icmp eq ptr %5, null - br i1 %stackIsNil11, label %rundefers.end7, label %rundefers.loop9 - -rundefers.loop9: ; preds = %rundefers.loophead10 - %stack.next.gep12 = getelementptr inbounds nuw i8, ptr %5, i32 4 - %stack.next13 = load ptr, ptr %stack.next.gep12, align 4 - store ptr %stack.next13, ptr %deferPtr, align 4 - %callback15 = load i32, ptr %5, align 4 - switch i32 %callback15, label %rundefers.default8 [ - i32 0, label %rundefers.callback016 - i32 1, label %rundefers.callback119 - ] +lpad: ; preds = %entry + br label %rundefers.loophead6 -rundefers.callback016: ; preds = %rundefers.loop9 - %setjmp17 = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 - %setjmp.result18 = icmp eq i32 %setjmp17, 0 - br i1 %setjmp.result18, label %6, label %lpad +rundefers.loophead6: ; preds = %rundefers.callback113, %rundefers.callback012, %lpad + %3 = load ptr, ptr %deferPtr, align 4 + %stackIsNil7 = icmp eq ptr %3, null + br i1 %stackIsNil7, label %rundefers.end3, label %rundefers.loop5 -6: ; preds = %rundefers.callback016 - call void @"main.deferMultiple$1"(ptr undef) - br label %rundefers.loophead10 +rundefers.loop5: ; preds = %rundefers.loophead6 + %stack.next.gep8 = getelementptr inbounds nuw i8, ptr %3, i32 4 + %stack.next9 = load ptr, ptr %stack.next.gep8, align 4 + store ptr %stack.next9, ptr %deferPtr, align 4 + %callback11 = load i32, ptr %3, align 4 + switch i32 %callback11, label %rundefers.default4 [ + i32 0, label %rundefers.callback012 + i32 1, label %rundefers.callback113 + ] -rundefers.callback119: ; preds = %rundefers.loop9 - %setjmp20 = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5 - %setjmp.result21 = icmp eq i32 %setjmp20, 0 - br i1 %setjmp.result21, label %7, label %lpad +rundefers.callback012: ; preds = %rundefers.loop5 + call void @"main.deferMultiple$1"(ptr undef) + br label %rundefers.loophead6 -7: ; preds = %rundefers.callback119 +rundefers.callback113: ; preds = %rundefers.loop5 call void @"main.deferMultiple$2"(ptr undef) - br label %rundefers.loophead10 + br label %rundefers.loophead6 -rundefers.default8: ; preds = %rundefers.loop9 +rundefers.default4: ; preds = %rundefers.loop5 unreachable -rundefers.end7: ; preds = %rundefers.loophead10 +rundefers.end3: ; preds = %rundefers.loophead6 br label %recover } From 13037fac28a26d948954008c604af5252466f6c4 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:37:07 -0700 Subject: [PATCH 6/6] compiler: conservatively handle new SSA instructions --- compiler/calls.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/compiler/calls.go b/compiler/calls.go index ac537f6792..fb3f8a8486 100644 --- a/compiler/calls.go +++ b/compiler/calls.go @@ -397,6 +397,11 @@ func (b *builder) functionMayUnwind(fn *ssa.Function) bool { func instructionCallProperties(instruction ssa.Instruction) callProperties { switch instruction := instruction.(type) { + case *ssa.Alloc, *ssa.Call, *ssa.ChangeInterface, *ssa.ChangeType, + *ssa.Convert, *ssa.DebugRef, *ssa.Defer, *ssa.Extract, *ssa.Field, + *ssa.Go, *ssa.If, *ssa.Jump, *ssa.MakeClosure, *ssa.MakeInterface, + *ssa.MakeMap, *ssa.Phi, *ssa.Range, *ssa.Return, *ssa.RunDefers: + return 0 case *ssa.Send: return callMaySuspend | callMayUnwind case *ssa.Next: @@ -421,7 +426,9 @@ func instructionCallProperties(instruction ssa.Instruction) callProperties { } return properties } - return 0 + // New SSA instructions must be treated conservatively until their lowering + // is audited for suspension and unwind paths. + return callMaySuspend | callMayUnwind } func binOpMayUnwind(instruction *ssa.BinOp) bool {