Skip to content

Commit 3d13f9c

Browse files
aykevldeadprogram
authored andcommitted
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.
1 parent c4191da commit 3d13f9c

File tree

2 files changed

+63
-3
lines changed

2 files changed

+63
-3
lines changed

main.go

+10
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/tinygo-org/tinygo/goenv"
2626
"github.com/tinygo-org/tinygo/interp"
2727
"github.com/tinygo-org/tinygo/loader"
28+
"github.com/tinygo-org/tinygo/transform"
2829
"tinygo.org/x/go-llvm"
2930

3031
"go.bug.st/serial"
@@ -776,6 +777,15 @@ func printCompilerError(logln func(...interface{}), err error) {
776777
logln()
777778
}
778779
}
780+
case transform.CoroutinesError:
781+
logln(err.Pos.String() + ": " + err.Msg)
782+
logln("\ntraceback:")
783+
for _, line := range err.Traceback {
784+
logln(line.Name)
785+
if line.Position.IsValid() {
786+
logln("\t" + line.Position.String())
787+
}
788+
}
779789
case loader.Errors:
780790
logln("#", err.Pkg.ImportPath)
781791
for _, err := range err.Errs {

transform/coroutines.go

+53-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package transform
55

66
import (
77
"errors"
8+
"go/token"
89
"strconv"
910

1011
"github.com/tinygo-org/tinygo/compiler/llvmutil"
@@ -104,6 +105,31 @@ func LowerCoroutines(mod llvm.Module, needStackSlots bool) error {
104105
return nil
105106
}
106107

108+
// CoroutinesError is an error returned when coroutine lowering failed, for
109+
// example because an async function is exported.
110+
type CoroutinesError struct {
111+
Msg string
112+
Pos token.Position
113+
Traceback []CoroutinesErrorLine
114+
}
115+
116+
// CoroutinesErrorLine is a single line of a CoroutinesError traceback.
117+
type CoroutinesErrorLine struct {
118+
Name string // function name
119+
Position token.Position // position in the function
120+
}
121+
122+
// Error implements the error interface by returning a simple error message
123+
// without the stack.
124+
func (err CoroutinesError) Error() string {
125+
return err.Msg
126+
}
127+
128+
type asyncCallInfo struct {
129+
fn llvm.Value
130+
call llvm.Value
131+
}
132+
107133
// asyncFunc is a metadata container for an asynchronous function.
108134
type asyncFunc struct {
109135
// fn is the underlying function pointer.
@@ -168,10 +194,11 @@ type coroutineLoweringPass struct {
168194

169195
// findAsyncFuncs finds all asynchronous functions.
170196
// A function is considered asynchronous if it calls an asynchronous function or intrinsic.
171-
func (c *coroutineLoweringPass) findAsyncFuncs() {
197+
func (c *coroutineLoweringPass) findAsyncFuncs() error {
172198
asyncs := map[llvm.Value]*asyncFunc{}
173199
asyncsOrdered := []llvm.Value{}
174200
calls := []llvm.Value{}
201+
callsAsyncFunction := map[llvm.Value]asyncCallInfo{}
175202

176203
// Use a breadth-first search to find all async functions.
177204
worklist := []llvm.Value{c.pause}
@@ -183,7 +210,18 @@ func (c *coroutineLoweringPass) findAsyncFuncs() {
183210
// Get task pointer argument.
184211
task := fn.LastParam()
185212
if fn != c.pause && (task.IsNil() || task.Name() != "parentHandle") {
186-
panic("trying to make exported function async: " + fn.Name())
213+
// Exported functions must not do async operations.
214+
err := CoroutinesError{
215+
Msg: "blocking operation in exported function: " + fn.Name(),
216+
Pos: getPosition(fn),
217+
}
218+
f := fn
219+
for !f.IsNil() && f != c.pause {
220+
data := callsAsyncFunction[f]
221+
err.Traceback = append(err.Traceback, CoroutinesErrorLine{f.Name(), getPosition(data.call)})
222+
f = data.fn
223+
}
224+
return err
187225
}
188226

189227
// Search all uses of the function while collecting callers.
@@ -218,6 +256,13 @@ func (c *coroutineLoweringPass) findAsyncFuncs() {
218256
asyncs[caller] = nil
219257
asyncsOrdered = append(asyncsOrdered, caller)
220258

259+
// Track which calls caused this function to be marked async (for
260+
// better diagnostics).
261+
callsAsyncFunction[caller] = asyncCallInfo{
262+
fn: fn,
263+
call: user,
264+
}
265+
221266
// Put the caller on the worklist.
222267
worklist = append(worklist, caller)
223268
}
@@ -243,6 +288,8 @@ func (c *coroutineLoweringPass) findAsyncFuncs() {
243288
c.asyncFuncs = asyncs
244289
c.asyncFuncsOrdered = asyncFuncsOrdered
245290
c.calls = calls
291+
292+
return nil
246293
}
247294

248295
func (c *coroutineLoweringPass) load() error {
@@ -302,7 +349,10 @@ func (c *coroutineLoweringPass) load() error {
302349
}
303350

304351
// Find async functions.
305-
c.findAsyncFuncs()
352+
err := c.findAsyncFuncs()
353+
if err != nil {
354+
return err
355+
}
306356

307357
// Get i8* type.
308358
c.i8ptr = llvm.PointerType(c.ctx.Int8Type(), 0)

0 commit comments

Comments
 (0)