From 3d13f9cfe198e44aec81b7cf0d79061509410e9b Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 5 Mar 2021 14:07:49 +0100 Subject: [PATCH] transform: show better error message in coroutines lowering A common error is when someone tries to export a blocking function. This is not possible with the coroutines scheduler. Previously, it resulted in an error like this: panic: trying to make exported function async: messageHandler With this change, the error is much better and shows where it comes from exactly: /home/ayke/tmp/export-async.go:8: blocking operation in exported function: messageHandler traceback: messageHandler /home/ayke/tmp/export-async.go:9:5 main.foo /home/ayke/tmp/export-async.go:15:2 runtime.chanSend /home/ayke/src/github.com/tinygo-org/tinygo/src/runtime/chan.go:494:12 This should make it easier to identify and fix the problem. And it avoids a compiler panic, which is a really bad way of showing diagnostics. --- main.go | 10 ++++++++ transform/coroutines.go | 56 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index aa81c35e..2b52c939 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ import ( "github.com/tinygo-org/tinygo/goenv" "github.com/tinygo-org/tinygo/interp" "github.com/tinygo-org/tinygo/loader" + "github.com/tinygo-org/tinygo/transform" "tinygo.org/x/go-llvm" "go.bug.st/serial" @@ -776,6 +777,15 @@ func printCompilerError(logln func(...interface{}), err error) { logln() } } + case transform.CoroutinesError: + logln(err.Pos.String() + ": " + err.Msg) + logln("\ntraceback:") + for _, line := range err.Traceback { + logln(line.Name) + if line.Position.IsValid() { + logln("\t" + line.Position.String()) + } + } case loader.Errors: logln("#", err.Pkg.ImportPath) for _, err := range err.Errs { diff --git a/transform/coroutines.go b/transform/coroutines.go index 3756da25..2aeff43c 100644 --- a/transform/coroutines.go +++ b/transform/coroutines.go @@ -5,6 +5,7 @@ package transform import ( "errors" + "go/token" "strconv" "github.com/tinygo-org/tinygo/compiler/llvmutil" @@ -104,6 +105,31 @@ func LowerCoroutines(mod llvm.Module, needStackSlots bool) error { return nil } +// CoroutinesError is an error returned when coroutine lowering failed, for +// example because an async function is exported. +type CoroutinesError struct { + Msg string + Pos token.Position + Traceback []CoroutinesErrorLine +} + +// CoroutinesErrorLine is a single line of a CoroutinesError traceback. +type CoroutinesErrorLine struct { + Name string // function name + Position token.Position // position in the function +} + +// Error implements the error interface by returning a simple error message +// without the stack. +func (err CoroutinesError) Error() string { + return err.Msg +} + +type asyncCallInfo struct { + fn llvm.Value + call llvm.Value +} + // asyncFunc is a metadata container for an asynchronous function. type asyncFunc struct { // fn is the underlying function pointer. @@ -168,10 +194,11 @@ type coroutineLoweringPass struct { // findAsyncFuncs finds all asynchronous functions. // A function is considered asynchronous if it calls an asynchronous function or intrinsic. -func (c *coroutineLoweringPass) findAsyncFuncs() { +func (c *coroutineLoweringPass) findAsyncFuncs() error { asyncs := map[llvm.Value]*asyncFunc{} asyncsOrdered := []llvm.Value{} calls := []llvm.Value{} + callsAsyncFunction := map[llvm.Value]asyncCallInfo{} // Use a breadth-first search to find all async functions. worklist := []llvm.Value{c.pause} @@ -183,7 +210,18 @@ func (c *coroutineLoweringPass) findAsyncFuncs() { // Get task pointer argument. task := fn.LastParam() if fn != c.pause && (task.IsNil() || task.Name() != "parentHandle") { - panic("trying to make exported function async: " + fn.Name()) + // Exported functions must not do async operations. + err := CoroutinesError{ + Msg: "blocking operation in exported function: " + fn.Name(), + Pos: getPosition(fn), + } + f := fn + for !f.IsNil() && f != c.pause { + data := callsAsyncFunction[f] + err.Traceback = append(err.Traceback, CoroutinesErrorLine{f.Name(), getPosition(data.call)}) + f = data.fn + } + return err } // Search all uses of the function while collecting callers. @@ -218,6 +256,13 @@ func (c *coroutineLoweringPass) findAsyncFuncs() { asyncs[caller] = nil asyncsOrdered = append(asyncsOrdered, caller) + // Track which calls caused this function to be marked async (for + // better diagnostics). + callsAsyncFunction[caller] = asyncCallInfo{ + fn: fn, + call: user, + } + // Put the caller on the worklist. worklist = append(worklist, caller) } @@ -243,6 +288,8 @@ func (c *coroutineLoweringPass) findAsyncFuncs() { c.asyncFuncs = asyncs c.asyncFuncsOrdered = asyncFuncsOrdered c.calls = calls + + return nil } func (c *coroutineLoweringPass) load() error { @@ -302,7 +349,10 @@ func (c *coroutineLoweringPass) load() error { } // Find async functions. - c.findAsyncFuncs() + err := c.findAsyncFuncs() + if err != nil { + return err + } // Get i8* type. c.i8ptr = llvm.PointerType(c.ctx.Int8Type(), 0)