Skip to content

Commit 1496ad4

Browse files
committed
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.
1 parent 16fc1ea commit 1496ad4

48 files changed

Lines changed: 1406 additions & 260 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

GNUmakefile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,15 +510,14 @@ TEST_PACKAGES_HOST := $(TEST_PACKAGES_FAST) $(TEST_PACKAGES_WINDOWS)
510510
TEST_IOFS := false
511511
endif
512512

513-
TEST_SKIP_FLAG := -skip='TestExtraMethods|TestParseAndBytesRoundTrip/P256/Generic|TestAsValidation'
513+
TEST_SKIP_FLAG := -skip='TestExtraMethods|TestAsValidation'
514514
TEST_ADDITIONAL_FLAGS ?=
515515

516516
# Test known-working standard library packages.
517517
# TODO: parallelize, and only show failing tests (no implied -v flag).
518518
.PHONY: tinygo-test
519519
tinygo-test:
520520
@# TestExtraMethods: used by many crypto packages and uses reflect.Type.Method which is not implemented.
521-
@# TestParseAndBytesRoundTrip/P256/Generic: needs Goexit to run defers on wasm.
522521
$(TINYGO) test $(TEST_ADDITIONAL_FLAGS) $(TEST_SKIP_FLAG) $(filter-out encoding/xml,$(TEST_PACKAGES_HOST)) $(TEST_PACKAGES_SLOW)
523522
ifeq ($(TEST_ENCODING_XML),true)
524523
$(TINYGO) test $(TEST_ADDITIONAL_FLAGS) $(TEST_SKIP_FLAG) -stack-size=16MB encoding/xml

builder/build.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
216216
Debug: !config.Options.SkipDWARF, // emit DWARF except when -internal-nodwarf is passed
217217
Nobounds: config.Options.Nobounds,
218218
PanicStrategy: config.PanicStrategy(),
219+
PanicUnwind: config.PanicUnwind(),
219220
}
220221

221222
// Load the target machine, which is the LLVM object that contains all

builder/config.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package builder
22

33
import (
4+
"errors"
45
"fmt"
56
"runtime"
67

@@ -54,10 +55,24 @@ func NewConfig(options *compileopts.Options) (*compileopts.Config, error) {
5455
return nil, fmt.Errorf("cannot compile with Go toolchain version go%d.%d (TinyGo was built using toolchain version %s)", gorootMajor, gorootMinor, runtime.Version())
5556
}
5657

57-
return &compileopts.Config{
58+
config := &compileopts.Config{
5859
Options: options,
5960
Target: spec,
6061
GoMinorVersion: gorootMinor,
6162
TestConfig: options.TestConfig,
62-
}, nil
63+
}
64+
requestedPanicUnwind := options.PanicUnwind
65+
if requestedPanicUnwind == "" {
66+
requestedPanicUnwind = spec.PanicUnwind
67+
}
68+
if requestedPanicUnwind == "explicit" && config.Scheduler() == "asyncify" {
69+
return nil, errors.New("explicit panic unwinding cannot be used with the asyncify scheduler")
70+
}
71+
if config.PanicUnwind() == "explicit" && !config.SupportsExplicitUnwind() {
72+
return nil, fmt.Errorf("explicit panic unwinding is not supported on %s", config.Triple())
73+
}
74+
if config.PanicUnwind() == "explicit" && config.Scheduler() == "threads" {
75+
return nil, errors.New("explicit panic unwinding is not supported with the threads scheduler")
76+
}
77+
return config, nil
6378
}

