From fb6ace6301f9c300d57425587f0cbcb56670f348 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 01/11] 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 | 61 ++++ transform/transform_test.go | 1 + transform/unwind.go | 53 +++ transform/unwind_test.go | 20 ++ 48 files changed, 1410 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 974ddc2a37..bb50aebcb3 100644 --- a/builder/build.go +++ b/builder/build.go @@ -217,6 +217,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 51197bac34..87701efa6d 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 d9188790cd..8f9a79d564 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -391,6 +391,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 4be9d9e4fe..67bca09a32 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 nocapture writeonly, ptr noalias 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 nocapture writeonly, i8, i32, i1 immarg) #7 +declare void @llvm.memset.p0.i32(ptr nocapture writeonly, 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 nocapture) #8 +declare void @llvm.lifetime.start.p0(ptr nocapture) #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 nocapture) #8 +declare void @llvm.lifetime.end.p0(ptr nocapture) #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 986e8c176a..3644270e4a 100644 --- a/main.go +++ b/main.go @@ -1766,6 +1766,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") @@ -1905,6 +1906,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..d00cf7bfa2 --- /dev/null +++ b/transform/testdata/unwind.out.ll @@ -0,0 +1,61 @@ +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 + +; Function Attrs: mustprogress nofree norecurse nosync nounwind willreturn memory(readwrite, argmem: none, inaccessiblemem: write, target_mem0: none, target_mem1: none) +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 +} + +; Function Attrs: mustprogress nofree norecurse nosync nounwind willreturn memory(readwrite, argmem: none, inaccessiblemem: write, target_mem0: none, target_mem1: none) +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 +} + +; Function Attrs: mustprogress nofree norecurse nosync nounwind willreturn memory(read, argmem: none, inaccessiblemem: none, target_mem0: none, target_mem1: none) +define i1 @runtime.unwindPending() local_unnamed_addr #1 { +entry: + %unwind = load i1, ptr @runtime.unwindPendingSignal, align 1 + ret i1 %unwind +} + +; Function Attrs: mustprogress nocallback nofree nosync nounwind willreturn memory(inaccessiblemem: write) +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 89fda55e29..743fa92e60 100644 --- a/transform/transform_test.go +++ b/transform/transform_test.go @@ -202,6 +202,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 6178ec73a0ccf2afd59c2a8d6d2b5c6830ddb29d 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 02/11] 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 ee2e70408064878f034c7b59531b82f20ef07b8b 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 03/11] 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 c1645fa934eb3f13ea2691e8e43d3b5907bb52b9 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 04/11] 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 1bcba2027900d25d62f9595f7583e235c272c335 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 05/11] 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 87701efa6d..1b35be10dd 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 fc06de6fbdbc40b3df60fb78197ca2c34c9b5dff 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 06/11] 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 { From a860b9b435afb94732e05a4370fbba71a547b70e Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:18:48 -0700 Subject: [PATCH 07/11] runtime: preserve Wasm panic traceback --- compiler/calls.go | 64 +++++++- compiler/compiler.go | 2 + compiler/testdata/large.ll | 213 ++++++++++++++----------- main_test.go | 36 +++++ src/internal/task/task_asyncify.go | 53 ++++++ src/internal/task/task_asyncify_wasm.S | 35 ++++ src/runtime/panic.go | 8 + src/runtime/panic_unwind_asyncify.go | 31 +++- src/runtime/panic_unwind_explicit.go | 7 + src/runtime/panic_unwind_none.go | 7 + src/runtime/panic_unwind_setjmp.go | 7 + testdata/panic-traceback.go | 23 +++ 12 files changed, 389 insertions(+), 97 deletions(-) create mode 100644 testdata/panic-traceback.go diff --git a/compiler/calls.go b/compiler/calls.go index fb3f8a8486..0685725ca0 100644 --- a/compiler/calls.go +++ b/compiler/calls.go @@ -170,7 +170,8 @@ func (b *builder) createAsyncifyNoSuspendInvoke(fnType llvm.Type, fn llvm.Value, 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) + replay := b.createAsyncifyPanicReplay(wrapperType) + b.emitAsyncifyCatchReturn(builder, fnType, result, entry, unwinding, replay, wrapper) builder.Dispose() wrapperArgs := expanded @@ -278,7 +279,8 @@ func (b *builder) createAsyncifySuspendInvoke(fnType llvm.Type, fn llvm.Value, a 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) + replay := b.createAsyncifyPanicReplay(wrapperType) + b.emitAsyncifyCatchReturn(builder, fnType, result, entry, unwinding, replay, catchWrapper) builder.Dispose() // Keep the caller-to-catcher edge opaque so Binaryen instruments the @@ -292,7 +294,55 @@ func (b *builder) createAsyncifySuspendInvoke(fnType llvm.Type, fn llvm.Value, a 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) { +func (b *builder) createAsyncifyPanicReplay(catcherType llvm.Type) llvm.Value { + if replay, ok := b.asyncifyReplays[catcherType]; ok { + return replay + } + + replayType := llvm.FunctionType(b.ctx.VoidType(), []llvm.Type{b.uintptrType}, false) + replay := llvm.AddFunction(b.mod, "tinygo.asyncify.panicreplay."+strconv.Itoa(len(b.asyncifyReplays)), replayType) + replay.SetLinkage(llvm.InternalLinkage) + replay.AddFunctionAttr(b.ctx.CreateEnumAttribute(llvm.AttributeKindID("noinline"), 0)) + b.asyncifyReplays[catcherType] = replay + + builder := b.ctx.NewBuilder() + defer builder.Dispose() + entry := b.ctx.AddBasicBlock(replay, "entry") + builder.SetInsertPointAtEnd(entry) + + dataType, dataFn := b.getRuntimeFunction("panicRewindData") + data := builder.CreateCall(dataType, dataFn, []llvm.Value{llvm.Undef(b.dataPtrType)}, "") + stackType, stackFn := b.getRuntimeFunction("panicRewindStackPointer") + stackPointer := builder.CreateCall(stackType, stackFn, []llvm.Value{llvm.Undef(b.dataPtrType)}, "") + targetPointer := builder.CreateIntToPtr(replay.Param(0), b.dataPtrType, "") + setStackType, setStackFn := b.getRuntimeFunction("setPanicRewindStackPointer") + builder.CreateCall(setStackType, setStackFn, []llvm.Value{stackPointer, llvm.Undef(b.dataPtrType)}, "") + + rewinding := b.mod.NamedGlobal("tinygo_rewinding") + if rewinding.IsNil() { + rewinding = llvm.AddGlobal(b.mod, b.ctx.Int8Type(), "tinygo_rewinding") + rewinding.SetLinkage(llvm.ExternalLinkage) + } + builder.CreateStore(llvm.ConstInt(b.ctx.Int8Type(), 1, false), rewinding) + panicRewinding := b.mod.NamedGlobal("tinygo_panic_rewinding") + if panicRewinding.IsNil() { + panicRewinding = llvm.AddGlobal(b.mod, b.ctx.Int8Type(), "tinygo_panic_rewinding") + panicRewinding.SetLinkage(llvm.ExternalLinkage) + } + builder.CreateStore(llvm.ConstInt(b.ctx.Int8Type(), 1, false), panicRewinding) + + startType, startFn := b.getRuntimeFunction("asyncifyStartRewindImport") + builder.CreateCall(startType, startFn, []llvm.Value{data}, "") + args := make([]llvm.Value, len(catcherType.ParamTypes())) + for i, typ := range catcherType.ParamTypes() { + args[i] = llvm.Undef(typ) + } + builder.CreateCall(catcherType, targetPointer, args, "") + builder.CreateUnreachable() + return replay +} + +func (b *builder) emitAsyncifyCatchReturn(builder llvm.Builder, fnType llvm.Type, result llvm.Value, entry llvm.BasicBlock, unwinding, replay, replayTarget llvm.Value) { wrapper := entry.Parent() stopBlock := b.ctx.AddBasicBlock(wrapper, "unwind.stop") returnBlock := b.ctx.AddBasicBlock(wrapper, "return") @@ -301,6 +351,10 @@ func (b *builder) emitAsyncifyCatchReturn(builder llvm.Builder, fnType llvm.Type builder.SetInsertPointAtEnd(stopBlock) stopType, stopFn := b.getRuntimeFunction("asyncifyStopUnwindImport") builder.CreateCall(stopType, stopFn, nil, "") + saveType, saveFn := b.getRuntimeFunction("savePanicReplay") + replayPointer := builder.CreatePtrToInt(replay, b.uintptrType, "") + targetPointer := builder.CreatePtrToInt(replayTarget, b.uintptrType, "") + builder.CreateCall(saveType, saveFn, []llvm.Value{replayPointer, targetPointer, llvm.Undef(b.dataPtrType)}, "") builder.CreateBr(returnBlock) builder.SetInsertPointAtEnd(returnBlock) @@ -489,7 +543,9 @@ func (b *builder) isUnwindRuntime() bool { } switch b.fn.Name() { case "startUnwind", "currentDeferFrame", "unwindPending", "clearUnwind", - "getUnwindSignal", "setUnwindSignal": + "getUnwindSignal", "setUnwindSignal", "savePanicReplay", + "panicRewindData", "panicRewindStackPointer", + "setPanicRewindStackPointer", "rewindPanic": return true default: return false diff --git a/compiler/compiler.go b/compiler/compiler.go index 1b35be10dd..febcb1e4d4 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -90,6 +90,7 @@ type compilerContext struct { functionInfos map[*ssa.Function]functionInfo callProperties map[*ssa.Function]functionCallProperties asyncifyCatchers map[llvm.Type]llvm.Value + asyncifyReplays map[llvm.Type]llvm.Value astComments map[string]*ast.CommentGroup embedGlobals map[string][]*loader.EmbedFile pkg *types.Package @@ -112,6 +113,7 @@ func newCompilerContext(moduleName string, machine llvm.TargetMachine, config *C functionInfos: map[*ssa.Function]functionInfo{}, callProperties: map[*ssa.Function]functionCallProperties{}, asyncifyCatchers: map[llvm.Type]llvm.Value{}, + asyncifyReplays: map[llvm.Type]llvm.Value{}, astComments: map[string]*ast.CommentGroup{}, } diff --git a/compiler/testdata/large.ll b/compiler/testdata/large.ll index 67bca09a32..752f9c5dd1 100644 --- a/compiler/testdata/large.ll +++ b/compiler/testdata/large.ll @@ -9,6 +9,8 @@ target triple = "wasm32-unknown-wasi" %runtime.channelOp = type { ptr, ptr, i32, ptr } %runtime.chanSelectState = type { ptr, ptr } +@tinygo_rewinding = external global i8 +@tinygo_panic_rewinding = external global i8 @"runtime/gc.layout:258-000000000000000000000000000000000000000000000000000000000000000002" = linkonce_odr unnamed_addr constant { i32, [33 x i8] } { i32 258, [33 x i8] c"\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\02" } @"reflect/types.typeid:named:main.largeValue" = external constant i8 @llvm.used = appending global [15 x ptr] [ptr @"(main.largeReceiver).makeLargeValue", ptr @"(main.largeReceiver).readLargeValue", ptr @main.makeLargeValue, ptr @main.makeZeroLargeValue, ptr @main.readLargeValue, ptr @main.deferLargeValue, ptr @main.goLargeValue, ptr @main.makeLargeResults, ptr @main.makeTwoLargeResults, ptr @main.makeMixedLargeResults, ptr @main.chooseLargeValue, ptr @main.makePointerLargeValue, ptr @main.useLargeMap, ptr @main.useLargeChannel, ptr @main.selectLargeChannel] @@ -39,8 +41,8 @@ declare void @llvm.memcpy.p0.p0.i32(ptr noalias nocapture writeonly, ptr noalias 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) #12 - call void @runtime.trackPointer(ptr nonnull %value1, ptr nonnull %stackalloc, ptr undef) #12 + %value1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %value1, ptr nonnull %stackalloc, ptr undef) #13 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 @@ -54,12 +56,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) #12 - call void @runtime.trackPointer(ptr nonnull %result, ptr nonnull %stackalloc, ptr undef) #12 + %result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %result, ptr nonnull %stackalloc, ptr undef) #13 %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) #12 - call void @runtime.trackPointer(ptr nonnull %1, ptr nonnull %stackalloc, ptr undef) #12 + %1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %1, ptr nonnull %stackalloc, ptr undef) #13 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 @@ -76,8 +78,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) #12 - call void @runtime.trackPointer(ptr nonnull %"main.largeValue{}:main.largeValue", ptr nonnull %stackalloc, ptr undef) #12 + %"main.largeValue{}:main.largeValue" = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %"main.largeValue{}:main.largeValue", ptr nonnull %stackalloc, ptr undef) #13 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 @@ -87,8 +89,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) #12 - call void @runtime.trackPointer(ptr nonnull %value1, ptr nonnull %stackalloc, ptr undef) #12 + %value1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %value1, ptr nonnull %stackalloc, ptr undef) #13 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 @@ -99,8 +101,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) #12 - call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #12 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #13 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 @@ -110,18 +112,18 @@ 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) #12 - call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #12 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #13 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) #12 + %1 = call i8 %fn.funcptr(ptr nonnull %call.result, ptr %fn.context) #13 ret i8 %1 fpcall.throw: ; preds = %entry - call void @runtime.nilPanic(ptr undef) #12 + call void @runtime.nilPanic(ptr undef) #13 br label %unwind.return unwind.return: ; preds = %fpcall.throw @@ -134,10 +136,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) #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 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #13 + 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) #13 + %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) #13 ret i8 %0 } @@ -152,10 +154,10 @@ entry: %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 + call void @runtime.setupDeferFrame(ptr nonnull %deferframe.buf, ptr %0, ptr undef) #13 %stackalloc = alloca i8, align 1 %defer.next = load ptr, ptr %deferPtr, align 4 - call void @runtime.trackPointer(ptr nonnull %defer.alloca, ptr nonnull %stackalloc, ptr undef) #12 + call void @runtime.trackPointer(ptr nonnull %defer.alloca, ptr nonnull %stackalloc, ptr undef) #13 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 @@ -165,8 +167,8 @@ entry: 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 + call void @runtime.destroyDeferFrame(ptr nonnull %deferframe.buf, ptr undef) #13 + %unwind = call i1 @runtime.unwindPending(ptr undef) #13 br i1 %unwind, label %unwind.return, label %unwind.continue unwind.continue: ; preds = %rundefers.after @@ -192,8 +194,8 @@ rundefers.loop: ; preds = %rundefers.loophead rundefers.callback0: ; preds = %rundefers.loop %gep = getelementptr inbounds nuw i8, ptr %1, i32 8 %param = load ptr, ptr %gep, align 4 - %2 = call i8 @main.deferLargeValue.asyncifycatch.0(ptr %param, ptr undef) #12 - call void @runtime.clearUnwind(ptr undef) #12 + %2 = call i8 @main.deferLargeValue.asyncifycatch.0(ptr %param, ptr undef) #13 + call void @runtime.clearUnwind(ptr undef) #13 br label %rundefers.loophead rundefers.default: ; preds = %rundefers.loop @@ -250,13 +252,38 @@ entry: unwind.stop: ; preds = %entry call void @runtime.asyncifyStopUnwindImport() + call void @runtime.savePanicReplay(i32 ptrtoint (ptr @tinygo.asyncify.panicreplay.0 to i32), i32 ptrtoint (ptr @main.deferLargeValue.asyncifycatch.0 to i32), ptr undef) br label %return return: ; preds = %unwind.stop, %entry ret i8 %2 } -declare void @runtime.asyncifyStopUnwindImport() #8 +; Function Attrs: noinline +define internal void @tinygo.asyncify.panicreplay.0(i32 %0) #7 { +entry: + %1 = call ptr @runtime.panicRewindData(ptr undef) + %2 = call ptr @runtime.panicRewindStackPointer(ptr undef) + %3 = inttoptr i32 %0 to ptr + call void @tinygo_set_panic_rewind_stack_pointer(ptr %2, ptr undef) + store i8 1, ptr @tinygo_rewinding, align 1 + store i8 1, ptr @tinygo_panic_rewinding, align 1 + call void @runtime.asyncifyStartRewindImport(ptr %1) + %4 = call i8 %3(ptr undef, ptr undef) + unreachable +} + +declare ptr @runtime.panicRewindData(ptr) #0 + +declare ptr @runtime.panicRewindStackPointer(ptr) #0 + +declare void @tinygo_set_panic_rewind_stack_pointer(ptr, ptr) #0 + +declare void @runtime.asyncifyStartRewindImport(ptr nocapture) #8 + +declare void @runtime.asyncifyStopUnwindImport() #9 + +declare void @runtime.savePanicReplay(i32, i32, ptr) #0 declare void @runtime.clearUnwind(ptr) #0 @@ -269,6 +296,7 @@ entry: unwind.stop: ; preds = %entry call void @runtime.asyncifyStopUnwindImport() + call void @runtime.savePanicReplay(i32 ptrtoint (ptr @tinygo.asyncify.panicreplay.0 to i32), i32 ptrtoint (ptr @main.deferLargeValue.asyncifycatch.1 to i32), ptr undef) br label %return return: ; preds = %unwind.stop, %entry @@ -279,20 +307,20 @@ return: ; preds = %unwind.stop, %entry 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) #12 - call void @runtime.trackPointer(ptr nonnull %go.param, ptr nonnull %stackalloc, ptr undef) #12 + %go.param = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %go.param, ptr nonnull %stackalloc, ptr undef) #13 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) #12 + call void @"internal/task.start"(i32 ptrtoint (ptr @"main.readLargeValue$gowrapper" to i32), ptr nonnull %go.param, i32 65536, ptr undef) #13 ret void } declare void @runtime.deadlock(ptr) #0 ; Function Attrs: nounwind -define linkonce_odr void @"main.readLargeValue$gowrapper"(ptr %0) unnamed_addr #9 { +define linkonce_odr void @"main.readLargeValue$gowrapper"(ptr %0) unnamed_addr #10 { entry: %1 = call i8 @main.readLargeValue(ptr %0, ptr undef) - call void @runtime.deadlock(ptr undef) #12 + call void @runtime.deadlock(ptr undef) #13 unreachable } @@ -302,8 +330,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) #12 - call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #12 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #13 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 @@ -315,12 +343,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) #12 - call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #12 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #13 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) #12 - call void @runtime.trackPointer(ptr nonnull %call.result1, ptr nonnull %stackalloc, ptr undef) #12 + %call.result1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %call.result1, ptr nonnull %stackalloc, ptr undef) #13 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 @@ -332,13 +360,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) #12 - call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #12 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #13 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) #12 - call void @runtime.trackPointer(ptr nonnull %call.result1, ptr nonnull %stackalloc, ptr undef) #12 + %call.result1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %call.result1, ptr nonnull %stackalloc, ptr undef) #13 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 @@ -352,14 +380,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) #12 - call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #12 + %call.result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %call.result, ptr nonnull %stackalloc, ptr undef) #13 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) #12 - call void @runtime.trackPointer(ptr nonnull %call.result1, ptr nonnull %stackalloc, ptr undef) #12 + %call.result1 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %call.result1, ptr nonnull %stackalloc, ptr undef) #13 call void @main.makeLargeValue(ptr nonnull %call.result1, i8 42, ptr undef) br label %if.done @@ -373,15 +401,15 @@ 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) #12 - call void @runtime.trackPointer(ptr nonnull %complit, ptr nonnull %stackalloc, ptr undef) #12 + %complit = call align 4 dereferenceable(1032) ptr @runtime.alloc(i32 1032, ptr nonnull @"runtime/gc.layout:258-000000000000000000000000000000000000000000000000000000000000000002", ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %complit, ptr nonnull %stackalloc, ptr undef) #13 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) #12 - call void @runtime.trackPointer(ptr nonnull %1, ptr nonnull %stackalloc, ptr undef) #12 + %1 = call align 4 dereferenceable(1032) ptr @runtime.alloc(i32 1032, ptr nonnull @"runtime/gc.layout:258-000000000000000000000000000000000000000000000000000000000000000002", ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %1, ptr nonnull %stackalloc, ptr undef) #13 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 @@ -397,19 +425,19 @@ unwind.return: ; preds = %store.throw 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) #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 + %large = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %large, ptr nonnull %stackalloc, ptr undef) #13 + %typecode = call i1 @runtime.typeAssert(ptr %value.typecode, ptr nonnull @"reflect/types.typeid:named:main.largeValue", ptr undef) #13 + %typeassert.result = call align 1 dereferenceable(1026) ptr @runtime.alloc(i32 1026, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %typeassert.result, ptr nonnull %stackalloc, ptr undef) #13 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) #12 - call void @runtime.trackPointer(ptr nonnull %t2, ptr nonnull %stackalloc, ptr undef) #12 + %t2 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %t2, ptr nonnull %stackalloc, ptr undef) #13 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 @@ -430,24 +458,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 nocapture writeonly, i8, i32, i1 immarg) #10 +declare void @llvm.memset.p0.i32(ptr nocapture writeonly, i8, i32, i1 immarg) #11 ; 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) #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 + %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) #13 + call void @runtime.trackPointer(ptr %0, ptr nonnull %stackalloc, ptr undef) #13 + call void @runtime.hashmapBinarySet(ptr %0, ptr %key, ptr %value, ptr undef) #13 + %result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %result, ptr nonnull %stackalloc, ptr undef) #13 + %hashmap.result = call align 1 dereferenceable(1026) ptr @runtime.alloc(i32 1026, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %hashmap.result, ptr nonnull %stackalloc, ptr undef) #13 + %1 = call i1 @runtime.hashmapBinaryGet(ptr %0, ptr %key, ptr nonnull %hashmap.result, i32 1025, ptr undef) #13 %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) #12 - call void @runtime.trackPointer(ptr nonnull %t3, ptr nonnull %stackalloc, ptr undef) #12 + %t3 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %t3, ptr nonnull %stackalloc, ptr undef) #13 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 @@ -480,19 +508,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) #12 + call void @runtime.chanSend(ptr %ch, ptr %value, ptr nonnull %chan.op, ptr undef) #13 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) #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 + %result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %result, ptr nonnull %stackalloc, ptr undef) #13 + %chan.result = call align 1 dereferenceable(1026) ptr @runtime.alloc(i32 1026, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %chan.result, ptr nonnull %stackalloc, ptr undef) #13 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) #12 + %0 = call i1 @runtime.chanRecv(ptr %ch, ptr nonnull %chan.result, ptr nonnull %chan.op1, ptr undef) #13 %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) #12 - call void @runtime.trackPointer(ptr nonnull %t2, ptr nonnull %stackalloc, ptr undef) #12 + %t2 = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %t2, ptr nonnull %stackalloc, ptr undef) #13 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 @@ -509,12 +537,12 @@ if.then: ; preds = %entry } ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) -declare void @llvm.lifetime.start.p0(ptr nocapture) #11 +declare void @llvm.lifetime.start.p0(ptr nocapture) #12 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 nocapture) #11 +declare void @llvm.lifetime.end.p0(ptr nocapture) #12 declare i1 @runtime.chanRecv(ptr dereferenceable_or_null(36), ptr, ptr dereferenceable_or_null(16), ptr) #0 @@ -535,10 +563,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) #12 + %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) #13 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) #12 + call void @runtime.trackPointer(ptr nonnull %select.recvbuf.alloca, ptr nonnull %stackalloc, ptr undef) #13 %1 = extractvalue { i32, i1 } %select.result, 0 %2 = icmp eq i32 %1, 0 br i1 %2, label %select.body, label %select.next @@ -551,10 +579,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) #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 + %result = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %result, ptr nonnull %stackalloc, ptr undef) #13 + %select.received = call align 1 dereferenceable(1025) ptr @runtime.alloc(i32 1025, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull %select.received, ptr nonnull %stackalloc, ptr undef) #13 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 @@ -562,9 +590,9 @@ 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) #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 + call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:basic:string", ptr nonnull %stackalloc, ptr undef) #13 + call void @runtime.trackPointer(ptr nonnull @"main$pack", ptr nonnull %stackalloc, ptr undef) #13 + call void @runtime._panic(ptr nonnull @"reflect/types.type:basic:string", ptr nonnull @"main$pack", ptr undef) #13 br label %unwind.return unwind.return: ; preds = %select.next2 @@ -583,8 +611,9 @@ attributes #4 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirec 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 = { 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 } +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"="start_rewind" } +attributes #9 = { "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 #10 = { 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 #11 = { nocallback nofree nounwind willreturn memory(argmem: write) } +attributes #12 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) } +attributes #13 = { nounwind } diff --git a/main_test.go b/main_test.go index 2a9e32d96e..1bafd431aa 100644 --- a/main_test.go +++ b/main_test.go @@ -1074,6 +1074,42 @@ func TestGoexitCrash(t *testing.T) { } } +func TestWASIPanicTraceback(t *testing.T) { + t.Parallel() + + if testing.Short() { + t.Skip("skipping test in short mode") + } + + options := optionsFromTarget("wasip1", sema) + config, err := builder.NewConfig(&options) + if err != nil { + t.Fatal(err) + } + + result, err := builder.Build("testdata/panic-traceback.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) + } + + 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) + _, err = r.InstantiateWithConfig(ctx, data, wazero.NewModuleConfig()) + if err == nil { + t.Fatal("program unexpectedly exited successfully") + } + if !strings.Contains(err.Error(), "main.panicHere") { + t.Fatalf("panic traceback does not contain main.panicHere:\n%s", err) + } +} + func TestRuntimeFatal(t *testing.T) { t.Parallel() diff --git a/src/internal/task/task_asyncify.go b/src/internal/task/task_asyncify.go index a3d8fa999a..5a58d2a4e9 100644 --- a/src/internal/task/task_asyncify.go +++ b/src/internal/task/task_asyncify.go @@ -25,6 +25,11 @@ type state struct { stackState launched bool + + panicCapturing bool + panicOrigin stackState + panicReplay uintptr + panicTarget uintptr } // stackState is the saved state of a stack while unwound. @@ -105,11 +110,59 @@ func Pause() { // PanicUnwind starts an Asyncify unwind without pausing the task. A defer // frame stops the unwind before it reaches the scheduler. func PanicUnwind() { + currentTask.state.panicCapturing = true panicStackState.asyncifysp = currentTask.state.asyncifysp + if currentTask.state.panicReplay != 0 { + // Preserve the first unwind segment while later defer frames unwind. + panicStackState.asyncifysp = currentTask.state.panicOrigin.asyncifysp + } panicStackState.csp = currentTask.state.csp panicStackState.unwind() } +func StopPanicUnwind(replay, target uintptr) { + if !currentTask.state.panicCapturing { + return + } + currentTask.state.panicCapturing = false + if currentTask.state.panicReplay == 0 { + currentTask.state.panicOrigin = panicStackState + currentTask.state.panicReplay = replay + currentTask.state.panicTarget = target + } +} + +func ClearPanicReplay() { + if currentTask == nil { + return + } + // An older continuation may reference stack memory reused while handling a + // nested panic, so discard it instead of attempting an unsafe replay. + currentTask.state.panicCapturing = false + currentTask.state.panicOrigin = stackState{} + currentTask.state.panicReplay = 0 + currentTask.state.panicTarget = 0 +} + +func PanicRewindData() unsafe.Pointer { + return unsafe.Pointer(¤tTask.state.panicOrigin) +} + +func PanicRewindStackPointer() unsafe.Pointer { + return currentTask.state.panicOrigin.csp +} + +func RewindPanic() bool { + if currentTask.state.panicReplay == 0 { + return false + } + tinygoPanicReplay(currentTask.state.panicReplay, currentTask.state.panicTarget) + return true +} + +//go:linkname tinygoPanicReplay tinygo_panic_replay +func tinygoPanicReplay(replay, target uintptr) + //export tinygo_unwind func (*stackState) unwind() diff --git a/src/internal/task/task_asyncify_wasm.S b/src/internal/task/task_asyncify_wasm.S index 9aafc1108a..d6494ec3d8 100644 --- a/src/internal/task/task_asyncify_wasm.S +++ b/src/internal/task/task_asyncify_wasm.S @@ -25,6 +25,12 @@ tinygo_unwind: // func (state *stackState) unwind() // Stop rewinding. call stop_rewind i32.const 0 + i32.load8_u tinygo_panic_rewinding + if + // Trap as soon as the original frames have been reconstructed. + unreachable + end_if + i32.const 0 i32.const 0 i32.store8 tinygo_rewinding // tinygo_rewinding = false; else @@ -97,6 +103,27 @@ tinygo_rewind: // func (state *state) rewind() return end_function +.global tinygo_panic_replay +.hidden tinygo_panic_replay +.type tinygo_panic_replay,@function +tinygo_panic_replay: + .functype tinygo_panic_replay (i32, i32, i32) -> () + local.get 1 + local.get 0 + call_indirect (i32) -> () + return + end_function + +.global tinygo_set_panic_rewind_stack_pointer +.hidden tinygo_set_panic_rewind_stack_pointer +.type tinygo_set_panic_rewind_stack_pointer,@function +tinygo_set_panic_rewind_stack_pointer: + .functype tinygo_set_panic_rewind_stack_pointer (i32, i32) -> () + local.get 0 + global.set __stack_pointer + return + end_function + .hidden tinygo_rewinding # @tinygo_rewinding .type tinygo_rewinding,@object .section .bss.tinygo_rewinding,"",@ @@ -104,3 +131,11 @@ tinygo_rewind: // func (state *state) rewind() tinygo_rewinding: .int8 0 # 0x0 .size tinygo_rewinding, 1 + + .hidden tinygo_panic_rewinding + .type tinygo_panic_rewinding,@object + .section .bss.tinygo_panic_rewinding,"",@ + .globl tinygo_panic_rewinding +tinygo_panic_rewinding: + .int8 0 + .size tinygo_panic_rewinding, 1 diff --git a/src/runtime/panic.go b/src/runtime/panic.go index 3a837a7659..d79d1f8275 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -53,6 +53,7 @@ const ( // Builtin function panic(msg), used as a compiler intrinsic. func _panic(message interface{}) { + clearPanicReplay() panicOrGoexit(message, panicTrue) } @@ -92,6 +93,11 @@ func panicOrGoexit(message interface{}, panicking panicState) { printstring("panic: ") printitf(message) printnl() + // Reconstruct the original Wasm frames before trapping so the host can + // report the panic site instead of only the final defer frame. + if rewindPanic() { + return + } abort() } @@ -115,6 +121,7 @@ func runtimeFatal(msg string) { } func runtimePanicAt(addr unsafe.Pointer, msg string) { + clearPanicReplay() if panicStrategy() == tinygo.PanicStrategyTrap { trap() } @@ -226,6 +233,7 @@ func _recover(useParentFrame bool) interface{} { // Only the first call to recover returns the panic value. It also stops // the panicking sequence, hence setting panicking to false. frame.PanicState &^= panicTrue + clearPanicReplay() 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 index 0be2d6cb2a..0f99696220 100644 --- a/src/runtime/panic_unwind_asyncify.go +++ b/src/runtime/panic_unwind_asyncify.go @@ -2,14 +2,43 @@ package runtime -import "internal/task" +import ( + "internal/task" + "unsafe" +) //go:wasmimport asyncify stop_unwind func asyncifyStopUnwindImport() +//go:wasmimport asyncify start_rewind +func asyncifyStartRewindImport(state unsafe.Pointer) + func startUnwind(frame *deferFrame) bool { frame.PanicState |= panicUnwinding setUnwindSignal(true) task.PanicUnwind() return true } + +func savePanicReplay(replay, target uintptr) { + task.StopPanicUnwind(replay, target) +} + +func clearPanicReplay() { + task.ClearPanicReplay() +} + +func panicRewindData() unsafe.Pointer { + return task.PanicRewindData() +} + +func panicRewindStackPointer() unsafe.Pointer { + return task.PanicRewindStackPointer() +} + +//go:linkname setPanicRewindStackPointer tinygo_set_panic_rewind_stack_pointer +func setPanicRewindStackPointer(stackPointer unsafe.Pointer) + +func rewindPanic() bool { + return task.RewindPanic() +} diff --git a/src/runtime/panic_unwind_explicit.go b/src/runtime/panic_unwind_explicit.go index 231d4ae9c6..5cf369c786 100644 --- a/src/runtime/panic_unwind_explicit.go +++ b/src/runtime/panic_unwind_explicit.go @@ -7,3 +7,10 @@ func startUnwind(frame *deferFrame) bool { setUnwindSignal(true) return true } + +func clearPanicReplay() { +} + +func rewindPanic() bool { + return false +} diff --git a/src/runtime/panic_unwind_none.go b/src/runtime/panic_unwind_none.go index 3ccb5f5833..064a4a47c9 100644 --- a/src/runtime/panic_unwind_none.go +++ b/src/runtime/panic_unwind_none.go @@ -5,3 +5,10 @@ package runtime func startUnwind(frame *deferFrame) bool { return false } + +func clearPanicReplay() { +} + +func rewindPanic() bool { + return false +} diff --git a/src/runtime/panic_unwind_setjmp.go b/src/runtime/panic_unwind_setjmp.go index f91aa4ccf3..1b84589818 100644 --- a/src/runtime/panic_unwind_setjmp.go +++ b/src/runtime/panic_unwind_setjmp.go @@ -6,3 +6,10 @@ func startUnwind(frame *deferFrame) bool { tinygo_longjmp(frame) return false } + +func clearPanicReplay() { +} + +func rewindPanic() bool { + return false +} diff --git a/testdata/panic-traceback.go b/testdata/panic-traceback.go new file mode 100644 index 0000000000..a5f56b0963 --- /dev/null +++ b/testdata/panic-traceback.go @@ -0,0 +1,23 @@ +package main + +//go:noinline +func panicHere() { + panic("boom") +} + +//go:noinline +func inner() { + defer func() {}() + panicHere() +} + +//go:noinline +func outer() { + defer func() {}() + inner() +} + +func main() { + defer func() {}() + outer() +} From a9b94f9a613c0bceb10d295b843bde5704dc53b1 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:36:33 -0700 Subject: [PATCH 08/11] compiler: reduce Asyncify catcher stack usage --- compiler/calls.go | 27 ++++++++++----------------- src/runtime/panic_unwind_asyncify.go | 4 ++++ tests/testing/pass/pass_wasm_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 tests/testing/pass/pass_wasm_test.go diff --git a/compiler/calls.go b/compiler/calls.go index 0685725ca0..9b0e3f55fe 100644 --- a/compiler/calls.go +++ b/compiler/calls.go @@ -216,9 +216,9 @@ func (b *builder) createAsyncifySuspendInvoke(fnType llvm.Type, fn llvm.Value, a 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 = llvm.AddGlobal(b.mod, fnType.ReturnType(), wrapperName+".result") resultGlobal.SetLinkage(llvm.InternalLinkage) - resultGlobal.SetInitializer(llvm.ConstNull(b.dataPtrType)) + resultGlobal.SetInitializer(llvm.ConstNull(fnType.ReturnType())) } targetWrapper := llvm.AddFunction(b.mod, wrapperName+".target", targetType) targetWrapper.SetLinkage(llvm.InternalLinkage) @@ -235,9 +235,8 @@ func (b *builder) createAsyncifySuspendInvoke(fnType llvm.Type, fn llvm.Value, a if fnType.ReturnType().TypeKind() == llvm.VoidTypeKind { builder.CreateRetVoid() } else { - resultPointer := builder.CreateLoad(b.dataPtrType, resultGlobal, "") - resultPointer.SetVolatile(true) - builder.CreateStore(result, resultPointer) + storeResult := builder.CreateStore(result, resultGlobal) + storeResult.SetVolatile(true) builder.CreateRetVoid() } builder.Dispose() @@ -255,19 +254,13 @@ func (b *builder) createAsyncifySuspendInvoke(fnType llvm.Type, fn llvm.Value, a 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) + // Asyncify runs one task at a time, and the target stores its result only + // after it finishes, so nested calls consume their results first. builder.CreateCall(targetType, targetWrapper, catchArgs, "") - restoreResultPointer := builder.CreateStore(previousResultPointer, resultGlobal) - restoreResultPointer.SetVolatile(true) - result = builder.CreateLoad(fnType.ReturnType(), resultStorage, "") + result = builder.CreateLoad(fnType.ReturnType(), resultGlobal, "") + result.SetVolatile(true) + clearResult := builder.CreateStore(llvm.ConstNull(fnType.ReturnType()), resultGlobal) + clearResult.SetVolatile(true) } unwindType, unwindLLVMFn := b.getRuntimeFunction("unwindPending") // This call must stay opaque to AddUnwindAssumptions. This catcher observes diff --git a/src/runtime/panic_unwind_asyncify.go b/src/runtime/panic_unwind_asyncify.go index 0f99696220..eed30fd84b 100644 --- a/src/runtime/panic_unwind_asyncify.go +++ b/src/runtime/panic_unwind_asyncify.go @@ -20,6 +20,10 @@ func startUnwind(frame *deferFrame) bool { return true } +// Keep this bookkeeping out of the uninstrumented panic catcher so ordinary +// scheduler unwinds do not retain its stack frame. +// +//go:noinline func savePanicReplay(replay, target uintptr) { task.StopPanicUnwind(replay, target) } diff --git a/tests/testing/pass/pass_wasm_test.go b/tests/testing/pass/pass_wasm_test.go new file mode 100644 index 0000000000..810bcdde35 --- /dev/null +++ b/tests/testing/pass/pass_wasm_test.go @@ -0,0 +1,28 @@ +//go:build wasm + +package pass_test + +import "testing" + +func TestDeepSuspendWithDefer(t *testing.T) { + ready := make(chan struct{}) + release := make(chan struct{}) + done := make(chan struct{}) + go func() { + deepSuspendWithDefer(512, ready, release) + close(done) + }() + <-ready + close(release) + <-done +} + +func deepSuspendWithDefer(depth int, ready chan<- struct{}, release <-chan struct{}) { + defer func() {}() + if depth == 0 { + ready <- struct{}{} + <-release + return + } + deepSuspendWithDefer(depth-1, ready, release) +} From 54c8c0d3f9046965785c1d5d9b7f32b8d1d4d371 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:09:52 -0700 Subject: [PATCH 09/11] compiler: reuse Asyncify panic catchers --- compiler/calls.go | 10 ++++++++++ compiler/compiler.go | 4 ++++ compiler/testdata/large.ll | 16 ---------------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/compiler/calls.go b/compiler/calls.go index 9b0e3f55fe..7dab9432b1 100644 --- a/compiler/calls.go +++ b/compiler/calls.go @@ -148,6 +148,13 @@ func (b *builder) createAsyncifyNoSuspendInvoke(fnType llvm.Type, fn llvm.Value, paramTypes = append([]llvm.Type{fn.Type()}, paramTypes...) } wrapperType := llvm.FunctionType(fnType.ReturnType(), paramTypes, false) + if direct { + if wrapper, ok := b.directCatchers[fn]; ok { + return b.CreateCall(wrapperType, wrapper, expanded, name) + } + } else if wrapper, ok := b.indirectCatchers[wrapperType]; ok { + return b.CreateCall(wrapperType, wrapper, append([]llvm.Value{fn}, expanded...), name) + } wrapperName := b.llvmFn.Name() + ".asyncifycatch." + strconv.Itoa(b.asyncifyCatchIndex) b.asyncifyCatchIndex++ wrapper := llvm.AddFunction(b.mod, wrapperName, wrapperType) @@ -177,6 +184,9 @@ func (b *builder) createAsyncifyNoSuspendInvoke(fnType llvm.Type, fn llvm.Value, wrapperArgs := expanded if !direct { wrapperArgs = append([]llvm.Value{fn}, expanded...) + b.indirectCatchers[wrapperType] = wrapper + } else { + b.directCatchers[fn] = wrapper } return b.CreateCall(wrapperType, wrapper, wrapperArgs, name) } diff --git a/compiler/compiler.go b/compiler/compiler.go index febcb1e4d4..30eb1fd468 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -90,6 +90,8 @@ type compilerContext struct { functionInfos map[*ssa.Function]functionInfo callProperties map[*ssa.Function]functionCallProperties asyncifyCatchers map[llvm.Type]llvm.Value + directCatchers map[llvm.Value]llvm.Value + indirectCatchers map[llvm.Type]llvm.Value asyncifyReplays map[llvm.Type]llvm.Value astComments map[string]*ast.CommentGroup embedGlobals map[string][]*loader.EmbedFile @@ -113,6 +115,8 @@ func newCompilerContext(moduleName string, machine llvm.TargetMachine, config *C functionInfos: map[*ssa.Function]functionInfo{}, callProperties: map[*ssa.Function]functionCallProperties{}, asyncifyCatchers: map[llvm.Type]llvm.Value{}, + directCatchers: map[llvm.Value]llvm.Value{}, + indirectCatchers: map[llvm.Type]llvm.Value{}, asyncifyReplays: map[llvm.Type]llvm.Value{}, astComments: map[string]*ast.CommentGroup{}, } diff --git a/compiler/testdata/large.ll b/compiler/testdata/large.ll index 752f9c5dd1..9a837643f8 100644 --- a/compiler/testdata/large.ll +++ b/compiler/testdata/large.ll @@ -287,22 +287,6 @@ declare void @runtime.savePanicReplay(i32, i32, ptr) #0 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() - call void @runtime.savePanicReplay(i32 ptrtoint (ptr @tinygo.asyncify.panicreplay.0 to i32), i32 ptrtoint (ptr @main.deferLargeValue.asyncifycatch.1 to i32), ptr undef) - 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: From 4d804d79117676e59b6d07cdebac304ece4b28de Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:11:53 -0700 Subject: [PATCH 10/11] runtime: name Goexit task exit explicitly --- src/internal/task/task_exit.go | 4 ++-- src/internal/task/task_threads.go | 4 ++-- src/runtime/scheduler_cooperative.go | 2 +- src/runtime/scheduler_cores.go | 2 +- src/runtime/scheduler_threads.go | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/internal/task/task_exit.go b/src/internal/task/task_exit.go index 40f988058c..21e5860a08 100644 --- a/src/internal/task/task_exit.go +++ b/src/internal/task/task_exit.go @@ -17,8 +17,8 @@ func addLiveTask(t *Task) { atomic.AddUint32(&liveTasks, 1) } -// Exit exits the current task because runtime.Goexit was called. -func Exit() { +// Goexit exits the current task because runtime.Goexit was called. +func Goexit() { exit(true) } diff --git a/src/internal/task/task_threads.go b/src/internal/task/task_threads.go index 52374dec87..2ca3c93f82 100644 --- a/src/internal/task/task_threads.go +++ b/src/internal/task/task_threads.go @@ -161,9 +161,9 @@ func otherTasks(current *Task) uint32 { return activeTaskCount - 1 } -// Exit exits the current task. If this is the main task and there are no other +// Goexit exits the current task. If this is the main task and there are no other // goroutines, it reports a deadlock. -func Exit() { +func Goexit() { t := Current() if t == &mainTask { activeTaskLock.Lock() diff --git a/src/runtime/scheduler_cooperative.go b/src/runtime/scheduler_cooperative.go index 6f8d6b0dae..00013458ba 100644 --- a/src/runtime/scheduler_cooperative.go +++ b/src/runtime/scheduler_cooperative.go @@ -55,7 +55,7 @@ func deadlock() { } func goexit() { - task.Exit() + task.Goexit() } // Add this task to the end of the run queue. diff --git a/src/runtime/scheduler_cores.go b/src/runtime/scheduler_cores.go index 853812bd23..b26abc7f30 100644 --- a/src/runtime/scheduler_cores.go +++ b/src/runtime/scheduler_cores.go @@ -33,7 +33,7 @@ func deadlock() { } func goexit() { - task.Exit() + task.Goexit() } // Mark the given task as ready to resume. diff --git a/src/runtime/scheduler_threads.go b/src/runtime/scheduler_threads.go index 5b6638b98f..166a342c82 100644 --- a/src/runtime/scheduler_threads.go +++ b/src/runtime/scheduler_threads.go @@ -44,7 +44,7 @@ func deadlock() { } func goexit() { - task.Exit() + task.Goexit() } func scheduleTask(t *task.Task) { From 5ececc48783f2abf1c26b9878ade1d4d7f5207c5 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:12:05 -0700 Subject: [PATCH 11/11] runtime: release completed Asyncify stacks --- compiler/goroutine.go | 12 ++++++------ compiler/testdata/goroutine-wasm-asyncify.ll | 12 ++++++------ compiler/testdata/large.ll | 4 ++-- main_test.go | 2 +- src/internal/task/task.go | 6 ++++++ src/internal/task/task_asyncify.go | 11 ++++++++++- src/internal/task/task_exit.go | 8 ++++++++ src/internal/task/task_stack.go | 2 ++ src/runtime/gc_blocks.go | 14 ++++++++++++++ src/runtime/gc_boehm.go | 4 ++++ src/runtime/gc_custom.go | 4 ++++ src/runtime/gc_leaking.go | 3 +++ src/runtime/gc_none.go | 3 +++ 13 files changed, 69 insertions(+), 16 deletions(-) diff --git a/compiler/goroutine.go b/compiler/goroutine.go index 26d489a3a4..68e74a67c2 100644 --- a/compiler/goroutine.go +++ b/compiler/goroutine.go @@ -309,10 +309,10 @@ func (c *compilerContext) createGoroutineStartWrapper(fnType llvm.Type, fn llvm. } defer b.Dispose() - var deadlock llvm.Value - var deadlockType llvm.Type + var taskExit llvm.Value + var taskExitType llvm.Type if c.Scheduler == "asyncify" { - deadlockType, deadlock = c.getFunction(c.program.ImportedPackage("runtime").Members["deadlock"].(*ssa.Function)) + taskExitType, taskExit = c.getFunction(c.program.ImportedPackage("internal/task").Members["Exit"].(*ssa.Function)) } if !fn.IsAFunction().IsNil() { @@ -377,7 +377,7 @@ func (c *compilerContext) createGoroutineStartWrapper(fnType llvm.Type, fn llvm. b.CreateCall(fnType, fn, params, "") if c.Scheduler == "asyncify" { - b.CreateCall(deadlockType, deadlock, []llvm.Value{ + b.CreateCall(taskExitType, taskExit, []llvm.Value{ llvm.Undef(c.dataPtrType), }, "") } @@ -528,14 +528,14 @@ func (c *compilerContext) createGoroutineStartWrapper(fnType llvm.Type, fn llvm. b.CreateCall(fnType, fnPtr, params, "") if c.Scheduler == "asyncify" { - b.CreateCall(deadlockType, deadlock, []llvm.Value{ + b.CreateCall(taskExitType, taskExit, []llvm.Value{ llvm.Undef(c.dataPtrType), }, "") } } if c.Scheduler == "asyncify" { - // The goroutine was terminated via deadlock. + // The goroutine was terminated by Exit. b.CreateUnreachable() } else { // Finish the function. Every basic block must end in a terminator, and diff --git a/compiler/testdata/goroutine-wasm-asyncify.ll b/compiler/testdata/goroutine-wasm-asyncify.ll index 0c3f2f7073..41fa1ad341 100644 --- a/compiler/testdata/goroutine-wasm-asyncify.ll +++ b/compiler/testdata/goroutine-wasm-asyncify.ll @@ -22,14 +22,14 @@ entry: declare void @main.regularFunction(i32, ptr) #0 -declare void @runtime.deadlock(ptr) #0 +declare void @"internal/task.Exit"(ptr) #0 ; Function Attrs: nounwind define linkonce_odr void @"main.regularFunction$gowrapper"(ptr %0) unnamed_addr #2 { entry: %unpack.int = ptrtoint ptr %0 to i32 call void @main.regularFunction(i32 %unpack.int, ptr undef) #11 - call void @runtime.deadlock(ptr undef) #11 + call void @"internal/task.Exit"(ptr undef) #11 unreachable } @@ -53,7 +53,7 @@ define linkonce_odr void @"main.inlineFunctionGoroutine$1$gowrapper"(ptr %0) unn entry: %unpack.int = ptrtoint ptr %0 to i32 call void @"main.inlineFunctionGoroutine$1"(i32 %unpack.int, ptr undef) - call void @runtime.deadlock(ptr undef) #11 + call void @"internal/task.Exit"(ptr undef) #11 unreachable } @@ -96,7 +96,7 @@ entry: %2 = getelementptr inbounds nuw i8, ptr %0, i32 4 %3 = load ptr, ptr %2, align 4 call void @"main.closureFunctionGoroutine$1"(i32 %1, ptr %3) - call void @runtime.deadlock(ptr undef) #11 + call void @"internal/task.Exit"(ptr undef) #11 unreachable } @@ -130,7 +130,7 @@ entry: %4 = getelementptr inbounds nuw i8, ptr %0, i32 8 %5 = load ptr, ptr %4, align 4 call void %5(i32 %1, ptr %3) #11 - call void @runtime.deadlock(ptr undef) #11 + call void @"internal/task.Exit"(ptr undef) #11 unreachable } @@ -193,7 +193,7 @@ entry: %6 = getelementptr inbounds nuw i8, ptr %0, i32 12 %7 = load ptr, ptr %6, align 4 call void @"interface:{Print:func:{basic:string}{}}.Print$invoke"(ptr %1, ptr %3, i32 %5, ptr %7, ptr undef) #11 - call void @runtime.deadlock(ptr undef) #11 + call void @"internal/task.Exit"(ptr undef) #11 unreachable } diff --git a/compiler/testdata/large.ll b/compiler/testdata/large.ll index 9a837643f8..ed9bdd813c 100644 --- a/compiler/testdata/large.ll +++ b/compiler/testdata/large.ll @@ -298,13 +298,13 @@ entry: ret void } -declare void @runtime.deadlock(ptr) #0 +declare void @"internal/task.Exit"(ptr) #0 ; Function Attrs: nounwind define linkonce_odr void @"main.readLargeValue$gowrapper"(ptr %0) unnamed_addr #10 { entry: %1 = call i8 @main.readLargeValue(ptr %0, ptr undef) - call void @runtime.deadlock(ptr undef) #13 + call void @"internal/task.Exit"(ptr undef) #13 unreachable } diff --git a/main_test.go b/main_test.go index 1bafd431aa..80abaa6e1a 100644 --- a/main_test.go +++ b/main_test.go @@ -1033,7 +1033,7 @@ func TestGoexitCrash(t *testing.T) { panicStrategy string want string }{ - {"wasip1-deadlock", "deadlock", "", "deadlocked: no event source"}, + {"wasip1-deadlock", "deadlock", "", "fatal error: all goroutines are asleep - deadlock!"}, {"wasip1-goexit-panic-trap", "defer", "trap", "defer ran"}, } { t.Run(tc.name, func(t *testing.T) { diff --git a/src/internal/task/task.go b/src/internal/task/task.go index 55f89fa9fd..50f615b5e6 100644 --- a/src/internal/task/task.go +++ b/src/internal/task/task.go @@ -30,6 +30,9 @@ type Task struct { // since it falls into the padding of the FipsIndicator bit above. RunState uint8 + // Exited is set after a task with a releasable stack has finished. + Exited bool + // DeferFrame stores a pointer to the (stack allocated) defer frame of the // goroutine that is used for the recover builtin. DeferFrame unsafe.Pointer @@ -72,5 +75,8 @@ func getGoroutineStackSize(fn uintptr) uintptr //go:linkname runtime_alloc runtime.alloc func runtime_alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer +//go:linkname runtime_freeTaskStack runtime.freeTaskStack +func runtime_freeTaskStack(ptr unsafe.Pointer) + //go:linkname scheduleTask runtime.scheduleTask func scheduleTask(*Task) diff --git a/src/internal/task/task_asyncify.go b/src/internal/task/task_asyncify.go index 5a58d2a4e9..c6cdb019c2 100644 --- a/src/internal/task/task_asyncify.go +++ b/src/internal/task/task_asyncify.go @@ -32,6 +32,8 @@ type state struct { panicTarget uintptr } +const hasReleasableStack = true + // stackState is the saved state of a stack while unwound. // The stack is arranged with asyncify at the bottom, C stack at the top, and a gap of available stack space between the two. type stackState struct { @@ -70,7 +72,6 @@ func (s *state) initialize(fn uintptr, args unsafe.Pointer, stackSize uintptr) { s.entry = fn s.args = args - // Create a stack. stack := runtime_alloc(stackSize, nil) // Set up the stack canary, a random number that should be checked when @@ -130,6 +131,7 @@ func StopPanicUnwind(replay, target uintptr) { currentTask.state.panicReplay = replay currentTask.state.panicTarget = target } + panicStackState = stackState{} } func ClearPanicReplay() { @@ -142,6 +144,7 @@ func ClearPanicReplay() { currentTask.state.panicOrigin = stackState{} currentTask.state.panicReplay = 0 currentTask.state.panicTarget = 0 + panicStackState = stackState{} } func PanicRewindData() unsafe.Pointer { @@ -188,6 +191,12 @@ func (t *Task) Resume() { if uintptr(t.state.asyncifysp) > uintptr(t.state.csp) { runtimeFatal("stack overflow") } + if t.Exited { + runtime_freeTaskStack(unsafe.Pointer(t.state.canaryPtr)) + t.state = state{} + t.gcData = gcData{} + t.DeferFrame = nil + } } //go:linkname saveStackPointer runtime.saveStackPointer diff --git a/src/internal/task/task_exit.go b/src/internal/task/task_exit.go index 21e5860a08..2bd008cff1 100644 --- a/src/internal/task/task_exit.go +++ b/src/internal/task/task_exit.go @@ -22,8 +22,16 @@ func Goexit() { exit(true) } +// Exit exits the current task after its entry function returns. +func Exit() { + exit(false) +} + func exit(goexit bool) { t := Current() + if hasReleasableStack { + t.Exited = true + } remaining := atomic.AddUint32(&liveTasks, ^uint32(0)) if t == mainTask { if goexit { diff --git a/src/internal/task/task_stack.go b/src/internal/task/task_stack.go index 23f3b9097f..1ae065795e 100644 --- a/src/internal/task/task_stack.go +++ b/src/internal/task/task_stack.go @@ -28,6 +28,8 @@ type state struct { canaryPtr *uintptr } +const hasReleasableStack = false + //export tinygo_task_exit func taskExit() { exit(false) diff --git a/src/runtime/gc_blocks.go b/src/runtime/gc_blocks.go index 78387cb312..11ed46699b 100644 --- a/src/runtime/gc_blocks.go +++ b/src/runtime/gc_blocks.go @@ -536,6 +536,20 @@ func free(ptr unsafe.Pointer) { // TODO: free blocks on request, when the compiler knows they're unused. } +func freeTaskStack(ptr unsafe.Pointer) { + gcLock.Lock() + firstBlock := blockFromAddr(uintptr(ptr)) + if gcAsserts && firstBlock.pointer() != ptr { + runtimeFatal("gc: freeing pointer inside allocation") + } + lastBlock := firstBlock.findHead() + for block := firstBlock; block <= lastBlock; block++ { + block.free() + } + insertFreeRange(ptr, uintptr(lastBlock-firstBlock+1)) + gcLock.Unlock() +} + // GC performs a garbage collection cycle. func GC() { gcLock.Lock() diff --git a/src/runtime/gc_boehm.go b/src/runtime/gc_boehm.go index e0fc16a67f..9f94904694 100644 --- a/src/runtime/gc_boehm.go +++ b/src/runtime/gc_boehm.go @@ -102,6 +102,10 @@ func free(ptr unsafe.Pointer) { libgc_free(ptr) } +func freeTaskStack(ptr unsafe.Pointer) { + free(ptr) +} + func GC() { gcLock.Lock() libgc_gcollect() diff --git a/src/runtime/gc_custom.go b/src/runtime/gc_custom.go index 0125f1688b..b48605ca8b 100644 --- a/src/runtime/gc_custom.go +++ b/src/runtime/gc_custom.go @@ -47,6 +47,10 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer // free is called to explicitly free a previously allocated pointer. func free(ptr unsafe.Pointer) +func freeTaskStack(ptr unsafe.Pointer) { + free(ptr) +} + // markRoots is called with the start and end addresses to scan for references. // It is currently only called with the top and bottom of the stack. func markRoots(start, end uintptr) diff --git a/src/runtime/gc_leaking.go b/src/runtime/gc_leaking.go index 3f1595ff63..d1dabf640a 100644 --- a/src/runtime/gc_leaking.go +++ b/src/runtime/gc_leaking.go @@ -84,6 +84,9 @@ func free(ptr unsafe.Pointer) { // Memory is never freed. } +func freeTaskStack(ptr unsafe.Pointer) { +} + func markRoots(start, end uintptr) { runtimeFatal("unreachable: markRoots") } diff --git a/src/runtime/gc_none.go b/src/runtime/gc_none.go index ce9649c719..6063eab533 100644 --- a/src/runtime/gc_none.go +++ b/src/runtime/gc_none.go @@ -28,6 +28,9 @@ func free(ptr unsafe.Pointer) { // Nothing to free when nothing gets allocated. } +func freeTaskStack(ptr unsafe.Pointer) { +} + func GC() { // Unimplemented. }