diff --git a/compiler/package.go b/compiler/package.go index f360d9af1..7838e8eb3 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -14,9 +14,9 @@ import ( "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/typesutil" "github.com/neelance/astrewrite" "golang.org/x/tools/go/gcexportdata" - "golang.org/x/tools/go/types/typeutil" ) // pkgContext maintains compiler context for a specific package. @@ -28,8 +28,7 @@ type pkgContext struct { pkgVars map[string]string objectNames map[types.Object]string varPtrNames map[*types.Var]string - anonTypes []*types.TypeName - anonTypeMap typeutil.Map + anonTypes typesutil.AnonymousTypes escapingVars map[*types.Var]bool indentation int dependencies map[types.Object]bool @@ -38,6 +37,14 @@ type pkgContext struct { errList ErrorList } +// genericCtx contains compiler context for a generic function or type. +// +// It is used to accumulate information about types and objects that depend on +// type parameters and must be constructed in a generic factory function. +type genericCtx struct { + anonTypes typesutil.AnonymousTypes +} + func (p *pkgContext) SelectionOf(e *ast.SelectorExpr) (selection, bool) { if sel, ok := p.Selections[e]; ok { return sel, true @@ -78,6 +85,8 @@ type funcContext struct { *analysis.FuncInfo // Surrounding package context. pkgCtx *pkgContext + // Surrounding generic function context. nil if non-generic code. + genericCtx *genericCtx // Function context, surrounding this function definition. For package-level // functions or methods it is the package-level function context (even though // it technically doesn't correspond to a function). nil for the package-level @@ -597,7 +606,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor } // anonymous types - for _, t := range funcCtx.pkgCtx.anonTypes { + for _, t := range funcCtx.pkgCtx.anonTypes.Ordered() { d := Decl{ Vars: []string{t.Name()}, DceObjectFilter: t.Name(), @@ -758,6 +767,7 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, c := &funcContext{ FuncInfo: info, pkgCtx: outerContext.pkgCtx, + genericCtx: outerContext.genericCtx, parent: outerContext, sigTypes: &signatureTypes{Sig: sig}, allVars: make(map[string]int, len(outerContext.allVars)), @@ -769,6 +779,9 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, for k, v := range outerContext.allVars { c.allVars[k] = v } + if c.sigTypes.IsGeneric() { + c.genericCtx = &genericCtx{} + } prevEV := c.pkgCtx.escapingVars var params []string @@ -786,7 +799,7 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, } } - bodyOutput := string(c.CatchOutput(1, func() { + bodyOutput := string(c.CatchOutput(c.bodyIndent(), func() { if len(c.Blocking) != 0 { c.pkgCtx.Scopes[body] = c.pkgCtx.Scopes[typ] c.handleEscapingVars(body) @@ -888,13 +901,13 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, } if prefix != "" { - bodyOutput = c.Indentation(1) + "/* */" + prefix + "\n" + bodyOutput + bodyOutput = c.Indentation(c.bodyIndent()) + "/* */" + prefix + "\n" + bodyOutput } if suffix != "" { - bodyOutput = bodyOutput + c.Indentation(1) + "/* */" + suffix + "\n" + bodyOutput = bodyOutput + c.Indentation(c.bodyIndent()) + "/* */" + suffix + "\n" } if localVarDefs != "" { - bodyOutput = c.Indentation(1) + localVarDefs + bodyOutput + bodyOutput = c.Indentation(c.bodyIndent()) + localVarDefs + bodyOutput } c.pkgCtx.escapingVars = prevEV @@ -907,14 +920,23 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, // from the call site. // TODO(nevkontakte): Cache function instances for a given combination of type // parameters. - // TODO(nevkontakte): Generate type parameter arguments and derive all dependent - // types inside the function. typeParams := []string{} for i := 0; i < c.sigTypes.Sig.TypeParams().Len(); i++ { typeParam := c.sigTypes.Sig.TypeParams().At(i) typeParams = append(typeParams, c.typeName(typeParam)) } - return params, fmt.Sprintf("function%s(%s){ return function(%s) {\n%s%s}; }", - functionName, strings.Join(typeParams, ", "), strings.Join(params, ", "), bodyOutput, c.Indentation(0)) + // anonymous types + typesInit := strings.Builder{} + for _, t := range c.genericCtx.anonTypes.Ordered() { + fmt.Fprintf(&typesInit, "%svar %s = $%sType(%s);\n", c.Indentation(1), t.Name(), strings.ToLower(typeKind(t.Type())[5:]), c.initArgs(t.Type())) + } + + code := &strings.Builder{} + fmt.Fprintf(code, "function%s(%s){\n", functionName, strings.Join(typeParams, ", ")) + fmt.Fprintf(code, "%s", typesInit.String()) + fmt.Fprintf(code, "%sreturn function(%s) {\n", c.Indentation(1), strings.Join(params, ", ")) + fmt.Fprintf(code, "%s", bodyOutput) + fmt.Fprintf(code, "%s};\n%s}", c.Indentation(1), c.Indentation(0)) + return params, code.String() } diff --git a/compiler/typesutil/typesutil.go b/compiler/typesutil/typesutil.go index 600925b81..44671f309 100644 --- a/compiler/typesutil/typesutil.go +++ b/compiler/typesutil/typesutil.go @@ -1,11 +1,18 @@ package typesutil -import "go/types" +import ( + "fmt" + "go/types" + "golang.org/x/tools/go/types/typeutil" +) + +// IsJsPackage returns is the package is github.com/gopherjs/gopherjs/js. func IsJsPackage(pkg *types.Package) bool { return pkg != nil && pkg.Path() == "github.com/gopherjs/gopherjs/js" } +// IsJsObject returns true if the type represents a pointer to github.com/gopherjs/gopherjs/js.Object. func IsJsObject(t types.Type) bool { ptr, isPtr := t.(*types.Pointer) if !isPtr { @@ -14,3 +21,92 @@ func IsJsObject(t types.Type) bool { named, isNamed := ptr.Elem().(*types.Named) return isNamed && IsJsPackage(named.Obj().Pkg()) && named.Obj().Name() == "Object" } + +// AnonymousTypes maintains a mapping between anonymous types encountered in a +// Go program to equivalent synthetic names types GopherJS generated from them. +// +// This enables a runtime performance optimization where different instances of +// the same anonymous type (e.g. in expression `x := map[int]string{}`) don't +// need to initialize type information (e.g. `$mapType($Int, $String)`) every +// time, but reuse the single synthesized type (e.g. `mapType$1`). +type AnonymousTypes struct { + m typeutil.Map + order []*types.TypeName +} + +// Get returns the synthesized type name for the provided anonymous type or nil +// if the type is not registered. +func (at *AnonymousTypes) Get(t types.Type) *types.TypeName { + s, _ := at.m.At(t).(*types.TypeName) + return s +} + +// Ordered returns synthesized type names for the registered anonymous types in +// the order they were registered. +func (at *AnonymousTypes) Ordered() []*types.TypeName { + return at.order +} + +// Register a synthesized type name for an anonymous type. +func (at *AnonymousTypes) Register(name *types.TypeName, anonType types.Type) { + at.m.Set(anonType, name) + at.order = append(at.order, name) +} + +// IsGeneric returns true if the provided type is a type parameter or depends +// on a type parameter. +func IsGeneric(t types.Type) bool { + switch t := t.(type) { + case *types.Array: + return IsGeneric(t.Elem()) + case *types.Basic: + return false + case *types.Chan: + return IsGeneric(t.Elem()) + case *types.Interface: + for i := 0; i < t.NumMethods(); i++ { + if IsGeneric(t.Method(i).Type()) { + return true + } + } + for i := 0; i < t.NumEmbeddeds(); i++ { + if IsGeneric(t.Embedded(i)) { + return true + } + } + return false + case *types.Map: + return IsGeneric(t.Key()) || IsGeneric(t.Elem()) + case *types.Named: + // Named type declarations dependent on a type param are currently not + // supported by the upstream Go compiler. + return false + case *types.Pointer: + return IsGeneric(t.Elem()) + case *types.Slice: + return IsGeneric(t.Elem()) + case *types.Signature: + for i := 0; i < t.Params().Len(); i++ { + if IsGeneric(t.Params().At(i).Type()) { + return true + } + } + for i := 0; i < t.Results().Len(); i++ { + if IsGeneric(t.Results().At(i).Type()) { + return true + } + } + return false + case *types.Struct: + for i := 0; i < t.NumFields(); i++ { + if IsGeneric(t.Field(i).Type()) { + return true + } + } + return false + case *types.TypeParam: + return true + default: + panic(fmt.Errorf("%v has unexpected type %T", t, t)) + } +} diff --git a/compiler/typesutil/typesutil_test.go b/compiler/typesutil/typesutil_test.go new file mode 100644 index 000000000..515cf0a5e --- /dev/null +++ b/compiler/typesutil/typesutil_test.go @@ -0,0 +1,168 @@ +package typesutil + +import ( + "go/token" + "go/types" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestAnonymousTypes(t *testing.T) { + t1 := types.NewSlice(types.Typ[types.String]) + t1Name := types.NewTypeName(token.NoPos, nil, "sliceType$1", t1) + + t2 := types.NewMap(types.Typ[types.Int], t1) + t2Name := types.NewTypeName(token.NoPos, nil, "mapType$1", t2) + + typs := []struct { + typ types.Type + name *types.TypeName + }{ + {typ: t1, name: t1Name}, + {typ: t2, name: t2Name}, + } + + anonTypes := AnonymousTypes{} + for _, typ := range typs { + anonTypes.Register(typ.name, typ.typ) + } + + for _, typ := range typs { + t.Run(typ.name.Name(), func(t *testing.T) { + got := anonTypes.Get(typ.typ) + if got != typ.name { + t.Errorf("Got: anonTypes.Get(%v) = %v. Want: %v.", typ.typ, typ.name, got) + } + }) + } + + gotNames := []string{} + for _, name := range anonTypes.Ordered() { + gotNames = append(gotNames, name.Name()) + } + wantNames := []string{"sliceType$1", "mapType$1"} + if !cmp.Equal(wantNames, gotNames) { + t.Errorf("Got: anonTypes.Ordered() = %v. Want: %v (in the order of registration)", gotNames, wantNames) + } +} + +func TestIsGeneric(t *testing.T) { + T := types.NewTypeParam(types.NewTypeName(token.NoPos, nil, "T", nil), types.NewInterface(nil, nil)) + + tests := []struct { + typ types.Type + want bool + }{ + { + typ: T, + want: true, + }, { + typ: types.Typ[types.Int], + want: false, + }, { + typ: types.NewArray(types.Typ[types.Int], 1), + want: false, + }, { + typ: types.NewArray(T, 1), + want: true, + }, { + typ: types.NewChan(types.SendRecv, types.Typ[types.Int]), + want: false, + }, { + typ: types.NewChan(types.SendRecv, T), + want: true, + }, { + typ: types.NewInterfaceType( + []*types.Func{ + types.NewFunc(token.NoPos, nil, "X", types.NewSignatureType( + nil, nil, nil, types.NewTuple(types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int])), nil, false, + )), + }, + []types.Type{ + types.NewNamed(types.NewTypeName(token.NoPos, nil, "myInt", nil), types.Typ[types.Int], nil), + }, + ), + want: false, + }, { + typ: types.NewInterfaceType( + []*types.Func{ + types.NewFunc(token.NoPos, nil, "X", types.NewSignatureType( + nil, nil, nil, types.NewTuple(types.NewVar(token.NoPos, nil, "x", T)), nil, false, + )), + }, + []types.Type{ + types.NewNamed(types.NewTypeName(token.NoPos, nil, "myInt", nil), types.Typ[types.Int], nil), + }, + ), + want: true, + }, { + typ: types.NewMap(types.Typ[types.Int], types.Typ[types.String]), + want: false, + }, { + typ: types.NewMap(T, types.Typ[types.String]), + want: true, + }, { + typ: types.NewMap(types.Typ[types.Int], T), + want: true, + }, { + typ: types.NewNamed(types.NewTypeName(token.NoPos, nil, "myInt", nil), types.Typ[types.Int], nil), + want: false, + }, { + typ: types.NewPointer(types.Typ[types.Int]), + want: false, + }, { + typ: types.NewPointer(T), + want: true, + }, { + typ: types.NewSlice(types.Typ[types.Int]), + want: false, + }, { + typ: types.NewSlice(T), + want: true, + }, { + typ: types.NewSignatureType( + nil, nil, nil, + types.NewTuple(types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int])), // params + types.NewTuple(types.NewVar(token.NoPos, nil, "", types.Typ[types.String])), // results + false, + ), + want: false, + }, { + typ: types.NewSignatureType( + nil, nil, nil, + types.NewTuple(types.NewVar(token.NoPos, nil, "x", T)), // params + types.NewTuple(types.NewVar(token.NoPos, nil, "", types.Typ[types.String])), // results + false, + ), + want: true, + }, { + typ: types.NewSignatureType( + nil, nil, nil, + types.NewTuple(types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int])), // params + types.NewTuple(types.NewVar(token.NoPos, nil, "", T)), // results + false, + ), + want: true, + }, { + typ: types.NewStruct([]*types.Var{ + types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int]), + }, nil), + want: false, + }, { + typ: types.NewStruct([]*types.Var{ + types.NewVar(token.NoPos, nil, "x", T), + }, nil), + want: true, + }, + } + + for _, test := range tests { + t.Run(test.typ.String(), func(t *testing.T) { + got := IsGeneric(test.typ) + if got != test.want { + t.Errorf("Got: IsGeneric(%v) = %v. Want: %v.", test.typ, got, test.want) + } + }) + } +} diff --git a/compiler/utils.go b/compiler/utils.go index cf59cd573..51cd85773 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -72,6 +72,16 @@ func (fc *funcContext) Indentation(extra int) string { return strings.Repeat("\t", fc.pkgCtx.indentation+extra) } +// bodyIndent returns the number of indentations necessary for the function +// body code. Generic functions need deeper indentation to account for the +// surrounding factory function. +func (fc *funcContext) bodyIndent() int { + if fc.sigTypes.IsGeneric() { + return 2 + } + return 1 +} + func (fc *funcContext) CatchOutput(indent int, f func()) []byte { origoutput := fc.output fc.output = nil @@ -245,7 +255,7 @@ type varLevel int const ( // A variable defined at a function level (e.g. local variables). - varFuncLocal = iota + varFuncLocal varLevel = iota // A variable that should be declared in a generic type or function factory. // This is mainly for type parameters and generic-dependent types. varGenericFactory @@ -430,13 +440,18 @@ func (fc *funcContext) typeName(ty types.Type) string { // declaration, which will be reused for all instances of this type. This // improves performance, since runtime won't have to synthesize the same type // repeatedly. - anonType, ok := fc.pkgCtx.anonTypeMap.At(ty).(*types.TypeName) - if !ok { + anonTypes := &fc.pkgCtx.anonTypes + level := varPackage + if typesutil.IsGeneric(ty) { + anonTypes = &fc.genericCtx.anonTypes + level = varGenericFactory + } + anonType := anonTypes.Get(ty) + if anonType == nil { fc.initArgs(ty) // cause all dependency types to be registered - varName := fc.newVariable(strings.ToLower(typeKind(ty)[5:])+"Type", varPackage) + varName := fc.newVariable(strings.ToLower(typeKind(ty)[5:])+"Type", level) anonType = types.NewTypeName(token.NoPos, fc.pkgCtx.Pkg, varName, ty) // fake types.TypeName - fc.pkgCtx.anonTypes = append(fc.pkgCtx.anonTypes, anonType) - fc.pkgCtx.anonTypeMap.Set(ty, anonType) + anonTypes.Register(anonType, ty) } fc.pkgCtx.dependencies[anonType] = true return anonType.Name()