builder/sizes_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ func TestBinarySize(t *testing.T) {
4242
// This is a small number of very diverse targets that we want to test.
4343
tests := []sizeTest{
4444
// microcontrollers
45-
{"hifive1b", "examples/echo", 4313, 323, 0, 2260},
46-
{"microbit", "examples/serial", 2838, 382, 8, 2256},
47-
{"wioterminal", "examples/pininterrupt", 8027, 1665, 132, 7488},
45+
{"hifive1b", "examples/echo", 4301, 323, 0, 2260},
46+
{"microbit", "examples/serial", 2834, 382, 8, 2256},
47+
{"wioterminal", "examples/pininterrupt", 7991, 1665, 132, 7488},
4848

4949
// TODO: also check wasm. Right now this is difficult, because
5050
// wasm binaries are run through wasm-opt and therefore the

compileopts/config.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ func (c *Config) BuildTags() []string {
113113
"osusergo", // to get os/user to work
114114
"math_big_pure_go", // to get math/big to work
115115
"gc." + c.GC(), "scheduler." + c.Scheduler(), // used inside the runtime package
116+
"tinygo.unwind." + c.PanicUnwind(),
116117
"serial." + c.Serial()}...) // used inside the machine package
117118
switch c.Scheduler() {
118119
case "threads", "cores":
@@ -202,6 +203,42 @@ func (c *Config) PanicStrategy() string {
202203
return c.Options.PanicStrategy
203204
}
204205

206+
// PanicUnwind returns the mechanism used to unwind panics and Goexit. Asyncify
207+
// provides unwinding whenever that scheduler is selected. Explicit
208+
// return-based unwinding must be requested by the command line or target
209+
// specification.
210+
func (c *Config) PanicUnwind() string {
211+
if c.Scheduler() == "asyncify" {
212+
return "asyncify"
213+
}
214+
requested := c.Target.PanicUnwind
215+
if c.Options.PanicUnwind != "" {
216+
requested = c.Options.PanicUnwind
217+
}
218+
if requested == "explicit" {
219+
return "explicit"
220+
}
221+
arch, _, _ := strings.Cut(c.Triple(), "-")
222+
switch arch {
223+
case "wasm32", "xtensa":
224+
return "none"
225+
default:
226+
return "setjmp"
227+
}
228+
}
229+
230+
// SupportsExplicitUnwind reports whether the target can use return-based
231+
// panic unwinding.
232+
func (c *Config) SupportsExplicitUnwind() bool {
233+
arch, _, _ := strings.Cut(c.Triple(), "-")
234+
switch arch {
235+
case "wasm32", "riscv64", "xtensa":
236+
return true
237+
default:
238+
return false
239+
}
240+
}
241+
205242
// AutomaticStackSize returns whether goroutine stack sizes should be determined
206243
// automatically at compile time, if possible. If it is false, no attempt is
207244
// made.

compileopts/options.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ var (
1515
validSerialOptions = []string{"none", "uart", "usb", "rtt"}
1616
validPrintSizeOptions = []string{"none", "short", "full", "html"}
1717
validPanicStrategyOptions = []string{"print", "trap"}
18+
validPanicUnwindOptions = []string{"auto", "explicit"}
1819
validOptOptions = []string{"none", "0", "1", "2", "s", "z"}
1920
)
2021

@@ -32,6 +33,7 @@ type Options struct {
3233
Opt string
3334
GC string
3435
PanicStrategy string
36+
PanicUnwind string
3537
Scheduler string
3638
StackSize uint64 // goroutine stack size (if none could be automatically determined)
3739
Serial string
@@ -120,6 +122,15 @@ func (o *Options) Verify() error {
120122
}
121123
}
122124

125+
if o.PanicUnwind != "" {
126+
valid := slices.Contains(validPanicUnwindOptions, o.PanicUnwind)
127+
if !valid {
128+
return fmt.Errorf(`invalid panic-unwind option '%s': valid values are %s`,
129+
o.PanicUnwind,
130+
strings.Join(validPanicUnwindOptions, ", "))
131+
}
132+
}
133+
123134
if o.Opt != "" {
124135
if !slices.Contains(validOptOptions, o.Opt) {
125136
return fmt.Errorf("invalid -opt=%s: valid values are %s", o.Opt, strings.Join(validOptOptions, ", "))

compileopts/options_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func TestVerifyOptions(t *testing.T) {
1313
expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify, threads, cores`)
1414
expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full, html`)
1515
expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`)
16+
expectedPanicUnwindError := errors.New(`invalid panic-unwind option 'incorrect': valid values are auto, explicit`)
1617

1718
testCases := []struct {
1819
name string
@@ -117,6 +118,25 @@ func TestVerifyOptions(t *testing.T) {
117118
PanicStrategy: "trap",
118119
},
119120
},
121+
{
122+
name: "InvalidPanicUnwindOption",
123+
opts: compileopts.Options{
124+
PanicUnwind: "incorrect",
125+
},
126+
expectedError: expectedPanicUnwindError,
127+
},
128+
{
129+
name: "PanicUnwindOptionAuto",
130+
opts: compileopts.Options{
131+
PanicUnwind: "auto",
132+
},
133+
},
134+
{
135+
name: "PanicUnwindOptionExplicit",
136+
opts: compileopts.Options{
137+
PanicUnwind: "explicit",
138+
},
139+
},
120140
}
121141

122142
for _, tc := range testCases {

compileopts/target.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type TargetSpec struct {
3535
BuildTags []string `json:"build-tags,omitempty"`
3636
BuildMode string `json:"buildmode,omitempty"` // default build mode (if nothing specified)
3737
GC string `json:"gc,omitempty"`
38+
PanicUnwind string `json:"panic-unwind,omitempty"`
3839
Scheduler string `json:"scheduler,omitempty"`
3940
Serial string `json:"serial,omitempty"` // which serial output to use (uart, usb, none)
4041
Linker string `json:"linker,omitempty"`
@@ -224,6 +225,11 @@ func LoadTarget(options *Options) (*TargetSpec, error) {
224225
if err != nil {
225226
return nil, fmt.Errorf("%s : %w", options.Target, err)
226227
}
228+
switch spec.PanicUnwind {
229+
case "", "auto", "explicit":
230+
default:
231+
return nil, fmt.Errorf("%s: invalid panic-unwind option %q", options.Target, spec.PanicUnwind)
232+
}
227233

228234
if spec.Scheduler == "asyncify" {
229235
spec.ExtraFiles = append(spec.ExtraFiles, "src/internal/task/task_asyncify_wasm.S")

compileopts/target_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,49 @@ func TestConfigLinkerFlavor(t *testing.T) {
161161
})
162162
}
163163
}
164+
165+
func TestConfigPanicUnwind(t *testing.T) {
166+
tests := []struct {
167+
name string
168+
options Options
169+
target TargetSpec
170+
want string
171+
}{
172+
{
173+
name: "native defaults to setjmp",
174+
target: TargetSpec{Triple: "x86_64-unknown-linux"},
175+
want: "setjmp",
176+
},
177+
{
178+
name: "riscv64 defaults to setjmp",
179+
target: TargetSpec{Triple: "riscv64-unknown-unknown"},
180+
want: "setjmp",
181+
},
182+
{
183+
name: "explicit command line opt in",
184+
options: Options{PanicUnwind: "explicit"},
185+
target: TargetSpec{Triple: "riscv64-unknown-unknown"},
186+
want: "explicit",
187+
},
188+
{
189+
name: "auto overrides target opt in",
190+
options: Options{PanicUnwind: "auto"},
191+
target: TargetSpec{Triple: "riscv64-unknown-unknown", PanicUnwind: "explicit"},
192+
want: "setjmp",
193+
},
194+
{
195+
name: "asyncify enables unwinding",
196+
options: Options{Scheduler: "asyncify"},
197+
target: TargetSpec{Triple: "wasm32-unknown-unknown", PanicUnwind: "auto"},
198+
want: "asyncify",
199+
},
200+
}
201+
for _, tc := range tests {
202+
t.Run(tc.name, func(t *testing.T) {
203+
config := Config{Options: &tc.options, Target: &tc.target}
204+
if got := config.PanicUnwind(); got != tc.want {
205+
t.Fatalf("got %q, want %q", got, tc.want)
206+
}
207+
})
208+
}
209+
}

compiler/asserts.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,11 +263,13 @@ func (b *builder) getRuntimeAssertBlock(blockPrefix, assertFunc string) llvm.Bas
263263
block := b.ctx.AddBasicBlock(b.llvmFn, blockPrefix+".throw")
264264
b.runtimeAssertBlocks[assertFunc] = block
265265
b.SetInsertPointAtEnd(block)
266+
b.inFaultBlock = true
266267
if b.hasDeferFrame() {
267268
b.createFaultCheckpoint()
268269
}
269-
b.createRuntimeCall(assertFunc, nil, "")
270-
b.CreateUnreachable()
270+
b.createRuntimeInvoke(assertFunc, nil, "")
271+
b.createUnwindReturnOrUnreachable()
272+
b.inFaultBlock = false
271273
b.SetInsertPointAtEnd(savedBlock)
272274
return block
273275
}

0 commit comments

Comments
 (0)