Skip to content

Commit ec9572f

Browse files
authored
Merge pull request gopherjs#1090 from nevkontakte/go-builtin
Correctly handle built-ins and `js.Object` methods with go keyword.
2 parents 9ae78a3 + 9356bc9 commit ec9572f

File tree

4 files changed

+93
-45
lines changed

4 files changed

+93
-45
lines changed

compiler/expressions.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,62 @@ func (fc *funcContext) translateCall(e *ast.CallExpr, sig *types.Signature, fun
794794
return fc.formatExpr("%s(%s)", fun, strings.Join(args, ", "))
795795
}
796796

797+
// delegatedCall returns a pair of JS expresions representing a callable function
798+
// and its arguments to be invoked elsewhere.
799+
//
800+
// This function is necessary in conjunction with keywords such as `go` and `defer`,
801+
// where we need to compute function and its arguments at the the keyword site,
802+
// but the call itself will happen elsewhere (hence "delegated").
803+
//
804+
// Built-in functions and cetrain `js.Object` methods don't translate into JS
805+
// function calls, and need to be wrapped before they can be delegated, which
806+
// this function handles and returns JS expressions that are safe to delegate
807+
// and behave like a regular JS function and a list of its argument values.
808+
func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, arglist *expression) {
809+
isBuiltin := false
810+
isJs := false
811+
switch fun := expr.Fun.(type) {
812+
case *ast.Ident:
813+
_, isBuiltin = fc.pkgCtx.Uses[fun].(*types.Builtin)
814+
case *ast.SelectorExpr:
815+
isJs = typesutil.IsJsPackage(fc.pkgCtx.Uses[fun.Sel].Pkg())
816+
}
817+
sig := fc.pkgCtx.TypeOf(expr.Fun).Underlying().(*types.Signature)
818+
sigTypes := signatureTypes{Sig: sig}
819+
args := fc.translateArgs(sig, expr.Args, expr.Ellipsis.IsValid())
820+
821+
if !isBuiltin && !isJs {
822+
// Normal function calls don't require wrappers.
823+
callable = fc.translateExpr(expr.Fun)
824+
arglist = fc.formatExpr("[%s]", strings.Join(args, ", "))
825+
return callable, arglist
826+
}
827+
828+
// Since some builtins or js.Object methods may not transpile into
829+
// callable expressions, we need to wrap then in a proxy lambda in order
830+
// to push them onto the deferral stack.
831+
vars := make([]string, len(expr.Args))
832+
callArgs := make([]ast.Expr, len(expr.Args))
833+
ellipsis := expr.Ellipsis
834+
835+
for i := range expr.Args {
836+
v := fc.newVariable("_arg")
837+
vars[i] = v
838+
// Subtle: the proxy lambda argument needs to be assigned with the type
839+
// that the original function expects, and not with the argument
840+
// expression result type, or we may do implicit type conversion twice.
841+
callArgs[i] = fc.newIdent(v, sigTypes.Param(i, ellipsis.IsValid()))
842+
}
843+
wrapper := &ast.CallExpr{
844+
Fun: expr.Fun,
845+
Args: callArgs,
846+
Ellipsis: expr.Ellipsis,
847+
}
848+
callable = fc.formatExpr("function(%s) { %e; }", strings.Join(vars, ", "), wrapper)
849+
arglist = fc.formatExpr("[%s]", strings.Join(args, ", "))
850+
return callable, arglist
851+
}
852+
797853
func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression {
798854
sel, _ := fc.pkgCtx.SelectionOf(e)
799855
if !sel.Obj().Exported() {

compiler/statements.go

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"go/ast"
66
"go/constant"
7+
"go/printer"
78
"go/token"
89
"go/types"
910
"strings"
@@ -39,6 +40,8 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) {
3940
pos = fc.pos
4041
}
4142
fmt.Fprintf(bail, "Occurred while compiling statement at %s:\n", fc.pkgCtx.fileSet.Position(pos))
43+
(&printer.Config{Tabwidth: 2, Indent: 1, Mode: printer.UseSpaces}).Fprint(bail, fc.pkgCtx.fileSet, stmt)
44+
fmt.Fprintf(bail, "\n\nDetailed AST:\n")
4245
ast.Fprint(bail, fc.pkgCtx.fileSet, stmt, ast.NotNilFilter)
4346
panic(bail) // Initiate orderly bailout.
4447
}()
@@ -360,47 +363,8 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) {
360363
return
361364

362365
case *ast.DeferStmt:
363-
isBuiltin := false
364-
isJs := false
365-
switch fun := s.Call.Fun.(type) {
366-
case *ast.Ident:
367-
var builtin *types.Builtin
368-
builtin, isBuiltin = fc.pkgCtx.Uses[fun].(*types.Builtin)
369-
if isBuiltin && builtin.Name() == "recover" {
370-
fc.Printf("$deferred.push([$recover, []]);")
371-
return
372-
}
373-
case *ast.SelectorExpr:
374-
isJs = typesutil.IsJsPackage(fc.pkgCtx.Uses[fun.Sel].Pkg())
375-
}
376-
sig := fc.pkgCtx.TypeOf(s.Call.Fun).Underlying().(*types.Signature)
377-
sigTypes := signatureTypes{Sig: sig}
378-
args := fc.translateArgs(sig, s.Call.Args, s.Call.Ellipsis.IsValid())
379-
if isBuiltin || isJs {
380-
// Since some builtins or js.Object methods may not transpile into
381-
// callable expressions, we need to wrap then in a proxy lambda in order
382-
// to push them onto the deferral stack.
383-
vars := make([]string, len(s.Call.Args))
384-
callArgs := make([]ast.Expr, len(s.Call.Args))
385-
ellipsis := s.Call.Ellipsis
386-
387-
for i := range s.Call.Args {
388-
v := fc.newVariable("_arg")
389-
vars[i] = v
390-
// Subtle: the proxy lambda argument needs to be assigned with the type
391-
// that the original function expects, and not with the argument
392-
// expression result type, or we may do implicit type conversion twice.
393-
callArgs[i] = fc.newIdent(v, sigTypes.Param(i, ellipsis.IsValid()))
394-
}
395-
call := fc.translateExpr(&ast.CallExpr{
396-
Fun: s.Call.Fun,
397-
Args: callArgs,
398-
Ellipsis: s.Call.Ellipsis,
399-
})
400-
fc.Printf("$deferred.push([function(%s) { %s; }, [%s]]);", strings.Join(vars, ", "), call, strings.Join(args, ", "))
401-
return
402-
}
403-
fc.Printf("$deferred.push([%s, [%s]]);", fc.translateExpr(s.Call.Fun), strings.Join(args, ", "))
366+
callable, arglist := fc.delegatedCall(s.Call)
367+
fc.Printf("$deferred.push([%s, %s]);", callable, arglist)
404368

405369
case *ast.AssignStmt:
406370
if s.Tok != token.ASSIGN && s.Tok != token.DEFINE {
@@ -496,7 +460,8 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) {
496460
fc.translateStmt(s.Stmt, label)
497461

498462
case *ast.GoStmt:
499-
fc.Printf("$go(%s, [%s]);", fc.translateExpr(s.Call.Fun), strings.Join(fc.translateArgs(fc.pkgCtx.TypeOf(s.Call.Fun).Underlying().(*types.Signature), s.Call.Args, s.Call.Ellipsis.IsValid()), ", "))
463+
callable, arglist := fc.delegatedCall(s.Call)
464+
fc.Printf("$go(%s, %s);", callable, arglist)
500465

501466
case *ast.SendStmt:
502467
chanType := fc.pkgCtx.TypeOf(s.Chan).Underlying().(*types.Chan)

compiler/utils.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"go/token"
1111
"go/types"
1212
"net/url"
13+
"regexp"
1314
"runtime/debug"
1415
"sort"
1516
"strconv"
@@ -791,12 +792,14 @@ func (b *FatalError) Write(p []byte) (n int, err error) { return b.clues.Write(p
791792

792793
func (b FatalError) Error() string {
793794
buf := &strings.Builder{}
794-
fmt.Fprintln(buf, "[compiler panic] ", b.Unwrap())
795+
fmt.Fprintln(buf, "[compiler panic] ", strings.TrimSpace(b.Unwrap().Error()))
795796
if b.clues.Len() > 0 {
796-
fmt.Fprintln(buf, "\n", b.clues.String())
797+
fmt.Fprintln(buf, "\n"+b.clues.String())
797798
}
798799
if len(b.stack) > 0 {
799-
fmt.Fprintln(buf, "\n", string(b.stack))
800+
// Shift stack track by 2 spaces for better readability.
801+
stack := regexp.MustCompile("(?m)^").ReplaceAll(b.stack, []byte(" "))
802+
fmt.Fprintln(buf, "\nOriginal stack trace:\n", string(stack))
800803
}
801804
return buf.String()
802805
}

tests/goroutine_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package tests
33
import (
44
"context"
55
"fmt"
6+
"runtime"
67
"testing"
78
"time"
89

@@ -265,3 +266,26 @@ func TestEventLoopStarvation(t *testing.T) {
265266
}()
266267
<-ctx.Done()
267268
}
269+
270+
func TestGoroutineBuiltin(t *testing.T) {
271+
// Test that a built-in function can be a goroutine body.
272+
// https://github.com/gopherjs/gopherjs/issues/547.
273+
c := make(chan bool)
274+
go close(c)
275+
<-c // Wait until goroutine executes successfully.
276+
}
277+
278+
func TestGoroutineJsObject(t *testing.T) {
279+
// Test that js.Object methods can be a goroutine body.
280+
// https://github.com/gopherjs/gopherjs/issues/547.
281+
if !(runtime.GOOS == "js" || runtime.GOARCH == "js") {
282+
t.Skip("Test requires GopherJS")
283+
}
284+
o := js.Global.Get("Object").New()
285+
go o.Set("x", "y")
286+
// Wait until the goroutine executes successfully. Can't use locks here
287+
// because goroutine body must be a bare js.Object method call.
288+
for o.Get("x").String() != "y" {
289+
runtime.Gosched()
290+
}
291+
}

0 commit comments

Comments
 (0)