diff --git a/compiler/analysis/info.go b/compiler/analysis/info.go index 304c8808a..5ff7f0d71 100644 --- a/compiler/analysis/info.go +++ b/compiler/analysis/info.go @@ -9,6 +9,7 @@ import ( "github.com/gopherjs/gopherjs/compiler/astutil" "github.com/gopherjs/gopherjs/compiler/typesutil" + "golang.org/x/exp/typeparams" ) type continueStmt struct { @@ -342,8 +343,8 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { return nil // No need to walk under this CallExpr, we already did it manually. default: if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { - // This is a type assertion, not a call. Type assertion itself is not - // blocking, but we will visit the expression itself. + // This is a type conversion, not a call. Type assertion itself is not + // blocking, but we will visit the input expression. } else { // The function is returned by a non-trivial expression. We have to be // conservative and assume that function might be blocking. @@ -357,6 +358,7 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { func (fi *FuncInfo) callToNamedFunc(callee types.Object) { switch o := callee.(type) { case *types.Func: + o = typeparams.OriginMethod(o) // TODO(nevkontakte): Can be replaced with o.Origin() in Go 1.19. if recv := o.Type().(*types.Signature).Recv(); recv != nil { if _, ok := recv.Type().Underlying().(*types.Interface); ok { // Conservatively assume that an interface implementation may be blocking. diff --git a/compiler/astutil/astutil.go b/compiler/astutil/astutil.go index 5cfe2dbd3..9ff88a48c 100644 --- a/compiler/astutil/astutil.go +++ b/compiler/astutil/astutil.go @@ -35,7 +35,15 @@ func NewIdent(name string, t types.Type, info *types.Info, pkg *types.Package) * return ident } +// IsTypeExpr returns true if expr denotes a type. This can be used to +// distinguish between calls and type conversions. func IsTypeExpr(expr ast.Expr, info *types.Info) bool { + // Note that we could've used info.Types[expr].IsType() instead of doing our + // own analysis. However, that creates a problem because we synthesize some + // *ast.CallExpr nodes and, more importantly, *ast.Ident nodes that denote a + // type. Unfortunately, because the flag that controls + // types.TypeAndValue.IsType() return value is unexported we wouldn't be able + // to set it correctly. Thus, we can't rely on IsType(). switch e := expr.(type) { case *ast.ArrayType, *ast.ChanType, *ast.FuncType, *ast.InterfaceType, *ast.MapType, *ast.StructType: return true @@ -47,6 +55,20 @@ func IsTypeExpr(expr ast.Expr, info *types.Info) bool { case *ast.SelectorExpr: _, ok := info.Uses[e.Sel].(*types.TypeName) return ok + case *ast.IndexExpr: + ident, ok := e.X.(*ast.Ident) + if !ok { + return false + } + _, ok = info.Uses[ident].(*types.TypeName) + return ok + case *ast.IndexListExpr: + ident, ok := e.X.(*ast.Ident) + if !ok { + return false + } + _, ok = info.Uses[ident].(*types.TypeName) + return ok case *ast.ParenExpr: return IsTypeExpr(e.X, info) default: diff --git a/compiler/compiler.go b/compiler/compiler.go index 0588a923c..1614b8a7f 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -17,6 +17,7 @@ import ( "strings" "time" + "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/gopherjs/gopherjs/compiler/prelude" "golang.org/x/tools/go/gcexportdata" ) @@ -112,9 +113,13 @@ type Decl struct { // Go compiler/linker toolchain. Used by GopherJS to support go:linkname // directives. Must be set for decls that are supported by go:linkname // implementation. - LinkingName SymName + LinkingName symbol.Name // A list of package-level JavaScript variable names this symbol needs to declare. Vars []string + // A JS expression by which the object represented by this decl may be + // referenced within the package context. Empty if the decl represents no such + // object. + RefExpr string // NamedRecvType is method named recv declare. NamedRecvType string // JavaScript code that declares basic information about a symbol. For a type @@ -326,7 +331,7 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameS if recv, method, ok := d.LinkingName.IsMethod(); ok { code = fmt.Sprintf("\t$linknames[%q] = $unsafeMethodToFunction(%v,%q,%t);\n", d.LinkingName.String(), d.NamedRecvType, method, strings.HasPrefix(recv, "*")) } else { - code = fmt.Sprintf("\t$linknames[%q] = %s;\n", d.LinkingName.String(), d.Vars[0]) + code = fmt.Sprintf("\t$linknames[%q] = %s;\n", d.LinkingName.String(), d.RefExpr) } if _, err := w.Write(removeWhitespace([]byte(code), minify)); err != nil { return err @@ -357,7 +362,7 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameS if !found { continue // The symbol is not affected by a go:linkname directive. } - lines = append(lines, fmt.Sprintf("\t\t%s = $linknames[%q];\n", d.Vars[0], impl.String())) + lines = append(lines, fmt.Sprintf("\t\t%s = $linknames[%q];\n", d.RefExpr, impl.String())) } if len(lines) > 0 { code := fmt.Sprintf("\t$pkg.$initLinknames = function() {\n%s};\n", strings.Join(lines, "")) diff --git a/compiler/expressions.go b/compiler/expressions.go index 21971ab5f..cb0b656f6 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -13,6 +13,7 @@ import ( "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) @@ -33,7 +34,7 @@ func (e *expression) StringWithParens() string { } func (fc *funcContext) translateExpr(expr ast.Expr) *expression { - exprType := fc.pkgCtx.TypeOf(expr) + exprType := fc.typeOf(expr) if value := fc.pkgCtx.Types[expr].Value; value != nil { basic := exprType.Underlying().(*types.Basic) switch { @@ -76,19 +77,16 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } } - var obj types.Object + var inst typeparams.Instance switch e := expr.(type) { case *ast.SelectorExpr: - obj = fc.pkgCtx.Uses[e.Sel] + inst = fc.instanceOf(e.Sel) case *ast.Ident: - obj = fc.pkgCtx.Defs[e] - if obj == nil { - obj = fc.pkgCtx.Uses[e] - } + inst = fc.instanceOf(e) } - if obj != nil && typesutil.IsJsPackage(obj.Pkg()) { - switch obj.Name() { + if inst.Object != nil && typesutil.IsJsPackage(inst.Object.Pkg()) { + switch inst.Object.Name() { case "Global": return fc.formatExpr("$global") case "Module": @@ -203,11 +201,16 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } case *ast.FuncLit: - _, fun := translateFunction(e.Type, nil, e.Body, fc, exprType.(*types.Signature), fc.pkgCtx.FuncLitInfos[e], "") + _, fun := translateFunction(e.Type, nil, e.Body, fc, exprType.(*types.Signature), fc.pkgCtx.FuncLitInfos[e], "", typeparams.Instance{}) if len(fc.pkgCtx.escapingVars) != 0 { names := make([]string, 0, len(fc.pkgCtx.escapingVars)) for obj := range fc.pkgCtx.escapingVars { - names = append(names, fc.pkgCtx.objectNames[obj]) + name, ok := fc.assignedObjectName(obj) + if !ok { + // This should never happen. + panic(fmt.Errorf("escaping variable %s hasn't been assigned a JS name", obj)) + } + names = append(names, name) } sort.Strings(names) list := strings.Join(names, ", ") @@ -216,7 +219,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { return fc.formatExpr("(%s)", fun) case *ast.UnaryExpr: - t := fc.pkgCtx.TypeOf(e.X) + t := fc.typeOf(e.X) switch e.Op { case token.AND: if typesutil.IsJsObject(exprType) { @@ -236,26 +239,31 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { switch x := astutil.RemoveParens(e.X).(type) { case *ast.CompositeLit: - return fc.formatExpr("$newDataPointer(%e, %s)", x, fc.typeName(fc.pkgCtx.TypeOf(e))) + return fc.formatExpr("$newDataPointer(%e, %s)", x, fc.typeName(fc.typeOf(e))) case *ast.Ident: obj := fc.pkgCtx.Uses[x].(*types.Var) if fc.pkgCtx.escapingVars[obj] { - return fc.formatExpr("(%1s.$ptr || (%1s.$ptr = new %2s(function() { return this.$target[0]; }, function($v) { this.$target[0] = $v; }, %1s)))", fc.pkgCtx.objectNames[obj], fc.typeName(exprType)) + name, ok := fc.assignedObjectName(obj) + if !ok { + // This should never happen. + panic(fmt.Errorf("escaping variable %s hasn't been assigned a JS name", obj)) + } + return fc.formatExpr("(%1s.$ptr || (%1s.$ptr = new %2s(function() { return this.$target[0]; }, function($v) { this.$target[0] = $v; }, %1s)))", name, fc.typeName(exprType)) } return fc.formatExpr(`(%1s || (%1s = new %2s(function() { return %3s; }, function($v) { %4s })))`, fc.varPtrName(obj), fc.typeName(exprType), fc.objectName(obj), fc.translateAssign(x, fc.newIdent("$v", elemType), false)) case *ast.SelectorExpr: - sel, ok := fc.pkgCtx.SelectionOf(x) + sel, ok := fc.selectionOf(x) if !ok { // qualified identifier obj := fc.pkgCtx.Uses[x.Sel].(*types.Var) return fc.formatExpr(`(%1s || (%1s = new %2s(function() { return %3s; }, function($v) { %4s })))`, fc.varPtrName(obj), fc.typeName(exprType), fc.objectName(obj), fc.translateAssign(x, fc.newIdent("$v", elemType), false)) } - newSel := &ast.SelectorExpr{X: fc.newIdent("this.$target", fc.pkgCtx.TypeOf(x.X)), Sel: x.Sel} + newSel := &ast.SelectorExpr{X: fc.newIdent("this.$target", fc.typeOf(x.X)), Sel: x.Sel} fc.setType(newSel, exprType) fc.pkgCtx.additionalSelections[newSel] = sel return fc.formatExpr("(%1e.$ptr_%2s || (%1e.$ptr_%2s = new %3s(function() { return %4e; }, function($v) { %5s }, %1e)))", x.X, x.Sel.Name, fc.typeName(exprType), newSel, fc.translateAssign(newSel, fc.newIdent("$v", exprType), false)) case *ast.IndexExpr: - if _, ok := fc.pkgCtx.TypeOf(x.X).Underlying().(*types.Slice); ok { + if _, ok := fc.typeOf(x.X).Underlying().(*types.Slice); ok { return fc.formatExpr("$indexPtr(%1e.$array, %1e.$offset + %2e, %3s)", x.X, x.Index, fc.typeName(exprType)) } return fc.formatExpr("$indexPtr(%e, %e, %s)", x.X, x.Index, fc.typeName(exprType)) @@ -312,8 +320,8 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { })) } - t := fc.pkgCtx.TypeOf(e.X) - t2 := fc.pkgCtx.TypeOf(e.Y) + t := fc.typeOf(e.X) + t2 := fc.typeOf(e.Y) _, isInterface := t2.Underlying().(*types.Interface) if isInterface || types.Identical(t, types.Typ[types.UntypedNil]) { t = t2 @@ -477,7 +485,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { return fc.formatParenExpr("%e", e.X) case *ast.IndexExpr: - switch t := fc.pkgCtx.TypeOf(e.X).Underlying().(type) { + switch t := fc.typeOf(e.X).Underlying().(type) { case *types.Pointer: if _, ok := t.Elem().Underlying().(*types.Array); !ok { // Should never happen in type-checked code. @@ -498,7 +506,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case *types.Slice: return fc.formatExpr(rangeCheck("%1e.$array[%1e.$offset + %2f]", fc.pkgCtx.Types[e.Index].Value != nil, false), e.X, e.Index) case *types.Map: - if typesutil.IsJsObject(fc.pkgCtx.TypeOf(e.Index)) { + if typesutil.IsJsObject(fc.typeOf(e.Index)) { fc.pkgCtx.errList = append(fc.pkgCtx.errList, types.Error{Fset: fc.pkgCtx.fileSet, Pos: e.Index.Pos(), Msg: "cannot use js.Object as map key"}) } key := fmt.Sprintf("%s.keyFor(%s)", fc.typeName(t.Key()), fc.translateImplicitConversion(e.Index, t.Key())) @@ -521,14 +529,19 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case *types.Basic: return fc.formatExpr("%e.charCodeAt(%f)", e.X, e.Index) case *types.Signature: - err := bailout(fmt.Errorf(`unsupported type parameters used at %s`, fc.pkgCtx.fileSet.Position(e.Pos()))) - panic(err) + return fc.formatExpr("%s", fc.instName(fc.instanceOf(e.X.(*ast.Ident)))) default: panic(fmt.Errorf(`unhandled IndexExpr: %T`, t)) } - + case *ast.IndexListExpr: + switch t := fc.typeOf(e.X).Underlying().(type) { + case *types.Signature: + return fc.formatExpr("%s", fc.instName(fc.instanceOf(e.X.(*ast.Ident)))) + default: + panic(fmt.Errorf("unhandled IndexListExpr: %T", t)) + } case *ast.SliceExpr: - if b, isBasic := fc.pkgCtx.TypeOf(e.X).Underlying().(*types.Basic); isBasic && isString(b) { + if b, isBasic := fc.typeOf(e.X).Underlying().(*types.Basic); isBasic && isString(b) { switch { case e.Low == nil && e.High == nil: return fc.translateExpr(e.X) @@ -559,10 +572,10 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } case *ast.SelectorExpr: - sel, ok := fc.pkgCtx.SelectionOf(e) + sel, ok := fc.selectionOf(e) if !ok { // qualified identifier - return fc.formatExpr("%s", fc.objectName(obj)) + return fc.formatExpr("%s", fc.instName(inst)) } switch sel.Kind() { @@ -593,10 +606,10 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { plainFun := astutil.RemoveParens(e.Fun) if astutil.IsTypeExpr(plainFun, fc.pkgCtx.Info.Info) { - return fc.formatExpr("(%s)", fc.translateConversion(e.Args[0], fc.pkgCtx.TypeOf(plainFun))) + return fc.formatExpr("(%s)", fc.translateConversion(e.Args[0], fc.typeOf(plainFun))) } - sig := fc.pkgCtx.TypeOf(plainFun).Underlying().(*types.Signature) + sig := fc.typeOf(plainFun).Underlying().(*types.Signature) switch f := plainFun.(type) { case *ast.Ident: @@ -610,10 +623,13 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { return fc.translateCall(e, sig, fc.translateExpr(f)) case *ast.SelectorExpr: - sel, ok := fc.pkgCtx.SelectionOf(f) + sel, ok := fc.selectionOf(f) if !ok { // qualified identifier obj := fc.pkgCtx.Uses[f.Sel] + if o, ok := obj.(*types.Builtin); ok { + return fc.translateBuiltin(o.Name(), sig, e.Args, e.Ellipsis.IsValid()) + } if typesutil.IsJsPackage(obj.Pkg()) { switch obj.Name() { case "Debugger": @@ -626,7 +642,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } externalizeExpr := func(e ast.Expr) string { - t := fc.pkgCtx.TypeOf(e) + t := fc.typeOf(e) if types.Identical(t, types.Typ[types.UntypedNil]) { return "null" } @@ -746,11 +762,11 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } case *ast.StarExpr: - if typesutil.IsJsObject(fc.pkgCtx.TypeOf(e.X)) { + if typesutil.IsJsObject(fc.typeOf(e.X)) { return fc.formatExpr("new $jsObjectPtr(%e)", e.X) } if c1, isCall := e.X.(*ast.CallExpr); isCall && len(c1.Args) == 1 { - if c2, isCall := c1.Args[0].(*ast.CallExpr); isCall && len(c2.Args) == 1 && types.Identical(fc.pkgCtx.TypeOf(c2.Fun), types.Typ[types.UnsafePointer]) { + if c2, isCall := c1.Args[0].(*ast.CallExpr); isCall && len(c2.Args) == 1 && types.Identical(fc.typeOf(c2.Fun), types.Typ[types.UnsafePointer]) { if unary, isUnary := c2.Args[0].(*ast.UnaryExpr); isUnary && unary.Op == token.AND { return fc.translateExpr(unary.X) // unsafe conversion } @@ -766,7 +782,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if e.Type == nil { return fc.translateExpr(e.X) } - t := fc.pkgCtx.TypeOf(e.Type) + t := fc.typeOf(e.Type) if _, isTuple := exprType.(*types.Tuple); isTuple { return fc.formatExpr("$assertType(%e, %s, true)", e.X, fc.typeName(t)) } @@ -776,11 +792,11 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if e.Name == "_" { panic("Tried to translate underscore identifier.") } - switch o := obj.(type) { + switch o := inst.Object.(type) { case *types.Var, *types.Const: - return fc.formatExpr("%s", fc.objectName(o)) + return fc.formatExpr("%s", fc.instName(inst)) case *types.Func: - return fc.formatExpr("%s", fc.objectName(o)) + return fc.formatExpr("%s", fc.instName(inst)) case *types.TypeName: return fc.formatExpr("%s", fc.typeName(o.Type())) case *types.Nil: @@ -857,7 +873,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, case *ast.SelectorExpr: isJs = typesutil.IsJsPackage(fc.pkgCtx.Uses[fun.Sel].Pkg()) } - sig := fc.pkgCtx.TypeOf(expr.Fun).Underlying().(*types.Signature) + sig := fc.typeOf(expr.Fun).Underlying().(*types.Signature) sigTypes := signatureTypes{Sig: sig} args := fc.translateArgs(sig, expr.Args, expr.Ellipsis.IsValid()) @@ -894,7 +910,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, } func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression { - sel, _ := fc.pkgCtx.SelectionOf(e) + sel, _ := fc.selectionOf(e) if !sel.Obj().Exported() { fc.pkgCtx.dependencies[sel.Obj()] = true } @@ -911,12 +927,7 @@ func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression { } fakeSel := &ast.SelectorExpr{X: x, Sel: ast.NewIdent("o")} - fc.pkgCtx.additionalSelections[fakeSel] = &fakeSelection{ - kind: types.FieldVal, - recv: sel.Recv(), - index: sel.Index()[:len(sel.Index())-1], - typ: recvType, - } + fc.pkgCtx.additionalSelections[fakeSel] = typesutil.NewSelection(types.FieldVal, sel.Recv(), sel.Index()[:len(sel.Index())-1], nil, recvType) x = fc.setType(fakeSel, recvType) } @@ -953,9 +964,9 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args return fc.formatExpr("$newDataPointer(%e, %s)", fc.zeroValue(t.Elem()), fc.typeName(t)) } case "make": - switch argType := fc.pkgCtx.TypeOf(args[0]).Underlying().(type) { + switch argType := fc.typeOf(args[0]).Underlying().(type) { case *types.Slice: - t := fc.typeName(fc.pkgCtx.TypeOf(args[0])) + t := fc.typeName(fc.typeOf(args[0])) if len(args) == 3 { return fc.formatExpr("$makeSlice(%s, %f, %f)", t, args[1], args[2]) } @@ -970,12 +981,12 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args if len(args) == 2 { length = fc.formatExpr("%f", args[1]).String() } - return fc.formatExpr("new $Chan(%s, %s)", fc.typeName(fc.pkgCtx.TypeOf(args[0]).Underlying().(*types.Chan).Elem()), length) + return fc.formatExpr("new $Chan(%s, %s)", fc.typeName(fc.typeOf(args[0]).Underlying().(*types.Chan).Elem()), length) default: panic(fmt.Sprintf("Unhandled make type: %T\n", argType)) } case "len": - switch argType := fc.pkgCtx.TypeOf(args[0]).Underlying().(type) { + switch argType := fc.typeOf(args[0]).Underlying().(type) { case *types.Basic: return fc.formatExpr("%e.length", args[0]) case *types.Slice: @@ -991,7 +1002,7 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args panic(fmt.Sprintf("Unhandled len type: %T\n", argType)) } case "cap": - switch argType := fc.pkgCtx.TypeOf(args[0]).Underlying().(type) { + switch argType := fc.typeOf(args[0]).Underlying().(type) { case *types.Slice, *types.Chan: return fc.formatExpr("%e.$capacity", args[0]) case *types.Pointer: @@ -1011,7 +1022,7 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args return fc.formatExpr("$append(%e, %s)", args[0], strings.Join(fc.translateExprSlice(args[1:], sliceType.Elem()), ", ")) case "delete": args = fc.expandTupleArgs(args) - keyType := fc.pkgCtx.TypeOf(args[0]).Underlying().(*types.Map).Key() + keyType := fc.typeOf(args[0]).Underlying().(*types.Map).Key() return fc.formatExpr( `$mapDelete(%1e, %2s.keyFor(%3s))`, args[0], @@ -1020,7 +1031,7 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args ) case "copy": args = fc.expandTupleArgs(args) - if basic, isBasic := fc.pkgCtx.TypeOf(args[1]).Underlying().(*types.Basic); isBasic && isString(basic) { + if basic, isBasic := fc.typeOf(args[1]).Underlying().(*types.Basic); isBasic && isString(basic) { return fc.formatExpr("$copyString(%e, %e)", args[0], args[1]) } return fc.formatExpr("$copySlice(%e, %e)", args[0], args[1]) @@ -1041,6 +1052,13 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args return fc.formatExpr("$recover()") case "close": return fc.formatExpr(`$close(%e)`, args[0]) + case "Sizeof": + return fc.formatExpr("%d", sizes32.Sizeof(fc.typeOf(args[0]))) + case "Alignof": + return fc.formatExpr("%d", sizes32.Alignof(fc.typeOf(args[0]))) + case "Offsetof": + sel, _ := fc.selectionOf(astutil.RemoveParens(args[0]).(*ast.SelectorExpr)) + return fc.formatExpr("%d", typesutil.OffsetOf(sizes32, sel)) default: panic(fmt.Sprintf("Unhandled builtin: %s\n", name)) } @@ -1072,13 +1090,13 @@ func (fc *funcContext) translateExprSlice(exprs []ast.Expr, desiredType types.Ty } func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type) *expression { - exprType := fc.pkgCtx.TypeOf(expr) + exprType := fc.typeOf(expr) if types.Identical(exprType, desiredType) { return fc.translateExpr(expr) } if fc.pkgCtx.Pkg.Path() == "reflect" || fc.pkgCtx.Pkg.Path() == "internal/reflectlite" { - if call, isCall := expr.(*ast.CallExpr); isCall && types.Identical(fc.pkgCtx.TypeOf(call.Fun), types.Typ[types.UnsafePointer]) { + if call, isCall := expr.(*ast.CallExpr); isCall && types.Identical(fc.typeOf(call.Fun), types.Typ[types.UnsafePointer]) { if ptr, isPtr := desiredType.(*types.Pointer); isPtr { if named, isNamed := ptr.Elem().(*types.Named); isNamed { switch named.Obj().Name() { @@ -1153,7 +1171,7 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type return fc.formatExpr("new Uint8Array(0)") } } - if ptr, isPtr := fc.pkgCtx.TypeOf(expr).(*types.Pointer); fc.pkgCtx.Pkg.Path() == "syscall" && isPtr { + if ptr, isPtr := fc.typeOf(expr).(*types.Pointer); fc.pkgCtx.Pkg.Path() == "syscall" && isPtr { if s, isStruct := ptr.Elem().Underlying().(*types.Struct); isStruct { array := fc.newVariable("_array") target := fc.newVariable("_struct") @@ -1166,7 +1184,7 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type } if call, ok := expr.(*ast.CallExpr); ok { if id, ok := call.Fun.(*ast.Ident); ok && id.Name == "new" { - return fc.formatExpr("new Uint8Array(%d)", int(sizes32.Sizeof(fc.pkgCtx.TypeOf(call.Args[0])))) + return fc.formatExpr("new Uint8Array(%d)", int(sizes32.Sizeof(fc.typeOf(call.Args[0])))) } } } @@ -1255,7 +1273,7 @@ func (fc *funcContext) translateImplicitConversion(expr ast.Expr, desiredType ty return fc.translateExpr(expr) } - exprType := fc.pkgCtx.TypeOf(expr) + exprType := fc.typeOf(expr) if types.Identical(exprType, desiredType) { return fc.translateExpr(expr) } @@ -1286,7 +1304,7 @@ func (fc *funcContext) translateImplicitConversion(expr ast.Expr, desiredType ty } func (fc *funcContext) translateConversionToSlice(expr ast.Expr, desiredType types.Type) *expression { - switch fc.pkgCtx.TypeOf(expr).Underlying().(type) { + switch fc.typeOf(expr).Underlying().(type) { case *types.Array, *types.Pointer: return fc.formatExpr("new %s(%e)", fc.typeName(desiredType), expr) } @@ -1447,7 +1465,7 @@ func (fc *funcContext) formatExprInternal(format string, a []interface{}, parens } out.WriteString(a[n].(string)) case 'd': - out.WriteString(strconv.Itoa(a[n].(int))) + fmt.Fprintf(out, "%d", a[n]) case 't': out.WriteString(a[n].(token.Token).String()) case 'e': @@ -1464,7 +1482,7 @@ func (fc *funcContext) formatExprInternal(format string, a []interface{}, parens out.WriteString(strconv.FormatInt(d, 10)) return } - if is64Bit(fc.pkgCtx.TypeOf(e).Underlying().(*types.Basic)) { + if is64Bit(fc.typeOf(e).Underlying().(*types.Basic)) { out.WriteString("$flatten64(") writeExpr("") out.WriteString(")") @@ -1475,7 +1493,7 @@ func (fc *funcContext) formatExprInternal(format string, a []interface{}, parens e := a[n].(ast.Expr) if val := fc.pkgCtx.Types[e].Value; val != nil { d, _ := constant.Uint64Val(constant.ToInt(val)) - if fc.pkgCtx.TypeOf(e).Underlying().(*types.Basic).Kind() == types.Int64 { + if fc.typeOf(e).Underlying().(*types.Basic).Kind() == types.Int64 { out.WriteString(strconv.FormatInt(int64(d)>>32, 10)) return } diff --git a/compiler/internal/symbol/symbol.go b/compiler/internal/symbol/symbol.go new file mode 100644 index 000000000..851ca1ef6 --- /dev/null +++ b/compiler/internal/symbol/symbol.go @@ -0,0 +1,60 @@ +package symbol + +import ( + "go/types" + "strings" +) + +// Name uniquely identifies a named symbol within a program. +// +// This is a logical equivalent of a symbol name used by traditional linkers. +// The following properties should hold true: +// +// - Each named symbol within a program has a unique Name. +// - Similarly named methods of different types will have different symbol names. +// - The string representation is opaque and should not be attempted to reversed +// to a struct form. +type Name struct { + PkgPath string // Full package import path. + Name string // Symbol name. +} + +// New constructs SymName for a given named symbol. +func New(o types.Object) Name { + if fun, ok := o.(*types.Func); ok { + sig := fun.Type().(*types.Signature) + if recv := sig.Recv(); recv != nil { + // Special case: disambiguate names for different types' methods. + typ := recv.Type() + if ptr, ok := typ.(*types.Pointer); ok { + return Name{ + PkgPath: o.Pkg().Path(), + Name: "(*" + ptr.Elem().(*types.Named).Obj().Name() + ")." + o.Name(), + } + } + return Name{ + PkgPath: o.Pkg().Path(), + Name: typ.(*types.Named).Obj().Name() + "." + o.Name(), + } + } + } + return Name{ + PkgPath: o.Pkg().Path(), + Name: o.Name(), + } +} + +func (n Name) String() string { return n.PkgPath + "." + n.Name } + +func (n Name) IsMethod() (recv string, method string, ok bool) { + pos := strings.IndexByte(n.Name, '.') + if pos == -1 { + return + } + recv, method, ok = n.Name[:pos], n.Name[pos+1:], true + size := len(recv) + if size > 2 && recv[0] == '(' && recv[size-1] == ')' { + recv = recv[1 : size-1] + } + return +} diff --git a/compiler/internal/symbol/symbol_test.go b/compiler/internal/symbol/symbol_test.go new file mode 100644 index 000000000..d4fc4196a --- /dev/null +++ b/compiler/internal/symbol/symbol_test.go @@ -0,0 +1,54 @@ +package symbol + +import ( + "go/token" + "go/types" + "testing" + + "github.com/gopherjs/gopherjs/internal/srctesting" +) + +func TestName(t *testing.T) { + const src = `package testcase + + func AFunction() {} + type AType struct {} + func (AType) AMethod() {} + func (*AType) APointerMethod() {} + var AVariable int32 + ` + + fset := token.NewFileSet() + _, pkg := srctesting.Check(t, fset, srctesting.Parse(t, fset, src)) + + tests := []struct { + obj types.Object + want Name + }{ + { + obj: pkg.Scope().Lookup("AFunction"), + want: Name{PkgPath: "test", Name: "AFunction"}, + }, { + obj: pkg.Scope().Lookup("AType"), + want: Name{PkgPath: "test", Name: "AType"}, + }, { + obj: types.NewMethodSet(pkg.Scope().Lookup("AType").Type()).Lookup(pkg, "AMethod").Obj(), + want: Name{PkgPath: "test", Name: "AType.AMethod"}, + }, { + obj: types.NewMethodSet(types.NewPointer(pkg.Scope().Lookup("AType").Type())).Lookup(pkg, "APointerMethod").Obj(), + want: Name{PkgPath: "test", Name: "(*AType).APointerMethod"}, + }, { + obj: pkg.Scope().Lookup("AVariable"), + want: Name{PkgPath: "test", Name: "AVariable"}, + }, + } + + for _, test := range tests { + t.Run(test.obj.Name(), func(t *testing.T) { + got := New(test.obj) + if got != test.want { + t.Errorf("NewSymName(%q) returned %#v, want: %#v", test.obj.Name(), got, test.want) + } + }) + } +} diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go new file mode 100644 index 000000000..6f0da9973 --- /dev/null +++ b/compiler/internal/typeparams/collect.go @@ -0,0 +1,263 @@ +package typeparams + +import ( + "fmt" + "go/ast" + "go/types" + + "github.com/gopherjs/gopherjs/compiler/internal/typeparams/subst" + "github.com/gopherjs/gopherjs/compiler/typesutil" + "golang.org/x/exp/typeparams" +) + +// Resolver translates types defined in terms of type parameters into concrete +// types, given a mapping from type params to type arguments. +type Resolver struct { + subster *subst.Subster + selMemo map[typesutil.Selection]typesutil.Selection +} + +// NewResolver creates a new Resolver with tParams entries mapping to tArgs +// entries with the same index. +func NewResolver(tc *types.Context, tParams []*types.TypeParam, tArgs []types.Type) *Resolver { + r := &Resolver{ + subster: subst.New(tc, tParams, tArgs), + selMemo: map[typesutil.Selection]typesutil.Selection{}, + } + return r +} + +// Substitute replaces references to type params in the provided type definition +// with the corresponding concrete types. +func (r *Resolver) Substitute(typ types.Type) types.Type { + if r == nil || r.subster == nil || typ == nil { + return typ // No substitutions to be made. + } + return r.subster.Type(typ) +} + +// SubstituteAll same as Substitute, but accepts a TypeList are returns +// substitution results as a slice in the same order. +func (r *Resolver) SubstituteAll(list *types.TypeList) []types.Type { + result := make([]types.Type, list.Len()) + for i := range result { + result[i] = r.Substitute(list.At(i)) + } + return result +} + +// SubstituteSelection replaces a method of field selection on a generic type +// defined in terms of type parameters with a method selection on a concrete +// instantiation of the type. +func (r *Resolver) SubstituteSelection(sel typesutil.Selection) typesutil.Selection { + if r == nil || r.subster == nil || sel == nil { + return sel // No substitutions to be made. + } + if concrete, ok := r.selMemo[sel]; ok { + return concrete + } + + switch sel.Kind() { + case types.MethodExpr, types.MethodVal, types.FieldVal: + recv := r.Substitute(sel.Recv()) + if types.Identical(recv, sel.Recv()) { + return sel // Non-generic receiver, no substitution necessary. + } + + // Look up the method on the instantiated receiver. + pkg := sel.Obj().Pkg() + obj, index, _ := types.LookupFieldOrMethod(recv, true, pkg, sel.Obj().Name()) + if obj == nil { + panic(fmt.Errorf("failed to lookup field %q in type %v", sel.Obj().Name(), recv)) + } + typ := obj.Type() + + if sel.Kind() == types.MethodExpr { + typ = typesutil.RecvAsFirstArg(typ.(*types.Signature)) + } + concrete := typesutil.NewSelection(sel.Kind(), recv, index, obj, typ) + r.selMemo[sel] = concrete + return concrete + default: + panic(fmt.Errorf("unexpected selection kind %v: %v", sel.Kind(), sel)) + } +} + +// ToSlice converts TypeParamList into a slice with the same order of entries. +func ToSlice(tpl *types.TypeParamList) []*types.TypeParam { + result := make([]*types.TypeParam, tpl.Len()) + for i := range result { + result[i] = tpl.At(i) + } + return result +} + +// visitor implements ast.Visitor and collects instances of generic types and +// functions into an InstanceSet. +// +// When traversing an AST subtree corresponding to a generic type, method or +// function, Resolver must be provided mapping the type parameters into concrete +// types. +type visitor struct { + instances *InstanceSet + resolver *Resolver + info *types.Info +} + +var _ ast.Visitor = &visitor{} + +func (c *visitor) Visit(n ast.Node) (w ast.Visitor) { + w = c // Always traverse the full depth of the AST tree. + + ident, ok := n.(*ast.Ident) + if !ok { + return + } + + instance, ok := c.info.Instances[ident] + if !ok { + return + } + + obj := c.info.ObjectOf(ident) + + // For types embedded in structs, the object the identifier resolves to is a + // *types.Var representing the implicitly declared struct field. However, the + // instance relates to the *types.TypeName behind the field type, which we + // obtain here. + typ := obj.Type() + if ptr, ok := typ.(*types.Pointer); ok { + typ = ptr.Elem() + } + if t, ok := typ.(*types.Named); ok { + obj = t.Obj() + } + c.instances.Add(Instance{ + Object: obj, + TArgs: c.resolver.SubstituteAll(instance.TypeArgs), + }) + + if t, ok := obj.Type().(*types.Named); ok { + for i := 0; i < t.NumMethods(); i++ { + method := t.Method(i) + c.instances.Add(Instance{ + Object: typeparams.OriginMethod(method), // TODO(nevkontakte): Can be replaced with method.Origin() in Go 1.19. + TArgs: c.resolver.SubstituteAll(instance.TypeArgs), + }) + } + } + return +} + +// seedVisitor implements ast.Visitor that collects information necessary to +// kickstart generic instantiation discovery. +// +// It serves double duty: +// - Builds a map from types.Object instances representing generic types, +// methods and functions to AST nodes that define them. +// - Collects an initial set of generic instantiations in the non-generic code. +type seedVisitor struct { + visitor + objMap map[types.Object]ast.Node + mapOnly bool // Only build up objMap, ignore any instances. +} + +var _ ast.Visitor = &seedVisitor{} + +func (c *seedVisitor) Visit(n ast.Node) ast.Visitor { + // Generic functions, methods and types require type arguments to scan for + // generic instantiations, remember their node for later and do not descend + // further. + switch n := n.(type) { + case *ast.FuncDecl: + obj := c.info.Defs[n.Name] + sig := obj.Type().(*types.Signature) + if sig.TypeParams().Len() != 0 || sig.RecvTypeParams().Len() != 0 { + c.objMap[obj] = n + return &seedVisitor{ + visitor: c.visitor, + objMap: c.objMap, + mapOnly: true, + } + } + case *ast.TypeSpec: + obj := c.info.Defs[n.Name] + named, ok := obj.Type().(*types.Named) + if !ok { + break + } + if named.TypeParams().Len() != 0 && named.TypeArgs().Len() == 0 { + c.objMap[obj] = n + return nil + } + } + + if !c.mapOnly { + // Otherwise check for fully defined instantiations and descend further into + // the AST tree. + c.visitor.Visit(n) + } + return c +} + +// Collector scans type-checked AST tree and adds discovered generic type and +// function instances to the InstanceSet. +// +// Collector will scan non-generic code for any instantiations of generic types +// or functions and add them to the InstanceSet. Then it will scan generic types +// and function with discovered sets of type arguments for more instantiations, +// until no new ones are discovered. +// +// InstanceSet may contain unprocessed instances of generic types and functions, +// which will be also scanned, for example found in depending packages. +// +// Note that instances of generic type methods are automatically added to the +// set whenever their receiver type instance is encountered. +type Collector struct { + TContext *types.Context + Info *types.Info + Instances *InstanceSet +} + +// Scan package files for generic instances. +func (c *Collector) Scan(files ...*ast.File) { + if c.Info.Instances == nil || c.Info.Defs == nil { + panic(fmt.Errorf("types.Info must have Instances and Defs populated")) + } + objMap := map[types.Object]ast.Node{} + + // Collect instances of generic objects in non-generic code in the package and + // add then to the existing InstanceSet. + sc := seedVisitor{ + visitor: visitor{ + instances: c.Instances, + resolver: nil, + info: c.Info, + }, + objMap: objMap, + } + for _, file := range files { + ast.Walk(&sc, file) + } + + for !c.Instances.exhausted() { + inst, _ := c.Instances.next() + switch typ := inst.Object.Type().(type) { + case *types.Signature: + v := visitor{ + instances: c.Instances, + resolver: NewResolver(c.TContext, ToSlice(SignatureTypeParams(typ)), inst.TArgs), + info: c.Info, + } + ast.Walk(&v, objMap[inst.Object]) + case *types.Named: + obj := typ.Obj() + v := visitor{ + instances: c.Instances, + resolver: NewResolver(c.TContext, ToSlice(typ.TypeParams()), inst.TArgs), + info: c.Info, + } + ast.Walk(&v, objMap[obj]) + } + } +} diff --git a/compiler/internal/typeparams/collect_test.go b/compiler/internal/typeparams/collect_test.go new file mode 100644 index 000000000..64aa43778 --- /dev/null +++ b/compiler/internal/typeparams/collect_test.go @@ -0,0 +1,449 @@ +package typeparams + +import ( + "go/ast" + "go/token" + "go/types" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/gopherjs/gopherjs/internal/srctesting" + "golang.org/x/tools/go/ast/astutil" +) + +func TestVisitor(t *testing.T) { + // This test verifies that instance collector is able to discover + // instantiations of generic types and functions in all possible contexts. + const src = `package testcase + + type A struct{} + type B struct{} + type C struct{} + type D struct{} + type E struct{} + type F struct{} + type G struct{} + + type typ[T any, V any] []T + func (t *typ[T, V]) method(x T) {} + func fun[U any, W any](x U, y W) {} + + func entry1(arg typ[int8, A]) (result typ[int16, A]) { + fun(1, A{}) + fun[int8, A](1, A{}) + println(fun[int16, A]) + + t := typ[int, A]{} + t.method(0) + (*typ[int32, A]).method(nil, 0) + type x struct{ T []typ[int64, A] } + + return + } + + func entry2[T any](arg typ[int8, T]) (result typ[int16, T]) { + var zeroT T + fun(1, zeroT) + fun[int8, T](1, zeroT) + println(fun[int16, T]) + + t := typ[int, T]{} + t.method(0) + (*typ[int32, T]).method(nil, 0) + type x struct{ T []typ[int64, T] } + + return + } + + type entry3[T any] struct{ + typ[int, T] + field1 struct { field2 typ[int8, T] } + } + func (e entry3[T]) method(arg typ[int8, T]) (result typ[int16, T]) { + var zeroT T + fun(1, zeroT) + fun[int8, T](1, zeroT) + println(fun[int16, T]) + + t := typ[int, T]{} + t.method(0) + (*typ[int32, T]).method(nil, 0) + type x struct{ T []typ[int64, T] } + + return + } + + type entry4 struct{ + typ[int, E] + field1 struct { field2 typ[int8, E] } + } + + type entry5 = typ[int, F] + ` + fset := token.NewFileSet() + file := srctesting.Parse(t, fset, src) + info, pkg := srctesting.Check(t, fset, file) + + lookupObj := func(name string) types.Object { + return srctesting.LookupObj(pkg, name) + } + lookupType := func(name string) types.Type { return lookupObj(name).Type() } + lookupDecl := func(name string) ast.Node { + obj := lookupObj(name) + path, _ := astutil.PathEnclosingInterval(file, obj.Pos(), obj.Pos()) + for _, n := range path { + switch n.(type) { + case *ast.FuncDecl, *ast.TypeSpec: + return n + } + } + t.Fatalf("Could not find AST node representing %v", obj) + return nil + } + + // Generates a list of instances we expect to discover from functions and + // methods. Sentinel type is a type parameter we use uniquely within one + // context, which allows us to make sure that collection is not being tested + // against a wrong part of AST. + instancesInFunc := func(sentinel types.Type) []Instance { + return []Instance{ + { + // Called with type arguments inferred. + Object: lookupObj("fun"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + // Called with type arguments explicitly specified. + Object: lookupObj("fun"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, { + // Passed as an argument. + Object: lookupObj("fun"), + TArgs: []types.Type{types.Typ[types.Int16], sentinel}, + }, { + // Literal expression. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + // Function argument. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, { + // Function return type. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int16], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int16], sentinel}, + }, { + // Method expression. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int32], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int32], sentinel}, + }, { + // Type decl statement. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int64], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int64], sentinel}, + }, + } + } + + // Generates a list of instances we expect to discover from type declarations. + // Sentinel type is a type parameter we use uniquely within one context, which + // allows us to make sure that collection is not being tested against a wrong + // part of AST. + instancesInType := func(sentinel types.Type) []Instance { + return []Instance{ + { + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, + } + } + + tests := []struct { + descr string + resolver *Resolver + node ast.Node + want []Instance + }{ + { + descr: "non-generic function", + resolver: nil, + node: lookupDecl("entry1"), + want: instancesInFunc(lookupType("A")), + }, { + descr: "generic function", + resolver: NewResolver( + types.NewContext(), + ToSlice(lookupType("entry2").(*types.Signature).TypeParams()), + []types.Type{lookupType("B")}, + ), + node: lookupDecl("entry2"), + want: instancesInFunc(lookupType("B")), + }, { + descr: "generic method", + resolver: NewResolver( + types.NewContext(), + ToSlice(lookupType("entry3.method").(*types.Signature).RecvTypeParams()), + []types.Type{lookupType("C")}, + ), + node: lookupDecl("entry3.method"), + want: append( + instancesInFunc(lookupType("C")), + Instance{ + Object: lookupObj("entry3"), + TArgs: []types.Type{lookupType("C")}, + }, + Instance{ + Object: lookupObj("entry3.method"), + TArgs: []types.Type{lookupType("C")}, + }, + ), + }, { + descr: "generic type declaration", + resolver: NewResolver( + types.NewContext(), + ToSlice(lookupType("entry3").(*types.Named).TypeParams()), + []types.Type{lookupType("D")}, + ), + node: lookupDecl("entry3"), + want: instancesInType(lookupType("D")), + }, { + descr: "non-generic type declaration", + resolver: nil, + node: lookupDecl("entry4"), + want: instancesInType(lookupType("E")), + }, { + descr: "non-generic type alias", + resolver: nil, + node: lookupDecl("entry5"), + want: []Instance{ + { + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int], lookupType("F")}, + }, + { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int], lookupType("F")}, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + v := visitor{ + instances: &InstanceSet{}, + resolver: test.resolver, + info: info, + } + ast.Walk(&v, test.node) + got := v.instances.Values() + if diff := cmp.Diff(test.want, got, instanceOpts()); diff != "" { + t.Errorf("Discovered instance diff (-want,+got):\n%s", diff) + } + }) + } +} + +func TestSeedVisitor(t *testing.T) { + src := `package test + type typ[T any] int + func (t typ[T]) method(arg T) { var x typ[string]; _ = x } + func fun[T any](arg T) { var y typ[string]; _ = y } + + const a typ[int] = 1 + var b typ[int] + type c struct { field typ[int8] } + func (_ c) method() { var _ typ[int16] } + type d = typ[int32] + func e() { var _ typ[int64] } + ` + + fset := token.NewFileSet() + file := srctesting.Parse(t, fset, src) + info, pkg := srctesting.Check(t, fset, file) + + sv := seedVisitor{ + visitor: visitor{ + instances: &InstanceSet{}, + resolver: nil, + info: info, + }, + objMap: map[types.Object]ast.Node{}, + } + ast.Walk(&sv, file) + + tInst := func(tArg types.Type) Instance { + return Instance{ + Object: pkg.Scope().Lookup("typ"), + TArgs: []types.Type{tArg}, + } + } + mInst := func(tArg types.Type) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, "typ.method"), + TArgs: []types.Type{tArg}, + } + } + want := []Instance{ + tInst(types.Typ[types.Int]), + mInst(types.Typ[types.Int]), + tInst(types.Typ[types.Int8]), + mInst(types.Typ[types.Int8]), + tInst(types.Typ[types.Int16]), + mInst(types.Typ[types.Int16]), + tInst(types.Typ[types.Int32]), + mInst(types.Typ[types.Int32]), + tInst(types.Typ[types.Int64]), + mInst(types.Typ[types.Int64]), + } + got := sv.instances.Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != "" { + t.Errorf("Instances from initialSeeder contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector(t *testing.T) { + src := `package test + type typ[T any] int + func (t typ[T]) method(arg T) { var _ typ[int]; fun[int8](0) } + func fun[T any](arg T) { + var _ typ[int16] + + type nested[U any] struct{} + _ = nested[T]{} + } + + type ignore = int + + func a() { + var _ typ[int32] + fun[int64](0) + } + ` + + fset := token.NewFileSet() + file := srctesting.Parse(t, fset, src) + info, pkg := srctesting.Check(t, fset, file) + + c := Collector{ + TContext: types.NewContext(), + Info: info, + Instances: &InstanceSet{}, + } + c.Scan(file) + + inst := func(name string, tArg types.Type) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TArgs: []types.Type{tArg}, + } + } + want := []Instance{ + inst("typ", types.Typ[types.Int]), + inst("typ.method", types.Typ[types.Int]), + inst("fun", types.Typ[types.Int8]), + inst("fun.nested", types.Typ[types.Int8]), + inst("typ", types.Typ[types.Int16]), + inst("typ.method", types.Typ[types.Int16]), + inst("typ", types.Typ[types.Int32]), + inst("typ.method", types.Typ[types.Int32]), + inst("fun", types.Typ[types.Int64]), + inst("fun.nested", types.Typ[types.Int64]), + } + got := c.Instances.Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != "" { + t.Errorf("Instances from initialSeeder contain diff (-want,+got):\n%s", diff) + } +} + +func TestResolver_SubstituteSelection(t *testing.T) { + tests := []struct { + descr string + src string + wantObj string + wantSig string + }{{ + descr: "type parameter method", + src: `package test + type stringer interface{ String() string } + + type x struct{} + func (_ x) String() string { return "" } + + type g[T stringer] struct{} + func (_ g[T]) Method(t T) string { + return t.String() + }`, + wantObj: "func (test.x).String() string", + wantSig: "func() string", + }, { + descr: "generic receiver type with type parameter", + src: `package test + type x struct{} + + type g[T any] struct{} + func (_ g[T]) Method(t T) string { + return g[T]{}.Method(t) + }`, + wantObj: "func (test.g[test.x]).Method(t test.x) string", + wantSig: "func(t test.x) string", + }, { + descr: "method expression", + src: `package test + type x struct{} + + type g[T any] struct{} + func (recv g[T]) Method(t T) string { + return g[T].Method(recv, t) + }`, + wantObj: "func (test.g[test.x]).Method(t test.x) string", + wantSig: "func(recv test.g[test.x], t test.x) string", + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + fset := token.NewFileSet() + file := srctesting.Parse(t, fset, test.src) + info, pkg := srctesting.Check(t, fset, file) + + method := srctesting.LookupObj(pkg, "g.Method").(*types.Func).Type().(*types.Signature) + resolver := NewResolver(nil, ToSlice(method.RecvTypeParams()), []types.Type{srctesting.LookupObj(pkg, "x").Type()}) + + if l := len(info.Selections); l != 1 { + t.Fatalf("Got: %d selections. Want: 1", l) + } + for _, sel := range info.Selections { + gotObj := types.ObjectString(resolver.SubstituteSelection(sel).Obj(), nil) + if gotObj != test.wantObj { + t.Fatalf("Got: resolver.SubstituteSelection().Obj() = %q. Want: %q.", gotObj, test.wantObj) + } + gotSig := types.TypeString(resolver.SubstituteSelection(sel).Type(), nil) + if gotSig != test.wantSig { + t.Fatalf("Got: resolver.SubstituteSelection().Type() = %q. Want: %q.", gotSig, test.wantSig) + } + } + }) + } +} diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go new file mode 100644 index 000000000..a64e3be8a --- /dev/null +++ b/compiler/internal/typeparams/instance.go @@ -0,0 +1,139 @@ +package typeparams + +import ( + "fmt" + "go/types" + + "github.com/gopherjs/gopherjs/compiler/internal/symbol" + "github.com/gopherjs/gopherjs/compiler/typesutil" +) + +// Instance of a generic type or function. +// +// Non-generic objects can be represented as an Instance with zero type params, +// they are instances of themselves. +type Instance struct { + Object types.Object // Object to be instantiated. + TArgs typesutil.TypeList // Type params to instantiate with. +} + +// String returns a string representation of the Instance. +// +// Two semantically different instances may have the same string representation +// if the instantiated object or its type arguments shadow other types. +func (i *Instance) String() string { + sym := symbol.New(i.Object).String() + if len(i.TArgs) == 0 { + return sym + } + + return fmt.Sprintf("%s<%s>", sym, i.TArgs) +} + +// TypeString returns a Go type string representing the instance (suitable for %T verb). +func (i *Instance) TypeString() string { + tArgs := "" + if len(i.TArgs) > 0 { + tArgs = "[" + i.TArgs.String() + "]" + } + return fmt.Sprintf("%s.%s%s", i.Object.Pkg().Name(), i.Object.Name(), tArgs) +} + +// IsTrivial returns true if this is an instance of a non-generic object. +func (i *Instance) IsTrivial() bool { + return len(i.TArgs) == 0 +} + +// Recv returns an instance of the receiver type of a method. +// +// Returns zero value if not a method. +func (i *Instance) Recv() Instance { + sig, ok := i.Object.Type().(*types.Signature) + if !ok { + return Instance{} + } + recv := typesutil.RecvType(sig) + if recv == nil { + return Instance{} + } + return Instance{ + Object: recv.Obj(), + TArgs: i.TArgs, + } +} + +// InstanceSet allows collecting and processing unique Instances. +// +// Each Instance may be added to the set any number of times, but it will be +// returned for processing exactly once. Processing order is not specified. +type InstanceSet struct { + values []Instance + unprocessed int // Index in values for the next unprocessed element. + seen InstanceMap[int] // Maps instance to a unique numeric id. +} + +// Add instances to the set. Instances that have been previously added to the +// set won't be requeued for processing regardless of whether they have been +// processed already. +func (iset *InstanceSet) Add(instances ...Instance) *InstanceSet { + for _, inst := range instances { + if iset.seen.Has(inst) { + continue + } + iset.seen.Set(inst, iset.seen.Len()) + iset.values = append(iset.values, inst) + } + return iset +} + +// ID returns a unique numeric identifier assigned to an instance in the set. +// The ID is guaranteed to be unique among all instances of the same object +// within a given program. The ID will be consistent, as long as instances are +// added to the set in the same order. +// +// In order to have an ID assigned, the instance must have been previously added +// to the set. +// +// Note: these ids are used in the generated code as keys to the specific +// type/function instantiation in the type/function object. Using this has two +// advantages: +// +// - More compact generated code compared to string keys derived from type args. +// +// - Collision avoidance in case of two different types having the same name due +// to shadowing. +// +// Here's an example where it's very difficult to assign non-colliding +// name-based keys to the two different types T: +// +// func foo() { +// type T int +// { type T string } // Code block creates a new nested scope allowing for shadowing. +// } +func (iset *InstanceSet) ID(inst Instance) int { + id, ok := iset.seen.get(inst) + if !ok { + panic(fmt.Errorf("requesting ID of instance %v that hasn't been added to the set", inst)) + } + return id +} + +// next returns the next Instance to be processed. +// +// If there are no unprocessed instances, the second returned value will be false. +func (iset *InstanceSet) next() (Instance, bool) { + if iset.exhausted() { + return Instance{}, false + } + next := iset.values[iset.unprocessed] + iset.unprocessed++ + return next, true +} + +// exhausted returns true if there are no unprocessed instances in the set. +func (iset *InstanceSet) exhausted() bool { return len(iset.values) <= iset.unprocessed } + +// Values returns instances that are currently in the set. Order is not specified. +func (iset *InstanceSet) Values() []Instance { + return iset.values +} diff --git a/compiler/internal/typeparams/instance_test.go b/compiler/internal/typeparams/instance_test.go new file mode 100644 index 000000000..d00b58beb --- /dev/null +++ b/compiler/internal/typeparams/instance_test.go @@ -0,0 +1,207 @@ +package typeparams + +import ( + "go/token" + "go/types" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/gopherjs/gopherjs/internal/srctesting" + "github.com/gopherjs/gopherjs/internal/testingx" +) + +func instanceOpts() cmp.Options { + return cmp.Options{ + // Instances are represented by their IDs for diffing purposes. + cmp.Transformer("Instance", func(i Instance) string { + return i.String() + }), + // Order of instances in a slice doesn't matter, sort them by ID. + cmpopts.SortSlices(func(a, b Instance) bool { + return a.String() < b.String() + }), + } +} + +func TestInstanceString(t *testing.T) { + const src = `package testcase + + type Ints []int + + type Typ[T any, V any] []T + func (t Typ[T, V]) Method(x T) {} + + type typ[T any, V any] []T + func (t typ[T, V]) method(x T) {} + + func Fun[U any, W any](x, y U) {} + func fun[U any, W any](x, y U) {} + ` + fset := token.NewFileSet() + _, pkg := srctesting.Check(t, fset, srctesting.Parse(t, fset, src)) + mustType := testingx.Must[types.Type](t) + + tests := []struct { + descr string + instance Instance + wantStr string + wantTypeString string + }{{ + descr: "exported type", + instance: Instance{ + Object: pkg.Scope().Lookup("Typ"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + wantStr: "test.Typ", + wantTypeString: "testcase.Typ[int, string]", + }, { + descr: "exported method", + instance: Instance{ + Object: pkg.Scope().Lookup("Typ").Type().(*types.Named).Method(0), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + wantStr: "test.Typ.Method", + }, { + descr: "exported function", + instance: Instance{ + Object: pkg.Scope().Lookup("Fun"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + wantStr: "test.Fun", + }, { + descr: "unexported type", + instance: Instance{ + Object: pkg.Scope().Lookup("typ"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + wantStr: "test.typ", + wantTypeString: "testcase.typ[int, string]", + }, { + descr: "unexported method", + instance: Instance{ + Object: pkg.Scope().Lookup("typ").Type().(*types.Named).Method(0), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + wantStr: "test.typ.method", + }, { + descr: "unexported function", + instance: Instance{ + Object: pkg.Scope().Lookup("fun"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + wantStr: "test.fun", + }, { + descr: "no type params", + instance: Instance{ + Object: pkg.Scope().Lookup("Ints"), + }, + wantStr: "test.Ints", + wantTypeString: "testcase.Ints", + }, { + descr: "complex parameter type", + instance: Instance{ + Object: pkg.Scope().Lookup("fun"), + TArgs: []types.Type{ + types.NewSlice(types.Typ[types.Int]), + mustType(types.Instantiate(nil, pkg.Scope().Lookup("typ").Type(), []types.Type{ + types.Typ[types.Int], + types.Typ[types.String], + }, true)), + }, + }, + wantStr: "test.fun<[]int, test.typ[int, string]>", + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + got := test.instance.String() + if got != test.wantStr { + t.Errorf("Got: instance string %q. Want: %q.", got, test.wantStr) + } + if test.wantTypeString != "" { + got = test.instance.TypeString() + if got != test.wantTypeString { + t.Errorf("Got: instance type string %q. Want: %q.", got, test.wantTypeString) + } + } + }) + } +} + +func TestInstanceQueue(t *testing.T) { + const src = `package test + type Typ[T any, V any] []T + func Fun[U any, W any](x, y U) {} + ` + fset := token.NewFileSet() + _, pkg := srctesting.Check(t, fset, srctesting.Parse(t, fset, src)) + + i1 := Instance{ + Object: pkg.Scope().Lookup("Typ"), + TArgs: []types.Type{types.Typ[types.String], types.Typ[types.String]}, + } + i2 := Instance{ + Object: pkg.Scope().Lookup("Typ"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.Int]}, + } + i3 := Instance{ + Object: pkg.Scope().Lookup("Fun"), + TArgs: []types.Type{types.Typ[types.String], types.Typ[types.String]}, + } + + set := InstanceSet{} + set.Add(i1, i2) + + if ex := set.exhausted(); ex { + t.Errorf("Got: set.exhausted() = true. Want: false") + } + + gotValues := set.Values() + wantValues := []Instance{i1, i2} + if diff := cmp.Diff(wantValues, gotValues, instanceOpts()); diff != "" { + t.Errorf("set.Values() returned diff (-want,+got):\n%s", diff) + } + + p1, ok := set.next() + if !ok { + t.Errorf("Got: _, ok := set.next(); ok == false. Want: true.") + } + p2, ok := set.next() + if !ok { + t.Errorf("Got: _, ok := set.next(); ok == false. Want: true.") + } + if ex := set.exhausted(); !ex { + t.Errorf("Got: set.exhausted() = false. Want: true") + } + + _, ok = set.next() + if ok { + t.Errorf("Got: _, ok := set.next(); ok == true. Want: false.") + } + + set.Add(i1) // Has been enqueued before. + if ex := set.exhausted(); !ex { + t.Errorf("Got: set.exhausted() = false. Want: true") + } + + set.Add(i3) + p3, ok := set.next() + if !ok { + t.Errorf("Got: _, ok := set.next(); ok == false. Want: true.") + } + + added := []Instance{i1, i2, i3} + processed := []Instance{p1, p2, p3} + + diff := cmp.Diff(added, processed, instanceOpts()) + if diff != "" { + t.Errorf("Processed instances differ from added (-want,+got):\n%s", diff) + } + + gotValues = set.Values() + wantValues = []Instance{i1, i2, i3} + if diff := cmp.Diff(wantValues, gotValues, instanceOpts()); diff != "" { + t.Errorf("set.Values() returned diff (-want,+got):\n%s", diff) + } +} diff --git a/compiler/internal/typeparams/map.go b/compiler/internal/typeparams/map.go new file mode 100644 index 000000000..aa16130e2 --- /dev/null +++ b/compiler/internal/typeparams/map.go @@ -0,0 +1,135 @@ +package typeparams + +import ( + "go/types" + "sync" + + "golang.org/x/tools/go/types/typeutil" +) + +type ( + mapEntry[V any] struct { + key Instance + value V + } + mapBucket[V any] []*mapEntry[V] + mapBuckets[V any] map[uint32]mapBucket[V] +) + +// InstanceMap implements a map-like data structure keyed by instances. +// +// Zero value is an equivalent of an empty map. Methods are not thread-safe. +// +// Since Instance contains a slice and is not comparable, it can not be used as +// a regular map key, but we can compare its fields manually. When comparing +// instance equality, objects are compared by pointer equality, and type +// arguments with types.Identical(). To reduce access complexity, we bucket +// entries by a combined hash of type args. This type is generally inspired by +// typeutil.Map. +type InstanceMap[V any] struct { + bootstrap sync.Once + data map[types.Object]mapBuckets[V] + len int + hasher typeutil.Hasher + zero V +} + +func (im *InstanceMap[V]) init() { + im.bootstrap.Do(func() { + im.data = map[types.Object]mapBuckets[V]{} + im.hasher = typeutil.MakeHasher() + }) +} + +func (im *InstanceMap[V]) get(key Instance) (V, bool) { + im.init() + + buckets, ok := im.data[key.Object] + if !ok { + return im.zero, false + } + bucket := buckets[typeHash(im.hasher, key.TArgs...)] + if len(bucket) == 0 { + return im.zero, false + } + + for _, candidate := range bucket { + if typeArgsEq(candidate.key.TArgs, key.TArgs) { + return candidate.value, true + } + } + return im.zero, false +} + +// Get returns the stored value for the provided key. If the key is missing from +// the map, zero value is returned. +func (im *InstanceMap[V]) Get(key Instance) V { + val, _ := im.get(key) + return val +} + +// Has returns true if the given key is present in the map. +func (im *InstanceMap[V]) Has(key Instance) bool { + _, ok := im.get(key) + return ok +} + +// Set new value for the key in the map. Returns the previous value that was +// stored in the map, or zero value if the key wasn't present before. +func (im *InstanceMap[V]) Set(key Instance, value V) (old V) { + im.init() + + if _, ok := im.data[key.Object]; !ok { + im.data[key.Object] = mapBuckets[V]{} + } + bucketID := typeHash(im.hasher, key.TArgs...) + + // If there is already an identical key in the map, override the entry value. + for _, candidate := range im.data[key.Object][bucketID] { + if typeArgsEq(candidate.key.TArgs, key.TArgs) { + old = candidate.value + candidate.value = value + return old + } + } + + // Otherwise append a new entry. + im.data[key.Object][bucketID] = append(im.data[key.Object][bucketID], &mapEntry[V]{ + key: key, + value: value, + }) + im.len++ + return im.zero +} + +// Len returns the number of elements in the map. +func (im *InstanceMap[V]) Len() int { + return im.len +} + +// typeHash returns a combined hash of several types. +// +// Provided hasher is used to compute hashes of individual types, which are +// xor'ed together. Xor preserves bit distribution property, so the combined +// hash should be as good for bucketing, as the original. +func typeHash(hasher typeutil.Hasher, types ...types.Type) uint32 { + var hash uint32 + for _, typ := range types { + hash ^= hasher.Hash(typ) + } + return hash +} + +// typeArgsEq returns if both lists of type arguments are identical. +func typeArgsEq(a, b []types.Type) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !types.Identical(a[i], b[i]) { + return false + } + } + + return true +} diff --git a/compiler/internal/typeparams/map_test.go b/compiler/internal/typeparams/map_test.go new file mode 100644 index 000000000..5018ab0d8 --- /dev/null +++ b/compiler/internal/typeparams/map_test.go @@ -0,0 +1,111 @@ +package typeparams + +import ( + "go/token" + "go/types" + "testing" +) + +func TestInstanceMap(t *testing.T) { + i1 := Instance{ + Object: types.NewTypeName(token.NoPos, nil, "i1", nil), + TArgs: []types.Type{ + types.Typ[types.Int], + types.Typ[types.Int8], + }, + } + i1clone := Instance{ + Object: i1.Object, + TArgs: []types.Type{ + types.Typ[types.Int], + types.Typ[types.Int8], + }, + } + + i2 := Instance{ + Object: types.NewTypeName(token.NoPos, nil, "i2", nil), // Different pointer. + TArgs: []types.Type{ + types.Typ[types.Int], + types.Typ[types.Int8], + }, + } + i3 := Instance{ + Object: i1.Object, + TArgs: []types.Type{types.Typ[types.String]}, // Different type args. + } + + _ = i1 + _ = i1clone + _ = i3 + _ = i2 + + m := InstanceMap[string]{} + + // Check operations on a missing key. + t.Run("empty", func(t *testing.T) { + if got := m.Has(i1); got { + t.Errorf("Got: empty map contains %s. Want: empty map contains nothing.", i1) + } + if got := m.Get(i1); got != "" { + t.Errorf("Got: getting missing key returned %q. Want: zero value.", got) + } + if got := m.Len(); got != 0 { + t.Errorf("Got: empty map length %d. Want: 0.", got) + } + if got := m.Set(i1, "abc"); got != "" { + t.Errorf("Got: setting a new key returned old value %q. Want: zero value", got) + } + if got := m.Len(); got != 1 { + t.Errorf("Got: map length %d. Want: 1.", got) + } + }) + + // Check operations on the existing key. + t.Run("first key", func(t *testing.T) { + if got := m.Set(i1, "def"); got != "abc" { + t.Errorf(`Got: setting an existing key returned old value %q. Want: "abc".`, got) + } + if got := m.Len(); got != 1 { + t.Errorf("Got: map length %d. Want: 1.", got) + } + if got := m.Has(i1); !got { + t.Errorf("Got: set map key is reported as missing. Want: key present.") + } + if got := m.Get(i1); got != "def" { + t.Errorf(`Got: getting set key returned %q. Want: "def"`, got) + } + if got := m.Get(i1clone); got != "def" { + t.Errorf(`Got: getting set key returned %q. Want: "def"`, got) + } + }) + + // Check for key collisions. + t.Run("different object", func(t *testing.T) { + if got := m.Has(i2); got { + t.Errorf("Got: a new key %q is reported as present. Want: not present.", i2) + } + if got := m.Set(i2, "123"); got != "" { + t.Errorf("Got: a new key %q overrode an old value %q. Want: zero value.", i2, got) + } + if got := m.Get(i2); got != "123" { + t.Errorf(`Got: getting set key %q returned: %q. Want: "123"`, i2, got) + } + if got := m.Len(); got != 2 { + t.Errorf("Got: map length %d. Want: 2.", got) + } + }) + t.Run("different tArgs", func(t *testing.T) { + if got := m.Has(i3); got { + t.Errorf("Got: a new key %q is reported as present. Want: not present.", i3) + } + if got := m.Set(i3, "456"); got != "" { + t.Errorf("Got: a new key %q overrode an old value %q. Want: zero value.", i3, got) + } + if got := m.Get(i3); got != "456" { + t.Errorf(`Got: getting set key %q returned: %q. Want: "456"`, i3, got) + } + if got := m.Len(); got != 3 { + t.Errorf("Got: map length %d. Want: 3.", got) + } + }) +} diff --git a/compiler/internal/typeparams/subst/export.go b/compiler/internal/typeparams/subst/export.go new file mode 100644 index 000000000..38e394bda --- /dev/null +++ b/compiler/internal/typeparams/subst/export.go @@ -0,0 +1,49 @@ +// Package subst is an excerpt from x/tools/go/ssa responsible for performing +// type substitution in types defined in terms of type parameters with provided +// type arguments. +package subst + +import ( + "go/types" +) + +// To simplify future updates of the borrowed code, we minimize modifications +// to it as much as possible. This file implements an exported interface to the +// original code for us to use. + +// Subster performs type parameter substitution. +type Subster struct { + impl *subster +} + +// New creates a new Subster with a given list of type parameters and matching args. +func New(tc *types.Context, tParams []*types.TypeParam, tArgs []types.Type) *Subster { + assert(len(tParams) == len(tArgs), "New() argument count must match") + + if len(tParams) == 0 { + return nil + } + + subst := &subster{ + replacements: make(map[*types.TypeParam]types.Type, len(tParams)), + cache: make(map[types.Type]types.Type), + ctxt: tc, + scope: nil, + debug: false, + } + for i := 0; i < len(tParams); i++ { + subst.replacements[tParams[i]] = tArgs[i] + } + return &Subster{ + impl: subst, + } +} + +// Type returns a version of typ with all references to type parameters replaced +// with the corresponding type arguments. +func (s *Subster) Type(typ types.Type) types.Type { + if s == nil { + return typ + } + return s.impl.typ(typ) +} diff --git a/compiler/internal/typeparams/subst/subst.go b/compiler/internal/typeparams/subst/subst.go new file mode 100644 index 000000000..9020e94f9 --- /dev/null +++ b/compiler/internal/typeparams/subst/subst.go @@ -0,0 +1,475 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package subst + +import ( + "go/types" +) + +// Type substituter for a fixed set of replacement types. +// +// A nil *subster is an valid, empty substitution map. It always acts as +// the identity function. This allows for treating parameterized and +// non-parameterized functions identically while compiling to ssa. +// +// Not concurrency-safe. +type subster struct { + replacements map[*types.TypeParam]types.Type // values should contain no type params + cache map[types.Type]types.Type // cache of subst results + ctxt *types.Context // cache for instantiation + scope *types.Scope // *types.Named declared within this scope can be substituted (optional) + debug bool // perform extra debugging checks + // TODO(taking): consider adding Pos + // TODO(zpavlinovic): replacements can contain type params + // when generating instances inside of a generic function body. +} + +// Returns a subster that replaces tparams[i] with targs[i]. Uses ctxt as a cache. +// targs should not contain any types in tparams. +// scope is the (optional) lexical block of the generic function for which we are substituting. +func makeSubster(ctxt *types.Context, scope *types.Scope, tparams *types.TypeParamList, targs []types.Type, debug bool) *subster { + assert(tparams.Len() == len(targs), "makeSubster argument count must match") + + subst := &subster{ + replacements: make(map[*types.TypeParam]types.Type, tparams.Len()), + cache: make(map[types.Type]types.Type), + ctxt: ctxt, + scope: scope, + debug: debug, + } + for i := 0; i < tparams.Len(); i++ { + subst.replacements[tparams.At(i)] = targs[i] + } + if subst.debug { + subst.wellFormed() + } + return subst +} + +// wellFormed asserts that subst was properly initialized. +func (subst *subster) wellFormed() { + if subst == nil { + return + } + // Check that all of the type params do not appear in the arguments. + s := make(map[types.Type]bool, len(subst.replacements)) + for tparam := range subst.replacements { + s[tparam] = true + } + for _, r := range subst.replacements { + if reaches(r, s) { + panic(subst) + } + } +} + +// typ returns the type of t with the type parameter tparams[i] substituted +// for the type targs[i] where subst was created using tparams and targs. +func (subst *subster) typ(t types.Type) (res types.Type) { + if subst == nil { + return t // A nil subst is type preserving. + } + if r, ok := subst.cache[t]; ok { + return r + } + defer func() { + subst.cache[t] = res + }() + + // fall through if result r will be identical to t, types.Identical(r, t). + switch t := t.(type) { + case *types.TypeParam: + r := subst.replacements[t] + assert(r != nil, "type param without replacement encountered") + return r + + case *types.Basic: + return t + + case *types.Array: + if r := subst.typ(t.Elem()); r != t.Elem() { + return types.NewArray(r, t.Len()) + } + return t + + case *types.Slice: + if r := subst.typ(t.Elem()); r != t.Elem() { + return types.NewSlice(r) + } + return t + + case *types.Pointer: + if r := subst.typ(t.Elem()); r != t.Elem() { + return types.NewPointer(r) + } + return t + + case *types.Tuple: + return subst.tuple(t) + + case *types.Struct: + return subst.struct_(t) + + case *types.Map: + key := subst.typ(t.Key()) + elem := subst.typ(t.Elem()) + if key != t.Key() || elem != t.Elem() { + return types.NewMap(key, elem) + } + return t + + case *types.Chan: + if elem := subst.typ(t.Elem()); elem != t.Elem() { + return types.NewChan(t.Dir(), elem) + } + return t + + case *types.Signature: + return subst.signature(t) + + case *types.Union: + return subst.union(t) + + case *types.Interface: + return subst.interface_(t) + + case *types.Named: + return subst.named(t) + + default: + panic("unreachable") + } +} + +// types returns the result of {subst.typ(ts[i])}. +func (subst *subster) types(ts []types.Type) []types.Type { + res := make([]types.Type, len(ts)) + for i := range ts { + res[i] = subst.typ(ts[i]) + } + return res +} + +func (subst *subster) tuple(t *types.Tuple) *types.Tuple { + if t != nil { + if vars := subst.varlist(t); vars != nil { + return types.NewTuple(vars...) + } + } + return t +} + +type varlist interface { + At(i int) *types.Var + Len() int +} + +// fieldlist is an adapter for structs for the varlist interface. +type fieldlist struct { + str *types.Struct +} + +func (fl fieldlist) At(i int) *types.Var { return fl.str.Field(i) } +func (fl fieldlist) Len() int { return fl.str.NumFields() } + +func (subst *subster) struct_(t *types.Struct) *types.Struct { + if t != nil { + if fields := subst.varlist(fieldlist{t}); fields != nil { + tags := make([]string, t.NumFields()) + for i, n := 0, t.NumFields(); i < n; i++ { + tags[i] = t.Tag(i) + } + return types.NewStruct(fields, tags) + } + } + return t +} + +// varlist reutrns subst(in[i]) or return nils if subst(v[i]) == v[i] for all i. +func (subst *subster) varlist(in varlist) []*types.Var { + var out []*types.Var // nil => no updates + for i, n := 0, in.Len(); i < n; i++ { + v := in.At(i) + w := subst.var_(v) + if v != w && out == nil { + out = make([]*types.Var, n) + for j := 0; j < i; j++ { + out[j] = in.At(j) + } + } + if out != nil { + out[i] = w + } + } + return out +} + +func (subst *subster) var_(v *types.Var) *types.Var { + if v != nil { + if typ := subst.typ(v.Type()); typ != v.Type() { + if v.IsField() { + return types.NewField(v.Pos(), v.Pkg(), v.Name(), typ, v.Embedded()) + } + return types.NewVar(v.Pos(), v.Pkg(), v.Name(), typ) + } + } + return v +} + +func (subst *subster) union(u *types.Union) *types.Union { + var out []*types.Term // nil => no updates + + for i, n := 0, u.Len(); i < n; i++ { + t := u.Term(i) + r := subst.typ(t.Type()) + if r != t.Type() && out == nil { + out = make([]*types.Term, n) + for j := 0; j < i; j++ { + out[j] = u.Term(j) + } + } + if out != nil { + out[i] = types.NewTerm(t.Tilde(), r) + } + } + + if out != nil { + return types.NewUnion(out) + } + return u +} + +func (subst *subster) interface_(iface *types.Interface) *types.Interface { + if iface == nil { + return nil + } + + // methods for the interface. Initially nil if there is no known change needed. + // Signatures for the method where recv is nil. NewInterfaceType fills in the receivers. + var methods []*types.Func + initMethods := func(n int) { // copy first n explicit methods + methods = make([]*types.Func, iface.NumExplicitMethods()) + for i := 0; i < n; i++ { + f := iface.ExplicitMethod(i) + norecv := changeRecv(f.Type().(*types.Signature), nil) + methods[i] = types.NewFunc(f.Pos(), f.Pkg(), f.Name(), norecv) + } + } + for i := 0; i < iface.NumExplicitMethods(); i++ { + f := iface.ExplicitMethod(i) + // On interfaces, we need to cycle break on anonymous interface types + // being in a cycle with their signatures being in cycles with their receivers + // that do not go through a Named. + norecv := changeRecv(f.Type().(*types.Signature), nil) + sig := subst.typ(norecv) + if sig != norecv && methods == nil { + initMethods(i) + } + if methods != nil { + methods[i] = types.NewFunc(f.Pos(), f.Pkg(), f.Name(), sig.(*types.Signature)) + } + } + + var embeds []types.Type + initEmbeds := func(n int) { // copy first n embedded types + embeds = make([]types.Type, iface.NumEmbeddeds()) + for i := 0; i < n; i++ { + embeds[i] = iface.EmbeddedType(i) + } + } + for i := 0; i < iface.NumEmbeddeds(); i++ { + e := iface.EmbeddedType(i) + r := subst.typ(e) + if e != r && embeds == nil { + initEmbeds(i) + } + if embeds != nil { + embeds[i] = r + } + } + + if methods == nil && embeds == nil { + return iface + } + if methods == nil { + initMethods(iface.NumExplicitMethods()) + } + if embeds == nil { + initEmbeds(iface.NumEmbeddeds()) + } + return types.NewInterfaceType(methods, embeds).Complete() +} + +func (subst *subster) named(t *types.Named) types.Type { + // A named type may be: + // (1) ordinary named type (non-local scope, no type parameters, no type arguments), + // (2) locally scoped type, + // (3) generic (type parameters but no type arguments), or + // (4) instantiated (type parameters and type arguments). + tparams := t.TypeParams() + if tparams.Len() == 0 { + if subst.scope != nil && !subst.scope.Contains(t.Obj().Pos()) { + // Outside the current function scope? + return t // case (1) ordinary + } + + // case (2) locally scoped type. + // Create a new named type to represent this instantiation. + // We assume that local types of distinct instantiations of a + // generic function are distinct, even if they don't refer to + // type parameters, but the spec is unclear; see golang/go#58573. + // + // Subtle: We short circuit substitution and use a newly created type in + // subst, i.e. cache[t]=n, to pre-emptively replace t with n in recursive + // types during traversal. This both breaks infinite cycles and allows for + // constructing types with the replacement applied in subst.typ(under). + // + // Example: + // func foo[T any]() { + // type linkedlist struct { + // next *linkedlist + // val T + // } + // } + // + // When the field `next *linkedlist` is visited during subst.typ(under), + // we want the substituted type for the field `next` to be `*n`. + n := types.NewNamed(t.Obj(), nil, nil) + subst.cache[t] = n + subst.cache[n] = n + n.SetUnderlying(subst.typ(t.Underlying())) + return n + } + targs := t.TypeArgs() + + // insts are arguments to instantiate using. + insts := make([]types.Type, tparams.Len()) + + // case (3) generic ==> targs.Len() == 0 + // Instantiating a generic with no type arguments should be unreachable. + // Please report a bug if you encounter this. + assert(targs.Len() != 0, "substition into a generic Named type is currently unsupported") + + // case (4) instantiated. + // Substitute into the type arguments and instantiate the replacements/ + // Example: + // type N[A any] func() A + // func Foo[T](g N[T]) {} + // To instantiate Foo[string], one goes through {T->string}. To get the type of g + // one subsitutes T with string in {N with typeargs == {T} and typeparams == {A} } + // to get {N with TypeArgs == {string} and typeparams == {A} }. + assert(targs.Len() == tparams.Len(), "typeargs.Len() must match typeparams.Len() if present") + for i, n := 0, targs.Len(); i < n; i++ { + inst := subst.typ(targs.At(i)) // TODO(generic): Check with rfindley for mutual recursion + insts[i] = inst + } + r, err := types.Instantiate(subst.ctxt, t.Origin(), insts, false) + assert(err == nil, "failed to Instantiate Named type") + return r +} + +func (subst *subster) signature(t *types.Signature) types.Type { + tparams := t.TypeParams() + + // We are choosing not to support tparams.Len() > 0 until a need has been observed in practice. + // + // There are some known usages for types.Types coming from types.{Eval,CheckExpr}. + // To support tparams.Len() > 0, we just need to do the following [psuedocode]: + // targs := {subst.replacements[tparams[i]]]}; Instantiate(ctxt, t, targs, false) + + assert(tparams.Len() == 0, "Substituting types.Signatures with generic functions are currently unsupported.") + + // Either: + // (1)non-generic function. + // no type params to substitute + // (2)generic method and recv needs to be substituted. + + // Receivers can be either: + // named + // pointer to named + // interface + // nil + // interface is the problematic case. We need to cycle break there! + recv := subst.var_(t.Recv()) + params := subst.tuple(t.Params()) + results := subst.tuple(t.Results()) + if recv != t.Recv() || params != t.Params() || results != t.Results() { + return types.NewSignatureType(recv, nil, nil, params, results, t.Variadic()) + } + return t +} + +// reaches returns true if a type t reaches any type t' s.t. c[t'] == true. +// It updates c to cache results. +// +// reaches is currently only part of the wellFormed debug logic, and +// in practice c is initially only type parameters. It is not currently +// relied on in production. +func reaches(t types.Type, c map[types.Type]bool) (res bool) { + if c, ok := c[t]; ok { + return c + } + + // c is populated with temporary false entries as types are visited. + // This avoids repeat visits and break cycles. + c[t] = false + defer func() { + c[t] = res + }() + + switch t := t.(type) { + case *types.TypeParam, *types.Basic: + return false + case *types.Array: + return reaches(t.Elem(), c) + case *types.Slice: + return reaches(t.Elem(), c) + case *types.Pointer: + return reaches(t.Elem(), c) + case *types.Tuple: + for i := 0; i < t.Len(); i++ { + if reaches(t.At(i).Type(), c) { + return true + } + } + case *types.Struct: + for i := 0; i < t.NumFields(); i++ { + if reaches(t.Field(i).Type(), c) { + return true + } + } + case *types.Map: + return reaches(t.Key(), c) || reaches(t.Elem(), c) + case *types.Chan: + return reaches(t.Elem(), c) + case *types.Signature: + if t.Recv() != nil && reaches(t.Recv().Type(), c) { + return true + } + return reaches(t.Params(), c) || reaches(t.Results(), c) + case *types.Union: + for i := 0; i < t.Len(); i++ { + if reaches(t.Term(i).Type(), c) { + return true + } + } + case *types.Interface: + for i := 0; i < t.NumEmbeddeds(); i++ { + if reaches(t.Embedded(i), c) { + return true + } + } + for i := 0; i < t.NumExplicitMethods(); i++ { + if reaches(t.ExplicitMethod(i).Type(), c) { + return true + } + } + case *types.Named: + return reaches(t.Underlying(), c) + default: + panic("unreachable") + } + return false +} diff --git a/compiler/internal/typeparams/subst/subst_test.go b/compiler/internal/typeparams/subst/subst_test.go new file mode 100644 index 000000000..53fadbcf0 --- /dev/null +++ b/compiler/internal/typeparams/subst/subst_test.go @@ -0,0 +1,103 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package subst + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" +) + +func TestSubst(t *testing.T) { + const source = ` +package P + +type t0 int +func (t0) f() +type t1 interface{ f() } +type t2 interface{ g() } +type t3 interface{ ~int } + +func Fn0[T t1](x T) T { + x.f() + return x +} + +type A[T any] [4]T +type B[T any] []T +type C[T, S any] []struct{s S; t T} +type D[T, S any] *struct{s S; t *T} +type E[T, S any] interface{ F() (T, S) } +type F[K comparable, V any] map[K]V +type G[T any] chan *T +type H[T any] func() T +type I[T any] struct{x, y, z int; t T} +type J[T any] interface{ t1 } +type K[T any] interface{ t1; F() T } +type L[T any] interface{ F() T; J[T] } + +var _ L[int] = Fn0[L[int]](nil) +` + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "hello.go", source, 0) + if err != nil { + t.Fatal(err) + } + + var conf types.Config + pkg, err := conf.Check("P", fset, []*ast.File{f}, nil) + if err != nil { + t.Fatal(err) + } + + for _, test := range []struct { + expr string // type expression of Named parameterized type + args []string // type expressions of args for named + want string // expected underlying value after substitution + }{ + {"A", []string{"string"}, "[4]string"}, + {"A", []string{"int"}, "[4]int"}, + {"B", []string{"int"}, "[]int"}, + {"B", []string{"int8"}, "[]int8"}, + {"C", []string{"int8", "string"}, "[]struct{s string; t int8}"}, + {"C", []string{"string", "int8"}, "[]struct{s int8; t string}"}, + {"D", []string{"int16", "string"}, "*struct{s string; t *int16}"}, + {"E", []string{"int32", "string"}, "interface{F() (int32, string)}"}, + {"F", []string{"int64", "string"}, "map[int64]string"}, + {"G", []string{"uint64"}, "chan *uint64"}, + {"H", []string{"uintptr"}, "func() uintptr"}, + {"I", []string{"t0"}, "struct{x int; y int; z int; t P.t0}"}, + {"J", []string{"t0"}, "interface{P.t1}"}, + {"K", []string{"t0"}, "interface{F() P.t0; P.t1}"}, + {"L", []string{"t0"}, "interface{F() P.t0; P.J[P.t0]}"}, + {"L", []string{"L[t0]"}, "interface{F() P.L[P.t0]; P.J[P.L[P.t0]]}"}, + } { + // Eval() expr for its type. + tv, err := types.Eval(fset, pkg, 0, test.expr) + if err != nil { + t.Fatalf("Eval(%s) failed: %v", test.expr, err) + } + // Eval() test.args[i] to get the i'th type arg. + var targs []types.Type + for _, astr := range test.args { + tv, err := types.Eval(fset, pkg, 0, astr) + if err != nil { + t.Fatalf("Eval(%s) failed: %v", astr, err) + } + targs = append(targs, tv.Type) + } + + T := tv.Type.(*types.Named) + + subst := makeSubster(types.NewContext(), nil, T.TypeParams(), targs, true) + sub := subst.typ(T.Underlying()) + if got := sub.String(); got != test.want { + t.Errorf("subst{%v->%v}.typ(%s) = %v, want %v", test.expr, test.args, T.Underlying(), got, test.want) + } + } +} diff --git a/compiler/internal/typeparams/subst/util.go b/compiler/internal/typeparams/subst/util.go new file mode 100644 index 000000000..22072e39f --- /dev/null +++ b/compiler/internal/typeparams/subst/util.go @@ -0,0 +1,23 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package subst + +import "go/types" + +// This file defines a number of miscellaneous utility functions. + +//// Sanity checking utilities + +// assert panics with the mesage msg if p is false. +// Avoid combining with expensive string formatting. +func assert(p bool, msg string) { + if !p { + panic(msg) + } +} + +func changeRecv(s *types.Signature, recv *types.Var) *types.Signature { + return types.NewSignatureType(recv, nil, nil, s.Params(), s.Results(), s.Variadic()) +} diff --git a/compiler/internal/typeparams/utils.go b/compiler/internal/typeparams/utils.go new file mode 100644 index 000000000..1d4598f3b --- /dev/null +++ b/compiler/internal/typeparams/utils.go @@ -0,0 +1,16 @@ +package typeparams + +import "go/types" + +// SignatureTypeParams returns receiver type params for methods, or function +// type params for standalone functions, or nil for non-generic functions and +// methods. +func SignatureTypeParams(sig *types.Signature) *types.TypeParamList { + if tp := sig.RecvTypeParams(); tp != nil { + return tp + } else if tp := sig.TypeParams(); tp != nil { + return tp + } else { + return nil + } +} diff --git a/compiler/linkname.go b/compiler/linkname.go index ae1e3ea2b..0bb2b3509 100644 --- a/compiler/linkname.go +++ b/compiler/linkname.go @@ -4,10 +4,10 @@ import ( "fmt" "go/ast" "go/token" - "go/types" "strings" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/symbol" ) // GoLinkname describes a go:linkname compiler directive found in the source code. @@ -17,62 +17,8 @@ import ( // symbols referencing it. This is subtly different from the upstream Go // implementation, which simply overrides symbol name the linker will use. type GoLinkname struct { - Implementation SymName - Reference SymName -} - -// SymName uniquely identifies a named submol within a program. -// -// This is a logical equivalent of a symbol name used by traditional linkers. -// The following properties should hold true: -// -// - Each named symbol within a program has a unique SymName. -// - Similarly named methods of different types will have different symbol names. -// - The string representation is opaque and should not be attempted to reversed -// to a struct form. -type SymName struct { - PkgPath string // Full package import path. - Name string // Symbol name. -} - -// newSymName constructs SymName for a given named symbol. -func newSymName(o types.Object) SymName { - if fun, ok := o.(*types.Func); ok { - sig := fun.Type().(*types.Signature) - if recv := sig.Recv(); recv != nil { - // Special case: disambiguate names for different types' methods. - typ := recv.Type() - if ptr, ok := typ.(*types.Pointer); ok { - return SymName{ - PkgPath: o.Pkg().Path(), - Name: "(*" + ptr.Elem().(*types.Named).Obj().Name() + ")." + o.Name(), - } - } - return SymName{ - PkgPath: o.Pkg().Path(), - Name: typ.(*types.Named).Obj().Name() + "." + o.Name(), - } - } - } - return SymName{ - PkgPath: o.Pkg().Path(), - Name: o.Name(), - } -} - -func (n SymName) String() string { return n.PkgPath + "." + n.Name } - -func (n SymName) IsMethod() (recv string, method string, ok bool) { - pos := strings.IndexByte(n.Name, '.') - if pos == -1 { - return - } - recv, method, ok = n.Name[:pos], n.Name[pos+1:], true - size := len(recv) - if size > 2 && recv[0] == '(' && recv[size-1] == ')' { - recv = recv[1 : size-1] - } - return + Implementation symbol.Name + Reference symbol.Name } // parseGoLinknames processed comments in a source file and extracts //go:linkname @@ -152,8 +98,8 @@ func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]Go } // Local function has no body, treat it as a reference to an external implementation. directives = append(directives, GoLinkname{ - Reference: SymName{PkgPath: localPkg, Name: localName}, - Implementation: SymName{PkgPath: extPkg, Name: extName}, + Reference: symbol.Name{PkgPath: localPkg, Name: localName}, + Implementation: symbol.Name{PkgPath: extPkg, Name: extName}, }) return nil } @@ -172,17 +118,17 @@ func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]Go // goLinknameSet is a utility that enables quick lookup of whether a decl is // affected by any go:linkname directive in the program. type goLinknameSet struct { - byImplementation map[SymName][]GoLinkname - byReference map[SymName]GoLinkname + byImplementation map[symbol.Name][]GoLinkname + byReference map[symbol.Name]GoLinkname } // Add more GoLinkname directives into the set. func (gls *goLinknameSet) Add(entries []GoLinkname) error { if gls.byImplementation == nil { - gls.byImplementation = map[SymName][]GoLinkname{} + gls.byImplementation = map[symbol.Name][]GoLinkname{} } if gls.byReference == nil { - gls.byReference = map[SymName]GoLinkname{} + gls.byReference = map[symbol.Name]GoLinkname{} } for _, e := range entries { gls.byImplementation[e.Implementation] = append(gls.byImplementation[e.Implementation], e) @@ -197,7 +143,7 @@ func (gls *goLinknameSet) Add(entries []GoLinkname) error { // IsImplementation returns true if there is a directive referencing this symbol // as an implementation. -func (gls *goLinknameSet) IsImplementation(sym SymName) bool { +func (gls *goLinknameSet) IsImplementation(sym symbol.Name) bool { _, found := gls.byImplementation[sym] return found } @@ -205,7 +151,7 @@ func (gls *goLinknameSet) IsImplementation(sym SymName) bool { // FindImplementation returns a symbol name, which provides the implementation // for the given symbol. The second value indicates whether the implementation // was found. -func (gls *goLinknameSet) FindImplementation(sym SymName) (SymName, bool) { +func (gls *goLinknameSet) FindImplementation(sym symbol.Name) (symbol.Name, bool) { directive, found := gls.byReference[sym] return directive.Implementation, found } diff --git a/compiler/linkname_test.go b/compiler/linkname_test.go index d0ce9c542..7f46c6cfb 100644 --- a/compiler/linkname_test.go +++ b/compiler/linkname_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/gopherjs/gopherjs/compiler/internal/symbol" ) func parseSource(t *testing.T, src string) (*ast.File, *token.FileSet) { @@ -41,49 +42,6 @@ func makePackage(t *testing.T, src string) *types.Package { return pkg } -func TestSymName(t *testing.T) { - pkg := makePackage(t, - `package testcase - - func AFunction() {} - type AType struct {} - func (AType) AMethod() {} - func (*AType) APointerMethod() {} - var AVariable int32 - `) - - tests := []struct { - obj types.Object - want SymName - }{ - { - obj: pkg.Scope().Lookup("AFunction"), - want: SymName{PkgPath: "testcase", Name: "AFunction"}, - }, { - obj: pkg.Scope().Lookup("AType"), - want: SymName{PkgPath: "testcase", Name: "AType"}, - }, { - obj: types.NewMethodSet(pkg.Scope().Lookup("AType").Type()).Lookup(pkg, "AMethod").Obj(), - want: SymName{PkgPath: "testcase", Name: "AType.AMethod"}, - }, { - obj: types.NewMethodSet(types.NewPointer(pkg.Scope().Lookup("AType").Type())).Lookup(pkg, "APointerMethod").Obj(), - want: SymName{PkgPath: "testcase", Name: "(*AType).APointerMethod"}, - }, { - obj: pkg.Scope().Lookup("AVariable"), - want: SymName{PkgPath: "testcase", Name: "AVariable"}, - }, - } - - for _, test := range tests { - t.Run(test.obj.Name(), func(t *testing.T) { - got := newSymName(test.obj) - if got != test.want { - t.Errorf("NewSymName(%q) returned %#v, want: %#v", test.obj.Name(), got, test.want) - } - }) - } -} - func TestParseGoLinknames(t *testing.T) { tests := []struct { desc string @@ -114,8 +72,8 @@ func TestParseGoLinknames(t *testing.T) { `, wantDirectives: []GoLinkname{ { - Reference: SymName{PkgPath: "testcase", Name: "a"}, - Implementation: SymName{PkgPath: "other/package", Name: "testcase_a"}, + Reference: symbol.Name{PkgPath: "testcase", Name: "a"}, + Implementation: symbol.Name{PkgPath: "other/package", Name: "testcase_a"}, }, }, }, { @@ -132,11 +90,11 @@ func TestParseGoLinknames(t *testing.T) { `, wantDirectives: []GoLinkname{ { - Reference: SymName{PkgPath: "testcase", Name: "a"}, - Implementation: SymName{PkgPath: "other/package", Name: "a"}, + Reference: symbol.Name{PkgPath: "testcase", Name: "a"}, + Implementation: symbol.Name{PkgPath: "other/package", Name: "a"}, }, { - Reference: SymName{PkgPath: "testcase", Name: "b"}, - Implementation: SymName{PkgPath: "other/package", Name: "b"}, + Reference: symbol.Name{PkgPath: "testcase", Name: "b"}, + Implementation: symbol.Name{PkgPath: "other/package", Name: "b"}, }, }, }, { diff --git a/compiler/package.go b/compiler/package.go index ad918ba3e..112a16af7 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -6,7 +6,6 @@ import ( "fmt" "go/ast" "go/constant" - "go/scanner" "go/token" "go/types" "sort" @@ -15,6 +14,9 @@ import ( "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/symbol" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/typesutil" "github.com/neelance/astrewrite" "golang.org/x/tools/go/gcexportdata" "golang.org/x/tools/go/types/typeutil" @@ -23,11 +25,11 @@ import ( // pkgContext maintains compiler context for a specific package. type pkgContext struct { *analysis.Info - additionalSelections map[*ast.SelectorExpr]selection + additionalSelections map[*ast.SelectorExpr]typesutil.Selection - typeNames []*types.TypeName + typesCtx *types.Context + typeNames typesutil.TypeNames pkgVars map[string]string - objectNames map[types.Object]string varPtrNames map[*types.Var]string anonTypes []*types.TypeName anonTypeMap typeutil.Map @@ -37,45 +39,18 @@ type pkgContext struct { minify bool fileSet *token.FileSet errList ErrorList + instanceSet *typeparams.InstanceSet } -func (p *pkgContext) SelectionOf(e *ast.SelectorExpr) (selection, bool) { - if sel, ok := p.Selections[e]; ok { - return sel, true - } - if sel, ok := p.additionalSelections[e]; ok { - return sel, true - } - return nil, false -} - -type selection interface { - Kind() types.SelectionKind - Recv() types.Type - Index() []int - Obj() types.Object - Type() types.Type -} - -type fakeSelection struct { - kind types.SelectionKind - recv types.Type - index []int - obj types.Object - typ types.Type -} - -func (sel *fakeSelection) Kind() types.SelectionKind { return sel.kind } -func (sel *fakeSelection) Recv() types.Type { return sel.recv } -func (sel *fakeSelection) Index() []int { return sel.index } -func (sel *fakeSelection) Obj() types.Object { return sel.obj } -func (sel *fakeSelection) Type() types.Type { return sel.typ } - // funcContext maintains compiler context for a specific function (lexical scope?). type funcContext struct { *analysis.FuncInfo - pkgCtx *pkgContext - parent *funcContext + pkgCtx *pkgContext + parent *funcContext + // Signature of the function this context corresponds to or nil. For generic + // functions it is the original generic signature to make sure result variable + // identity in the signature matches the variable objects referenced in the + // function body. sig *types.Signature allVars map[string]int localVars []string @@ -87,6 +62,9 @@ type funcContext struct { delayedOutput []byte posAvailable bool pos token.Pos + typeResolver *typeparams.Resolver + // Mapping from function-level objects to JS variable names they have been assigned. + objectNames map[types.Object]string } type flowData struct { @@ -151,6 +129,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor Implicits: make(map[ast.Node]types.Object), Selections: make(map[*ast.SelectorExpr]*types.Selection), Scopes: make(map[ast.Node]*types.Scope), + Instances: make(map[*ast.Ident]types.Instance), } var errList ErrorList @@ -172,6 +151,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor var importError error var previousErr error config := &types.Config{ + Context: types.NewContext(), Importer: packageImporter{ importContext: importContext, importError: &importError, @@ -231,26 +211,40 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor } panic(fullName) } + + tc := typeparams.Collector{ + TContext: config.Context, + Info: typesInfo, + Instances: &typeparams.InstanceSet{}, + } + tc.Scan(simplifiedFiles...) + instancesByObj := map[types.Object][]typeparams.Instance{} + for _, inst := range tc.Instances.Values() { + instancesByObj[inst.Object] = append(instancesByObj[inst.Object], inst) + } + pkgInfo := analysis.AnalyzePkg(simplifiedFiles, fileSet, typesInfo, typesPkg, isBlocking) funcCtx := &funcContext{ FuncInfo: pkgInfo.InitFuncInfo, pkgCtx: &pkgContext{ Info: pkgInfo, - additionalSelections: make(map[*ast.SelectorExpr]selection), + additionalSelections: make(map[*ast.SelectorExpr]typesutil.Selection), + typesCtx: config.Context, pkgVars: make(map[string]string), - objectNames: make(map[types.Object]string), varPtrNames: make(map[*types.Var]string), escapingVars: make(map[*types.Var]bool), indentation: 1, dependencies: make(map[types.Object]bool), minify: minify, fileSet: fileSet, + instanceSet: tc.Instances, }, allVars: make(map[string]int), flowDatas: map[*types.Label]*flowData{nil: {}}, caseCounter: 1, labelCases: make(map[*types.Label]int), + objectNames: map[types.Object]string{}, } for name := range reservedKeywords { funcCtx.allVars[name] = 1 @@ -299,7 +293,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor case token.TYPE: for _, spec := range d.Specs { o := funcCtx.pkgCtx.Defs[spec.(*ast.TypeSpec).Name].(*types.TypeName) - funcCtx.pkgCtx.typeNames = append(funcCtx.pkgCtx.typeNames, o) + funcCtx.pkgCtx.typeNames.Add(o) funcCtx.objectName(o) // register toplevel name } case token.VAR: @@ -393,69 +387,81 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor var mainFunc *types.Func for _, fun := range functions { o := funcCtx.pkgCtx.Defs[fun.Name].(*types.Func) + sig := o.Type().(*types.Signature) - if fun.Type.TypeParams.NumFields() > 0 { - return nil, scanner.Error{ - Pos: fileSet.Position(fun.Type.TypeParams.Pos()), - Msg: fmt.Sprintf("function %s: type parameters are not supported by GopherJS: https://github.com/gopherjs/gopherjs/issues/1013", o.Name()), - } - } - funcInfo := funcCtx.pkgCtx.FuncDeclInfos[o] - d := Decl{ - FullName: o.FullName(), - Blocking: len(funcInfo.Blocking) != 0, + var instances []typeparams.Instance + if typeparams.SignatureTypeParams(sig) != nil { + instances = instancesByObj[o] + } else { + instances = []typeparams.Instance{{Object: o}} } - d.LinkingName = newSymName(o) + if fun.Recv == nil { - d.Vars = []string{funcCtx.objectName(o)} - d.DceObjectFilter = o.Name() - switch o.Name() { - case "main": - mainFunc = o - d.DceObjectFilter = "" - case "init": - d.InitCode = funcCtx.CatchOutput(1, func() { - id := funcCtx.newIdent("", types.NewSignatureType(nil, nil, nil, nil, nil, false)) - funcCtx.pkgCtx.Uses[id] = o - call := &ast.CallExpr{Fun: id} - if len(funcCtx.pkgCtx.FuncDeclInfos[o].Blocking) != 0 { - funcCtx.Blocking[call] = true - } - funcCtx.translateStmt(&ast.ExprStmt{X: call}, nil) + // Auxiliary decl shared by all instances of the function that defines + // package-level variable by which they all are referenced. + // TODO(nevkontakte): Set DCE attributes such that it is eliminated if all + // instances are dead. + varDecl := Decl{} + varDecl.Vars = []string{funcCtx.objectName(o)} + if o.Type().(*types.Signature).TypeParams().Len() != 0 { + varDecl.DeclCode = funcCtx.CatchOutput(0, func() { + funcCtx.Printf("%s = {};", funcCtx.objectName(o)) }) - d.DceObjectFilter = "" } - } else { - recvType := o.Type().(*types.Signature).Recv().Type() - ptr, isPointer := recvType.(*types.Pointer) - namedRecvType, _ := recvType.(*types.Named) - if isPointer { - namedRecvType = ptr.Elem().(*types.Named) + funcDecls = append(funcDecls, &varDecl) + } + + for _, inst := range instances { + funcInfo := funcCtx.pkgCtx.FuncDeclInfos[o] + d := Decl{ + FullName: o.FullName(), + Blocking: len(funcInfo.Blocking) != 0, } - if namedRecvType.TypeParams() != nil { - return nil, scanner.Error{ - Pos: fileSet.Position(o.Pos()), - Msg: fmt.Sprintf("type %s: type parameters are not supported by GopherJS: https://github.com/gopherjs/gopherjs/issues/1013", o.FullName()), + d.LinkingName = symbol.New(o) + if fun.Recv == nil { + d.RefExpr = funcCtx.instName(inst) + d.DceObjectFilter = o.Name() + switch o.Name() { + case "main": + mainFunc = o + d.DceObjectFilter = "" + case "init": + d.InitCode = funcCtx.CatchOutput(1, func() { + id := funcCtx.newIdent("", types.NewSignatureType( /*recv=*/ nil /*rectTypeParams=*/, nil /*typeParams=*/, nil /*params=*/, nil /*results=*/, nil /*variadic=*/, false)) + funcCtx.pkgCtx.Uses[id] = o + call := &ast.CallExpr{Fun: id} + if len(funcCtx.pkgCtx.FuncDeclInfos[o].Blocking) != 0 { + funcCtx.Blocking[call] = true + } + funcCtx.translateStmt(&ast.ExprStmt{X: call}, nil) + }) + d.DceObjectFilter = "" + } + } else { + recvType := o.Type().(*types.Signature).Recv().Type() + ptr, isPointer := recvType.(*types.Pointer) + namedRecvType, _ := recvType.(*types.Named) + if isPointer { + namedRecvType = ptr.Elem().(*types.Named) + } + d.NamedRecvType = funcCtx.objectName(namedRecvType.Obj()) + d.DceObjectFilter = namedRecvType.Obj().Name() + if !fun.Name.IsExported() { + d.DceMethodFilter = o.Name() + "~" } } - name := funcCtx.objectName(namedRecvType.Obj()) - d.NamedRecvType = name - d.DceObjectFilter = namedRecvType.Obj().Name() - if !fun.Name.IsExported() { - d.DceMethodFilter = o.Name() + "~" - } - } - d.DceDeps = collectDependencies(func() { - d.DeclCode = funcCtx.translateToplevelFunction(fun, funcInfo) - }) - funcDecls = append(funcDecls, &d) + d.DceDeps = collectDependencies(func() { + d.DeclCode = funcCtx.translateToplevelFunction(fun, funcInfo, inst) + }) + funcDecls = append(funcDecls, &d) + } } if typesPkg.Name() == "main" { if mainFunc == nil { return nil, fmt.Errorf("missing main function") } - id := funcCtx.newIdent("", types.NewSignatureType(nil, nil, nil, nil, nil, false)) + id := funcCtx.newIdent("", types.NewSignatureType( /*recv=*/ nil /*rectTypeParams=*/, nil /*typeParams=*/, nil /*params=*/, nil /*results=*/, nil /*variadic=*/, false)) funcCtx.pkgCtx.Uses[id] = mainFunc call := &ast.CallExpr{Fun: id} ifStmt := &ast.IfStmt{ @@ -484,99 +490,120 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor // named types var typeDecls []*Decl - for _, o := range funcCtx.pkgCtx.typeNames { + for _, o := range funcCtx.pkgCtx.typeNames.Slice() { if o.IsAlias() { continue } + typ := o.Type().(*types.Named) + var instances []typeparams.Instance + if typ.TypeParams() != nil { + instances = instancesByObj[o] + } else { + instances = []typeparams.Instance{{Object: o}} + } + typeName := funcCtx.objectName(o) - if named, ok := o.Type().(*types.Named); ok && named.TypeParams().Len() > 0 { - return nil, scanner.Error{ - Pos: fileSet.Position(o.Pos()), - Msg: fmt.Sprintf("type %s: type parameters are not supported by GopherJS: https://github.com/gopherjs/gopherjs/issues/1013", o.Name()), - } + varDecl := Decl{Vars: []string{typeName}} + if typ.TypeParams() != nil { + varDecl.DeclCode = funcCtx.CatchOutput(0, func() { + funcCtx.Printf("%s = {};", funcCtx.objectName(o)) + }) } - - d := Decl{ - Vars: []string{typeName}, - DceObjectFilter: o.Name(), + if isPkgLevel(o) { + varDecl.TypeInitCode = funcCtx.CatchOutput(0, func() { + funcCtx.Printf("$pkg.%s = %s;", encodeIdent(o.Name()), funcCtx.objectName(o)) + }) } - d.DceDeps = collectDependencies(func() { - d.DeclCode = funcCtx.CatchOutput(0, func() { - typeName := funcCtx.objectName(o) - lhs := typeName - if isPkgLevel(o) { - lhs += " = $pkg." + encodeIdent(o.Name()) + typeDecls = append(typeDecls, &varDecl) + + for _, inst := range instances { + funcCtx.typeResolver = typeparams.NewResolver(funcCtx.pkgCtx.typesCtx, typeparams.ToSlice(typ.TypeParams()), inst.TArgs) + + named := typ + if !inst.IsTrivial() { + instantiated, err := types.Instantiate(funcCtx.pkgCtx.typesCtx, typ, inst.TArgs, true) + if err != nil { + return nil, fmt.Errorf("failed to instantiate type %v with args %v: %w", typ, inst.TArgs, err) } - size := int64(0) - constructor := "null" - switch t := o.Type().Underlying().(type) { - case *types.Struct: - params := make([]string, t.NumFields()) - for i := 0; i < t.NumFields(); i++ { - params[i] = fieldName(t, i) + "_" - } - constructor = fmt.Sprintf("function(%s) {\n\t\tthis.$val = this;\n\t\tif (arguments.length === 0) {\n", strings.Join(params, ", ")) - for i := 0; i < t.NumFields(); i++ { - constructor += fmt.Sprintf("\t\t\tthis.%s = %s;\n", fieldName(t, i), funcCtx.translateExpr(funcCtx.zeroValue(t.Field(i).Type())).String()) + named = instantiated.(*types.Named) + } + underlying := named.Underlying() + d := Decl{ + DceObjectFilter: o.Name(), + } + d.DceDeps = collectDependencies(func() { + d.DeclCode = funcCtx.CatchOutput(0, func() { + size := int64(0) + constructor := "null" + switch t := underlying.(type) { + case *types.Struct: + params := make([]string, t.NumFields()) + for i := 0; i < t.NumFields(); i++ { + params[i] = fieldName(t, i) + "_" + } + constructor = fmt.Sprintf("function(%s) {\n\t\tthis.$val = this;\n\t\tif (arguments.length === 0) {\n", strings.Join(params, ", ")) + for i := 0; i < t.NumFields(); i++ { + constructor += fmt.Sprintf("\t\t\tthis.%s = %s;\n", fieldName(t, i), funcCtx.translateExpr(funcCtx.zeroValue(t.Field(i).Type())).String()) + } + constructor += "\t\t\treturn;\n\t\t}\n" + for i := 0; i < t.NumFields(); i++ { + constructor += fmt.Sprintf("\t\tthis.%[1]s = %[1]s_;\n", fieldName(t, i)) + } + constructor += "\t}" + case *types.Basic, *types.Array, *types.Slice, *types.Chan, *types.Signature, *types.Interface, *types.Pointer, *types.Map: + size = sizes32.Sizeof(t) } - constructor += "\t\t\treturn;\n\t\t}\n" - for i := 0; i < t.NumFields(); i++ { - constructor += fmt.Sprintf("\t\tthis.%[1]s = %[1]s_;\n", fieldName(t, i)) + if tPointer, ok := underlying.(*types.Pointer); ok { + if _, ok := tPointer.Elem().Underlying().(*types.Array); ok { + // Array pointers have non-default constructors to support wrapping + // of the native objects. + constructor = "$arrayPtrCtor()" + } } - constructor += "\t}" - case *types.Basic, *types.Array, *types.Slice, *types.Chan, *types.Signature, *types.Interface, *types.Pointer, *types.Map: - size = sizes32.Sizeof(t) - } - if tPointer, ok := o.Type().Underlying().(*types.Pointer); ok { - if _, ok := tPointer.Elem().Underlying().(*types.Array); ok { - // Array pointers have non-default constructors to support wrapping - // of the native objects. - constructor = "$arrayPtrCtor()" + funcCtx.Printf(`%s = $newType(%d, %s, %q, %t, "%s", %t, %s);`, funcCtx.instName(inst), size, typeKind(typ), inst.TypeString(), o.Name() != "", o.Pkg().Path(), o.Exported(), constructor) + }) + d.MethodListCode = funcCtx.CatchOutput(0, func() { + if _, ok := underlying.(*types.Interface); ok { + return } - } - funcCtx.Printf(`%s = $newType(%d, %s, "%s.%s", %t, "%s", %t, %s);`, lhs, size, typeKind(o.Type()), o.Pkg().Name(), o.Name(), o.Name() != "", o.Pkg().Path(), o.Exported(), constructor) - }) - d.MethodListCode = funcCtx.CatchOutput(0, func() { - named := o.Type().(*types.Named) - if _, ok := named.Underlying().(*types.Interface); ok { - return - } - var methods []string - var ptrMethods []string - for i := 0; i < named.NumMethods(); i++ { - method := named.Method(i) - name := method.Name() - if reservedKeywords[name] { - name += "$" + var methods []string + var ptrMethods []string + for i := 0; i < named.NumMethods(); i++ { + method := named.Method(i) + name := method.Name() + if reservedKeywords[name] { + name += "$" + } + pkgPath := "" + if !method.Exported() { + pkgPath = method.Pkg().Path() + } + t := method.Type().(*types.Signature) + entry := fmt.Sprintf(`{prop: "%s", name: %s, pkg: "%s", typ: $funcType(%s)}`, name, encodeString(method.Name()), pkgPath, funcCtx.initArgs(t)) + if _, isPtr := t.Recv().Type().(*types.Pointer); isPtr { + ptrMethods = append(ptrMethods, entry) + continue + } + methods = append(methods, entry) } - pkgPath := "" - if !method.Exported() { - pkgPath = method.Pkg().Path() + if len(methods) > 0 { + funcCtx.Printf("%s.methods = [%s];", funcCtx.instName(inst), strings.Join(methods, ", ")) } - t := method.Type().(*types.Signature) - entry := fmt.Sprintf(`{prop: "%s", name: %s, pkg: "%s", typ: $funcType(%s)}`, name, encodeString(method.Name()), pkgPath, funcCtx.initArgs(t)) - if _, isPtr := t.Recv().Type().(*types.Pointer); isPtr { - ptrMethods = append(ptrMethods, entry) - continue + if len(ptrMethods) > 0 { + funcCtx.Printf("%s.methods = [%s];", funcCtx.typeName(types.NewPointer(named)), strings.Join(ptrMethods, ", ")) } - methods = append(methods, entry) - } - if len(methods) > 0 { - funcCtx.Printf("%s.methods = [%s];", funcCtx.typeName(named), strings.Join(methods, ", ")) - } - if len(ptrMethods) > 0 { - funcCtx.Printf("%s.methods = [%s];", funcCtx.typeName(types.NewPointer(named)), strings.Join(ptrMethods, ", ")) + }) + switch t := underlying.(type) { + case *types.Array, *types.Chan, *types.Interface, *types.Map, *types.Pointer, *types.Slice, *types.Signature, *types.Struct: + d.TypeInitCode = funcCtx.CatchOutput(0, func() { + funcCtx.Printf("%s.init(%s);", funcCtx.instName(inst), funcCtx.initArgs(t)) + }) } }) - switch t := o.Type().Underlying().(type) { - case *types.Array, *types.Chan, *types.Interface, *types.Map, *types.Pointer, *types.Slice, *types.Signature, *types.Struct: - d.TypeInitCode = funcCtx.CatchOutput(0, func() { - funcCtx.Printf("%s.init(%s);", funcCtx.objectName(o), funcCtx.initArgs(t)) - }) - } - }) - typeDecls = append(typeDecls, &d) + typeDecls = append(typeDecls, &d) + } + funcCtx.typeResolver = nil } // anonymous types @@ -670,8 +697,8 @@ func (fc *funcContext) initArgs(ty types.Type) string { } } -func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analysis.FuncInfo) []byte { - o := fc.pkgCtx.Defs[fun.Name].(*types.Func) +func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analysis.FuncInfo, inst typeparams.Instance) []byte { + o := inst.Object.(*types.Func) sig := o.Type().(*types.Signature) var recv *ast.Ident if fun.Recv != nil && fun.Recv.List[0].Names != nil { @@ -684,7 +711,7 @@ func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analys return []byte(fmt.Sprintf("\t%s = function() {\n\t\t$throwRuntimeError(\"native function not implemented: %s\");\n\t};\n", funcRef, o.FullName())) } - params, fun := translateFunction(fun.Type, recv, fun.Body, fc, sig, info, funcRef) + params, fun := translateFunction(fun.Type, recv, fun.Body, fc, sig, info, funcRef, inst) joinedParams = strings.Join(params, ", ") return []byte(fmt.Sprintf("\t%s = %s;\n", funcRef, fun)) } @@ -692,7 +719,7 @@ func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analys code := bytes.NewBuffer(nil) if fun.Recv == nil { - funcRef := fc.objectName(o) + funcRef := fc.instName(inst) code.Write(primaryFunction(funcRef)) if fun.Name.IsExported() { fmt.Fprintf(code, "\t$pkg.%s = %s;\n", encodeIdent(fun.Name.Name), funcRef) @@ -700,63 +727,70 @@ func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analys return code.Bytes() } - recvType := sig.Recv().Type() - ptr, isPointer := recvType.(*types.Pointer) - namedRecvType, _ := recvType.(*types.Named) - if isPointer { - namedRecvType = ptr.Elem().(*types.Named) - } - typeName := fc.objectName(namedRecvType.Obj()) + recvInst := inst.Recv() + recvInstName := fc.instName(recvInst) + recvType := recvInst.Object.Type().(*types.Named) funName := fun.Name.Name if reservedKeywords[funName] { funName += "$" } - if _, isStruct := namedRecvType.Underlying().(*types.Struct); isStruct { - code.Write(primaryFunction(typeName + ".ptr.prototype." + funName)) - fmt.Fprintf(code, "\t%s.prototype.%s = function(%s) { return this.$val.%s(%s); };\n", typeName, funName, joinedParams, funName, joinedParams) + if _, isStruct := recvType.Underlying().(*types.Struct); isStruct { + code.Write(primaryFunction(recvInstName + ".ptr.prototype." + funName)) + fmt.Fprintf(code, "\t%s.prototype.%s = function(%s) { return this.$val.%s(%s); };\n", recvInstName, funName, joinedParams, funName, joinedParams) return code.Bytes() } - if isPointer { + if ptr, isPointer := sig.Recv().Type().(*types.Pointer); isPointer { if _, isArray := ptr.Elem().Underlying().(*types.Array); isArray { - code.Write(primaryFunction(typeName + ".prototype." + funName)) - fmt.Fprintf(code, "\t$ptrType(%s).prototype.%s = function(%s) { return (new %s(this.$get())).%s(%s); };\n", typeName, funName, joinedParams, typeName, funName, joinedParams) + code.Write(primaryFunction(recvInstName + ".prototype." + funName)) + fmt.Fprintf(code, "\t$ptrType(%s).prototype.%s = function(%s) { return (new %s(this.$get())).%s(%s); };\n", recvInstName, funName, joinedParams, recvInstName, funName, joinedParams) return code.Bytes() } - return primaryFunction(fmt.Sprintf("$ptrType(%s).prototype.%s", typeName, funName)) + return primaryFunction(fmt.Sprintf("$ptrType(%s).prototype.%s", recvInstName, funName)) } value := "this.$get()" if isWrapped(recvType) { - value = fmt.Sprintf("new %s(%s)", typeName, value) + value = fmt.Sprintf("new %s(%s)", recvInstName, value) } - code.Write(primaryFunction(typeName + ".prototype." + funName)) - fmt.Fprintf(code, "\t$ptrType(%s).prototype.%s = function(%s) { return %s.%s(%s); };\n", typeName, funName, joinedParams, value, funName, joinedParams) + code.Write(primaryFunction(recvInstName + ".prototype." + funName)) + fmt.Fprintf(code, "\t$ptrType(%s).prototype.%s = function(%s) { return %s.%s(%s); };\n", recvInstName, funName, joinedParams, value, funName, joinedParams) return code.Bytes() } -func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, outerContext *funcContext, sig *types.Signature, info *analysis.FuncInfo, funcRef string) ([]string, string) { +func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, outerContext *funcContext, sig *types.Signature, info *analysis.FuncInfo, funcRef string, inst typeparams.Instance) ([]string, string) { if info == nil { panic("nil info") } c := &funcContext{ - FuncInfo: info, - pkgCtx: outerContext.pkgCtx, - parent: outerContext, - sig: sig, - allVars: make(map[string]int, len(outerContext.allVars)), - localVars: []string{}, - flowDatas: map[*types.Label]*flowData{nil: {}}, - caseCounter: 1, - labelCases: make(map[*types.Label]int), + FuncInfo: info, + pkgCtx: outerContext.pkgCtx, + parent: outerContext, + allVars: make(map[string]int, len(outerContext.allVars)), + localVars: []string{}, + flowDatas: map[*types.Label]*flowData{nil: {}}, + caseCounter: 1, + labelCases: make(map[*types.Label]int), + typeResolver: outerContext.typeResolver, + objectNames: map[types.Object]string{}, + sig: sig, } for k, v := range outerContext.allVars { c.allVars[k] = v } prevEV := c.pkgCtx.escapingVars + if sig.TypeParams().Len() > 0 { + c.typeResolver = typeparams.NewResolver(c.pkgCtx.typesCtx, typeparams.ToSlice(sig.TypeParams()), inst.TArgs) + } else if sig.RecvTypeParams().Len() > 0 { + c.typeResolver = typeparams.NewResolver(c.pkgCtx.typesCtx, typeparams.ToSlice(sig.RecvTypeParams()), inst.TArgs) + } + if c.objectNames == nil { + c.objectNames = map[types.Object]string{} + } + var params []string for _, param := range typ.Params.List { if len(param.Names) == 0 { @@ -782,16 +816,17 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, c.resultNames = make([]ast.Expr, c.sig.Results().Len()) for i := 0; i < c.sig.Results().Len(); i++ { result := c.sig.Results().At(i) - c.Printf("%s = %s;", c.objectName(result), c.translateExpr(c.zeroValue(result.Type())).String()) + typ := c.typeResolver.Substitute(result.Type()) + c.Printf("%s = %s;", c.objectName(result), c.translateExpr(c.zeroValue(typ)).String()) id := ast.NewIdent("") c.pkgCtx.Uses[id] = result - c.resultNames[i] = c.setType(id, result.Type()) + c.resultNames[i] = c.setType(id, typ) } } if recv != nil && !isBlank(recv) { this := "this" - if isWrapped(c.pkgCtx.TypeOf(recv)) { + if isWrapped(c.typeOf(recv)) { this = "this.$val" // Unwrap receiver value. } c.Printf("%s = %s;", c.translateExpr(recv), this) diff --git a/compiler/statements.go b/compiler/statements.go index 8518f9b71..225cd9cdb 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -135,10 +135,10 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { } fc.Printf("%s = %s;", refVar, fc.translateExpr(expr)) translateCond := func(cond ast.Expr) *expression { - if types.Identical(fc.pkgCtx.TypeOf(cond), types.Typ[types.UntypedNil]) { + if types.Identical(fc.typeOf(cond), types.Typ[types.UntypedNil]) { return fc.formatExpr("%s === $ifaceNil", refVar) } - return fc.formatExpr("$assertType(%s, %s, true)[1]", refVar, fc.typeName(fc.pkgCtx.TypeOf(cond))) + return fc.formatExpr("$assertType(%s, %s, true)[1]", refVar, fc.typeName(fc.typeOf(cond))) } var caseClauses []*ast.CaseClause var defaultClause *ast.CaseClause @@ -146,16 +146,17 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { clause := cc.(*ast.CaseClause) var bodyPrefix []ast.Stmt if implicit := fc.pkgCtx.Implicits[clause]; implicit != nil { + typ := fc.typeResolver.Substitute(implicit.Type()) value := refVar - if typesutil.IsJsObject(implicit.Type().Underlying()) { + if typesutil.IsJsObject(typ.Underlying()) { value += ".$val.object" - } else if _, ok := implicit.Type().Underlying().(*types.Interface); !ok { + } else if _, ok := typ.Underlying().(*types.Interface); !ok { value += ".$val" } bodyPrefix = []ast.Stmt{&ast.AssignStmt{ - Lhs: []ast.Expr{fc.newIdent(fc.objectName(implicit), implicit.Type())}, + Lhs: []ast.Expr{fc.newIdent(fc.objectName(implicit), typ)}, Tok: token.DEFINE, - Rhs: []ast.Expr{fc.newIdent(value, implicit.Type())}, + Rhs: []ast.Expr{fc.newIdent(value, typ)}, }} } c := &ast.CaseClause{ @@ -190,7 +191,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { refVar := fc.newVariable("_ref") fc.Printf("%s = %s;", refVar, fc.translateExpr(s.X)) - switch t := fc.pkgCtx.TypeOf(s.X).Underlying().(type) { + switch t := fc.typeOf(s.X).Underlying().(type) { case *types.Basic: iVar := fc.newVariable("_i") fc.Printf("%s = 0;", iVar) @@ -380,7 +381,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { case len(s.Lhs) == 1 && len(s.Rhs) == 1: lhs := astutil.RemoveParens(s.Lhs[0]) if isBlank(lhs) { - fc.Printf("$unused(%s);", fc.translateImplicitConversion(s.Rhs[0], fc.pkgCtx.TypeOf(s.Lhs[0]))) + fc.Printf("$unused(%s);", fc.translateImplicitConversion(s.Rhs[0], fc.typeOf(s.Lhs[0]))) return } fc.Printf("%s", fc.translateAssign(lhs, s.Rhs[0], s.Tok == token.DEFINE)) @@ -388,7 +389,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { case len(s.Lhs) > 1 && len(s.Rhs) == 1: tupleVar := fc.newVariable("_tuple") fc.Printf("%s = %s;", tupleVar, fc.translateExpr(s.Rhs[0])) - tuple := fc.pkgCtx.TypeOf(s.Rhs[0]).(*types.Tuple) + tuple := fc.typeOf(s.Rhs[0]).(*types.Tuple) for i, lhs := range s.Lhs { lhs = astutil.RemoveParens(lhs) if !isBlank(lhs) { @@ -403,12 +404,12 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { fc.Printf("$unused(%s);", fc.translateExpr(rhs)) continue } - fc.Printf("%s", fc.translateAssign(fc.newIdent(tmpVars[i], fc.pkgCtx.TypeOf(s.Lhs[i])), rhs, true)) + fc.Printf("%s", fc.translateAssign(fc.newIdent(tmpVars[i], fc.typeOf(s.Lhs[i])), rhs, true)) } for i, lhs := range s.Lhs { lhs = astutil.RemoveParens(lhs) if !isBlank(lhs) { - fc.Printf("%s", fc.translateAssign(lhs, fc.newIdent(tmpVars[i], fc.pkgCtx.TypeOf(lhs)), s.Tok == token.DEFINE)) + fc.Printf("%s", fc.translateAssign(lhs, fc.newIdent(tmpVars[i], fc.typeOf(lhs)), s.Tok == token.DEFINE)) } } @@ -431,7 +432,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { if len(rhs) == 0 { rhs = make([]ast.Expr, len(lhs)) for i, e := range lhs { - rhs[i] = fc.zeroValue(fc.pkgCtx.TypeOf(e)) + rhs[i] = fc.zeroValue(fc.typeOf(e)) } } fc.translateStmt(&ast.AssignStmt{ @@ -443,8 +444,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { case token.TYPE: for _, spec := range decl.Specs { o := fc.pkgCtx.Defs[spec.(*ast.TypeSpec).Name].(*types.TypeName) - fc.pkgCtx.typeNames = append(fc.pkgCtx.typeNames, o) - fc.pkgCtx.objectNames[o] = fc.newVariableWithLevel(o.Name(), true) + fc.pkgCtx.typeNames.Add(o) fc.pkgCtx.dependencies[o] = true } case token.CONST: @@ -469,7 +469,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { fc.Printf("$go(%s, %s);", callable, arglist) case *ast.SendStmt: - chanType := fc.pkgCtx.TypeOf(s.Chan).Underlying().(*types.Chan) + chanType := fc.typeOf(s.Chan).Underlying().(*types.Chan) call := &ast.CallExpr{ Fun: fc.newIdent("$send", types.NewSignatureType(nil, nil, nil, types.NewTuple(types.NewVar(0, nil, "", chanType), types.NewVar(0, nil, "", chanType.Elem())), nil, false)), Args: []ast.Expr{s.Chan, fc.newIdent(fc.translateImplicitConversionWithCloning(s.Value, chanType.Elem()).String(), chanType.Elem())}, @@ -494,7 +494,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { case *ast.AssignStmt: channels = append(channels, fc.formatExpr("[%e]", astutil.RemoveParens(comm.Rhs[0]).(*ast.UnaryExpr).X).String()) case *ast.SendStmt: - chanType := fc.pkgCtx.TypeOf(comm.Chan).Underlying().(*types.Chan) + chanType := fc.typeOf(comm.Chan).Underlying().(*types.Chan) channels = append(channels, fc.formatExpr("[%e, %s]", comm.Chan, fc.translateImplicitConversionWithCloning(comm.Value, chanType.Elem())).String()) default: panic(fmt.Sprintf("unhandled: %T", comm)) @@ -505,7 +505,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { var bodyPrefix []ast.Stmt if assign, ok := clause.Comm.(*ast.AssignStmt); ok { - switch rhsType := fc.pkgCtx.TypeOf(assign.Rhs[0]).(type) { + switch rhsType := fc.typeOf(assign.Rhs[0]).(type) { case *types.Tuple: bodyPrefix = []ast.Stmt{&ast.AssignStmt{Lhs: assign.Lhs, Rhs: []ast.Expr{fc.newIdent(selectionVar+"[1]", rhsType)}, Tok: assign.Tok}} default: @@ -700,8 +700,8 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { } if l, ok := lhs.(*ast.IndexExpr); ok { - if t, ok := fc.pkgCtx.TypeOf(l.X).Underlying().(*types.Map); ok { - if typesutil.IsJsObject(fc.pkgCtx.TypeOf(l.Index)) { + if t, ok := fc.typeOf(l.X).Underlying().(*types.Map); ok { + if typesutil.IsJsObject(fc.typeOf(l.Index)) { fc.pkgCtx.errList = append(fc.pkgCtx.errList, types.Error{Fset: fc.pkgCtx.fileSet, Pos: l.Index.Pos(), Msg: "cannot use js.Object as map key"}) } keyVar := fc.newVariable("_key") @@ -718,7 +718,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { } } - lhsType := fc.pkgCtx.TypeOf(lhs) + lhsType := fc.typeOf(lhs) rhsExpr := fc.translateConversion(rhs, lhsType) if _, ok := rhs.(*ast.CompositeLit); ok && define { return fmt.Sprintf("%s = %s;", fc.translateExpr(lhs), rhsExpr) // skip $copy @@ -742,7 +742,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { case *ast.Ident: return fmt.Sprintf("%s = %s;", fc.objectName(fc.pkgCtx.ObjectOf(l)), rhsExpr) case *ast.SelectorExpr: - sel, ok := fc.pkgCtx.SelectionOf(l) + sel, ok := fc.selectionOf(l) if !ok { // qualified identifier return fmt.Sprintf("%s = %s;", fc.objectName(fc.pkgCtx.Uses[l.Sel]), rhsExpr) @@ -755,7 +755,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { case *ast.StarExpr: return fmt.Sprintf("%s.$set(%s);", fc.translateExpr(l.X), rhsExpr) case *ast.IndexExpr: - switch t := fc.pkgCtx.TypeOf(l.X).Underlying().(type) { + switch t := fc.typeOf(l.X).Underlying().(type) { case *types.Array, *types.Pointer: pattern := rangeCheck("%1e[%2f] = %3s", fc.pkgCtx.Types[l.Index].Value != nil, true) if _, ok := t.(*types.Pointer); ok { // check pointer for nil (attribute getter causes a panic) @@ -773,7 +773,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { } func (fc *funcContext) translateResults(results []ast.Expr) string { - tuple := fc.sig.Results() + tuple := fc.typeResolver.Substitute(fc.sig.Results()).(*types.Tuple) switch tuple.Len() { case 0: return "" @@ -787,7 +787,7 @@ func (fc *funcContext) translateResults(results []ast.Expr) string { return " " + v.String() default: if len(results) == 1 { - resultTuple := fc.pkgCtx.TypeOf(results[0]).(*types.Tuple) + resultTuple := fc.typeOf(results[0]).(*types.Tuple) if resultTuple.Len() != tuple.Len() { panic("invalid tuple return assignment") diff --git a/compiler/typesutil/map.go b/compiler/typesutil/map.go new file mode 100644 index 000000000..146f09765 --- /dev/null +++ b/compiler/typesutil/map.go @@ -0,0 +1,34 @@ +package typesutil + +import ( + "go/types" + + "golang.org/x/tools/go/types/typeutil" +) + +// Map is a type-safe wrapper around golang.org/x/tools/go/types/typeutil.Map. +type Map[Val any] struct{ impl typeutil.Map } + +func (m *Map[Val]) At(key types.Type) Val { + val := m.impl.At(key) + if val != nil { + return val.(Val) + } + var zero Val + return zero +} + +func (m *Map[Val]) Set(key types.Type, value Val) (prev Val) { + old := m.impl.Set(key, value) + if old != nil { + return old.(Val) + } + var zero Val + return zero +} + +func (m *Map[Val]) Delete(key types.Type) bool { return m.impl.Delete(key) } + +func (m *Map[Val]) Len() int { return m.impl.Len() } + +func (m *Map[Val]) String() string { return m.impl.String() } diff --git a/compiler/typesutil/typelist.go b/compiler/typesutil/typelist.go new file mode 100644 index 000000000..04d0d6869 --- /dev/null +++ b/compiler/typesutil/typelist.go @@ -0,0 +1,20 @@ +package typesutil + +import ( + "go/types" + "strings" +) + +// TypeList an ordered list of types. +type TypeList []types.Type + +func (tl TypeList) String() string { + buf := strings.Builder{} + for i, typ := range tl { + if i != 0 { + buf.WriteString(", ") + } + buf.WriteString(types.TypeString(typ, nil)) + } + return buf.String() +} diff --git a/compiler/typesutil/typenames.go b/compiler/typesutil/typenames.go new file mode 100644 index 000000000..2f5ac6186 --- /dev/null +++ b/compiler/typesutil/typenames.go @@ -0,0 +1,30 @@ +package typesutil + +import "go/types" + +// TypeNames implements an ordered set of *types.TypeName pointers. +// +// The set is ordered to ensure deterministic behavior across compiler runs. +type TypeNames struct { + known map[*types.TypeName]struct{} + order []*types.TypeName +} + +// Add a type name to the set. If the type name has been previously added, +// this operation is a no-op. Two type names are considered equal iff they have +// the same memory address. +func (tn *TypeNames) Add(name *types.TypeName) { + if _, ok := tn.known[name]; ok { + return + } + if tn.known == nil { + tn.known = map[*types.TypeName]struct{}{} + } + tn.order = append(tn.order, name) + tn.known[name] = struct{}{} +} + +// Slice returns set elements in the order they were first added to the set. +func (tn *TypeNames) Slice() []*types.TypeName { + return tn.order +} diff --git a/compiler/typesutil/typenames_test.go b/compiler/typesutil/typenames_test.go new file mode 100644 index 000000000..332a5973f --- /dev/null +++ b/compiler/typesutil/typenames_test.go @@ -0,0 +1,46 @@ +package typesutil + +import ( + "go/token" + "go/types" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/gopherjs/gopherjs/internal/srctesting" +) + +func typeNameOpts() cmp.Options { + return cmp.Options{ + cmp.Transformer("TypeName", func(name *types.TypeName) string { + return types.ObjectString(name, nil) + }), + } +} + +func TestTypeNames(t *testing.T) { + src := `package test + + type A int + type B int + type C int + ` + fset := token.NewFileSet() + _, pkg := srctesting.Check(t, fset, srctesting.Parse(t, fset, src)) + A := srctesting.LookupObj(pkg, "A").(*types.TypeName) + B := srctesting.LookupObj(pkg, "B").(*types.TypeName) + C := srctesting.LookupObj(pkg, "C").(*types.TypeName) + + tn := TypeNames{} + tn.Add(A) + tn.Add(B) + tn.Add(A) + tn.Add(C) + tn.Add(B) + + got := tn.Slice() + want := []*types.TypeName{A, B, C} + + if diff := cmp.Diff(want, got, typeNameOpts()); diff != "" { + t.Errorf("tn.Slice() returned diff (-want,+got):\n%s", diff) + } +} diff --git a/compiler/typesutil/typesutil.go b/compiler/typesutil/typesutil.go index 600925b81..1434c23c5 100644 --- a/compiler/typesutil/typesutil.go +++ b/compiler/typesutil/typesutil.go @@ -1,6 +1,9 @@ package typesutil -import "go/types" +import ( + "fmt" + "go/types" +) func IsJsPackage(pkg *types.Package) bool { return pkg != nil && pkg.Path() == "github.com/gopherjs/gopherjs/js" @@ -14,3 +17,93 @@ func IsJsObject(t types.Type) bool { named, isNamed := ptr.Elem().(*types.Named) return isNamed && IsJsPackage(named.Obj().Pkg()) && named.Obj().Name() == "Object" } + +// RecvType returns a named type of a method receiver, or nil if it's not a method. +// +// For methods on a pointer receiver, the underlying named type is returned. +func RecvType(sig *types.Signature) *types.Named { + recv := sig.Recv() + if recv == nil { + return nil + } + + typ := recv.Type() + if ptrType, ok := typ.(*types.Pointer); ok { + typ = ptrType.Elem() + } + + return typ.(*types.Named) +} + +// RecvAsFirstArg takes a method signature and returns a function +// signature with receiver as the first parameter. +func RecvAsFirstArg(sig *types.Signature) *types.Signature { + params := make([]*types.Var, 0, 1+sig.Params().Len()) + params = append(params, sig.Recv()) + for i := 0; i < sig.Params().Len(); i++ { + params = append(params, sig.Params().At(i)) + } + return types.NewSignatureType(nil, nil, nil, types.NewTuple(params...), sig.Results(), sig.Variadic()) +} + +// Selection is a common interface for go/types.Selection and our custom-constructed +// method and field selections. +type Selection interface { + Kind() types.SelectionKind + Recv() types.Type + Index() []int + Obj() types.Object + Type() types.Type +} + +// NewSelection creates a new selection. +func NewSelection(kind types.SelectionKind, recv types.Type, index []int, obj types.Object, typ types.Type) Selection { + return &selectionImpl{ + kind: kind, + recv: recv, + index: index, + obj: obj, + typ: typ, + } +} + +type selectionImpl struct { + kind types.SelectionKind + recv types.Type + index []int + obj types.Object + typ types.Type +} + +func (sel *selectionImpl) Kind() types.SelectionKind { return sel.kind } +func (sel *selectionImpl) Recv() types.Type { return sel.recv } +func (sel *selectionImpl) Index() []int { return sel.index } +func (sel *selectionImpl) Obj() types.Object { return sel.obj } +func (sel *selectionImpl) Type() types.Type { return sel.typ } + +func fieldsOf(s *types.Struct) []*types.Var { + fields := make([]*types.Var, s.NumFields()) + for i := 0; i < s.NumFields(); i++ { + fields[i] = s.Field(i) + } + return fields +} + +// OffsetOf returns byte offset of a struct field specified by the provided +// selection. +// +// Adapted from go/types.Config.offsetof(). +func OffsetOf(sizes types.Sizes, sel Selection) int64 { + if sel.Kind() != types.FieldVal { + panic(fmt.Errorf("byte offsets are only defined for struct fields")) + } + typ := sel.Recv() + var o int64 + for _, idx := range sel.Index() { + s := typ.Underlying().(*types.Struct) + o += sizes.Offsetsof(fieldsOf(s))[idx] + typ = s.Field(idx).Type() + } + + return o +} diff --git a/compiler/utils.go b/compiler/utils.go index 058437f67..46794fe64 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -19,9 +19,18 @@ import ( "unicode" "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) +// root returns the topmost function context corresponding to the package scope. +func (fc *funcContext) root() *funcContext { + if fc.parent == nil { + return fc + } + return fc.parent.root() +} + func (fc *funcContext) Write(b []byte) (int, error) { fc.writePos() fc.output = append(fc.output, b...) @@ -101,7 +110,7 @@ func (fc *funcContext) expandTupleArgs(argExprs []ast.Expr) []ast.Expr { return argExprs } - tuple, isTuple := fc.pkgCtx.TypeOf(argExprs[0]).(*types.Tuple) + tuple, isTuple := fc.typeOf(argExprs[0]).(*types.Tuple) if !isTuple { return argExprs } @@ -158,7 +167,7 @@ func (fc *funcContext) translateArgs(sig *types.Signature, argExprs []ast.Expr, return args } -func (fc *funcContext) translateSelection(sel selection, pos token.Pos) ([]string, string) { +func (fc *funcContext) translateSelection(sel typesutil.Selection, pos token.Pos) ([]string, string) { var fields []string t := sel.Recv() for _, index := range sel.Index() { @@ -283,7 +292,7 @@ func (fc *funcContext) newIdent(name string, t types.Type) *ast.Ident { fc.setType(ident, t) obj := types.NewVar(0, fc.pkgCtx.Pkg, name, t) fc.pkgCtx.Uses[ident] = obj - fc.pkgCtx.objectNames[obj] = name + fc.objectNames[obj] = name return ident } @@ -319,9 +328,30 @@ func isVarOrConst(o types.Object) bool { } func isPkgLevel(o types.Object) bool { - return o.Parent() != nil && o.Parent().Parent() == types.Universe + // Note: named types are always assigned a variable at package level to be + // initialized with the rest of the package types, even the types declared + // in a statement inside a function. + _, isType := o.(*types.TypeName) + return (o.Parent() != nil && o.Parent().Parent() == types.Universe) || isType } +// assignedObjectName checks if the object has been previously assigned a name +// in this or one of the parent contexts. If not, found will be false. +func (fc *funcContext) assignedObjectName(o types.Object) (name string, found bool) { + if fc == nil { + return "", false + } + if name, found := fc.parent.assignedObjectName(o); found { + return name, true + } + + name, found = fc.objectNames[o] + return name, found +} + +// objectName returns a JS expression that refers to the given object. If the +// object hasn't been previously assigned a JS variable name, it will be +// allocated as needed. func (fc *funcContext) objectName(o types.Object) string { if isPkgLevel(o) { fc.pkgCtx.dependencies[o] = true @@ -331,10 +361,15 @@ func (fc *funcContext) objectName(o types.Object) string { } } - name, ok := fc.pkgCtx.objectNames[o] + name, ok := fc.assignedObjectName(o) if !ok { - name = fc.newVariableWithLevel(o.Name(), isPkgLevel(o)) - fc.pkgCtx.objectNames[o] = name + pkgLevel := isPkgLevel(o) + name = fc.newVariableWithLevel(o.Name(), pkgLevel) + if pkgLevel { + fc.root().objectNames[o] = name + } else { + fc.objectNames[o] = name + } } if v, ok := o.(*types.Var); ok && fc.pkgCtx.escapingVars[v] { @@ -343,6 +378,17 @@ func (fc *funcContext) objectName(o types.Object) string { return name } +// instName returns a JS expression that refers to the provided instance of a +// function or type. Non-generic objects may be represented as an instance with +// zero type arguments. +func (fc *funcContext) instName(inst typeparams.Instance) string { + objName := fc.objectName(inst.Object) + if inst.IsTrivial() { + return objName + } + return fmt.Sprintf("%s[%d /* %v */]", objName, fc.pkgCtx.instanceSet.ID(inst), inst.TArgs) +} + func (fc *funcContext) varPtrName(o *types.Var) string { if isPkgLevel(o) && o.Exported() { return fc.pkgVar(o.Pkg()) + "." + o.Name() + "$ptr" @@ -364,7 +410,11 @@ func (fc *funcContext) typeName(ty types.Type) string { if t.Obj().Name() == "error" { return "$error" } - return fc.objectName(t.Obj()) + inst := typeparams.Instance{Object: t.Obj()} + for i := 0; i < t.TypeArgs().Len(); i++ { + inst.TArgs = append(inst.TArgs, t.TypeArgs().At(i)) + } + return fc.instName(inst) case *types.Interface: if t.Empty() { return "$emptyInterface" @@ -383,6 +433,42 @@ func (fc *funcContext) typeName(ty types.Type) string { return anonType.Name() } +// instanceOf constructs an instance description of the object the ident is +// referring to. For non-generic objects, it will return a trivial instance with +// no type arguments. +func (fc *funcContext) instanceOf(ident *ast.Ident) typeparams.Instance { + inst := typeparams.Instance{Object: fc.pkgCtx.ObjectOf(ident)} + if i, ok := fc.pkgCtx.Instances[ident]; ok { + inst.TArgs = fc.typeResolver.SubstituteAll(i.TypeArgs) + } + return inst +} + +// typeOf returns a type associated with the given AST expression. For types +// defined in terms of type parameters, it will substitute type parameters with +// concrete types from the current set of type arguments. +func (fc *funcContext) typeOf(expr ast.Expr) types.Type { + typ := fc.pkgCtx.TypeOf(expr) + // If the expression is referring to an instance of a generic type or function, + // we want the instantiated type. + if ident, ok := expr.(*ast.Ident); ok { + if inst, ok := fc.pkgCtx.Instances[ident]; ok { + typ = inst.Type + } + } + return fc.typeResolver.Substitute(typ) +} + +func (fc *funcContext) selectionOf(e *ast.SelectorExpr) (typesutil.Selection, bool) { + if sel, ok := fc.pkgCtx.Selections[e]; ok { + return fc.typeResolver.SubstituteSelection(sel), true + } + if sel, ok := fc.pkgCtx.additionalSelections[e]; ok { + return sel, true + } + return nil, false +} + func (fc *funcContext) externalize(s string, t types.Type) string { if typesutil.IsJsObject(t) { return s diff --git a/go.mod b/go.mod index 8edafd89b..ccb130f48 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/evanw/esbuild v0.18.0 github.com/fsnotify/fsnotify v1.5.1 - github.com/google/go-cmp v0.5.7 + github.com/google/go-cmp v0.5.8 github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 @@ -13,13 +13,11 @@ require ( github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 github.com/visualfc/goembed v0.3.3 - golang.org/x/sync v0.3.0 + golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a + golang.org/x/sync v0.5.0 golang.org/x/sys v0.10.0 golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 - golang.org/x/tools v0.11.0 + golang.org/x/tools v0.16.0 ) -require ( - github.com/inconshreveable/mousetrap v1.0.0 // indirect - golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect -) +require github.com/inconshreveable/mousetrap v1.0.0 // indirect diff --git a/go.sum b/go.sum index 349d599ba..8e69980d0 100644 --- a/go.sum +++ b/go.sum @@ -120,8 +120,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -270,6 +270,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= +golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -295,7 +297,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -355,8 +357,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -467,14 +469,12 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= -golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= diff --git a/internal/srctesting/srctesting.go b/internal/srctesting/srctesting.go index 4e374845e..37ee248e9 100644 --- a/internal/srctesting/srctesting.go +++ b/internal/srctesting/srctesting.go @@ -9,6 +9,7 @@ import ( "go/parser" "go/token" "go/types" + "strings" "testing" ) @@ -37,6 +38,7 @@ func Check(t *testing.T, fset *token.FileSet, files ...*ast.File) (*types.Info, Implicits: make(map[ast.Node]types.Object), Selections: make(map[*ast.SelectorExpr]*types.Selection), Scopes: make(map[ast.Node]*types.Scope), + Instances: make(map[*ast.Ident]types.Instance), } config := &types.Config{ Sizes: &types.StdSizes{WordSize: 4, MaxAlign: 8}, @@ -107,3 +109,30 @@ func Format(t *testing.T, fset *token.FileSet, node any) string { } return buf.String() } + +// LookupObj returns a top-level object with the given name. +// +// Methods can be referred to as RecvTypeName.MethodName. +func LookupObj(pkg *types.Package, name string) types.Object { + path := strings.Split(name, ".") + scope := pkg.Scope() + var obj types.Object + + for len(path) > 0 { + obj = scope.Lookup(path[0]) + path = path[1:] + + if fun, ok := obj.(*types.Func); ok { + scope = fun.Scope() + continue + } + + // If we are here, the latest object is a named type. If there are more path + // elements left, they must refer to field or method. + if len(path) > 0 { + obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), path[0]) + path = path[1:] + } + } + return obj +} diff --git a/internal/testingx/must.go b/internal/testingx/must.go new file mode 100644 index 000000000..62d27dce8 --- /dev/null +++ b/internal/testingx/must.go @@ -0,0 +1,24 @@ +// Package testingx provides helpers for use with the testing package. +package testingx + +import "testing" + +// Must provides a concise way to handle returned error in cases that +// "should never happen"©. +// +// This function can be used in test case setup that can be presumed to be +// correct, but technically may return an error. This function MUST NOT be used +// to check for test case conditions themselves because it generates a generic, +// nondescript test error message. +// +// func startServer(addr string) (*server, err) +// mustServer := testingx.Must[*server](t) +// mustServer(startServer(":8080")) +func Must[T any](t *testing.T) func(v T, err error) T { + return func(v T, err error) T { + if err != nil { + t.Fatalf("Got: unexpected error: %s. Want: no error.", err) + } + return v + } +} diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index d58968ada..1259a42c3 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -109,9 +109,6 @@ var knownFails = map[string]failReason{ "fixedbugs/issue23188.go": {desc: "incorrect order of evaluation of index operations"}, "fixedbugs/issue24547.go": {desc: "incorrect computing method sets with shadowed methods"}, - // These are new tests in Go 1.11.5 - "fixedbugs/issue28688.go": {category: notApplicable, desc: "testing runtime optimisations"}, - // These are new tests in Go 1.12. "fixedbugs/issue23837.go": {desc: "missing panic on nil pointer-to-empty-struct dereference"}, "fixedbugs/issue27201.go": {desc: "incorrect stack trace for nil dereference in inlined function"}, @@ -121,7 +118,6 @@ var knownFails = map[string]failReason{ // These are new tests in Go 1.12.9. "fixedbugs/issue30977.go": {category: neverTerminates, desc: "does for { runtime.GC() }"}, "fixedbugs/issue32477.go": {category: notApplicable, desc: "uses runtime.SetFinalizer and runtime.GC"}, - "fixedbugs/issue32680.go": {category: notApplicable, desc: "uses -gcflags=-d=ssa/check/on flag"}, // These are new tests in Go 1.13-1.16. "fixedbugs/issue19113.go": {category: lowLevelRuntimeDifference, desc: "JavaScript bit shifts by negative amount don't cause an exception"}, @@ -134,7 +130,6 @@ var knownFails = map[string]failReason{ "fixedbugs/issue30116u.go": {desc: "GopherJS doesn't specify the array/slice index selector in the out-of-bounds message"}, "fixedbugs/issue34395.go": {category: neverTerminates, desc: "https://github.com/gopherjs/gopherjs/issues/1007"}, "fixedbugs/issue35027.go": {category: usesUnsupportedPackage, desc: "uses unsupported conversion to reflect.SliceHeader and -gcflags=-d=checkptr"}, - "fixedbugs/issue35073.go": {category: usesUnsupportedPackage, desc: "uses unsupported flag -gcflags=-d=checkptr"}, "fixedbugs/issue35576.go": {category: lowLevelRuntimeDifference, desc: "GopherJS print/println format for floats differs from Go's"}, "fixedbugs/issue40917.go": {category: notApplicable, desc: "uses pointer arithmetic and unsupported flag -gcflags=-d=checkptr"}, @@ -149,17 +144,22 @@ var knownFails = map[string]failReason{ "fixedbugs/issue50854.go": {category: lowLevelRuntimeDifference, desc: "negative int32 overflow behaves differently in JS"}, // These are new tests in Go 1.18 - "fixedbugs/issue46938.go": {category: notApplicable, desc: "tests -d=checkptr compiler mode, which GopherJS doesn't support"}, - "fixedbugs/issue47928.go": {category: notApplicable, desc: "//go:nointerface is a part of GOEXPERIMENT=fieldtrack and is not supported by GopherJS"}, - "fixedbugs/issue48898.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1128"}, - "fixedbugs/issue48536.go": {category: usesUnsupportedPackage, desc: "https://github.com/gopherjs/gopherjs/issues/1130"}, - "fixedbugs/issue53600.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, + "fixedbugs/issue47928.go": {category: notApplicable, desc: "//go:nointerface is a part of GOEXPERIMENT=fieldtrack and is not supported by GopherJS"}, + "fixedbugs/issue48536.go": {category: usesUnsupportedPackage, desc: "https://github.com/gopherjs/gopherjs/issues/1130"}, + "fixedbugs/issue48898.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1128"}, + "fixedbugs/issue53600.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, + "typeparam/chans.go": {category: neverTerminates, desc: "uses runtime.SetFinalizer() and runtime.GC()."}, + "typeparam/issue51733.go": {category: usesUnsupportedPackage, desc: "unsafe: uintptr to struct pointer conversion is unsupported"}, + "typeparam/typeswitch5.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, + + // Failures related to the lack of generics support. Ideally, this section + // should be emptied once https://github.com/gopherjs/gopherjs/issues/1013 is + // fixed. + "typeparam/nested.go": {category: usesUnsupportedGenerics, desc: "incomplete support for generic types inside generic functions"}, // These are new tests in Go 1.19 - "fixedbugs/issue50672.go": {category: usesUnsupportedGenerics, desc: "Checking function nesting with one function having a type parameter."}, - "fixedbugs/issue53137.go": {category: usesUnsupportedGenerics, desc: "Checking setting type parameter of struct in parameter of a generic function."}, - "fixedbugs/issue53309.go": {category: usesUnsupportedGenerics, desc: "Checking unused type parameter in method call to interface"}, - "fixedbugs/issue53635.go": {category: usesUnsupportedGenerics, desc: "Checking switch type against nil type with unsupported type parameters"}, + "typeparam/issue51521.go": {category: lowLevelRuntimeDifference, desc: "different panic message when calling a method on nil interface"}, + "fixedbugs/issue50672.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1271"}, "fixedbugs/issue53653.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format of int64 is different from Go's"}, } @@ -202,7 +202,7 @@ var ( // dirs are the directories to look for *.go files in. // TODO(bradfitz): just use all directories? - dirs = []string{".", "ken", "chan", "interface", "syntax", "dwarf", "fixedbugs"} + dirs = []string{".", "ken", "chan", "interface", "syntax", "dwarf", "fixedbugs", "typeparam"} // ratec controls the max number of tests running at a time. ratec chan bool @@ -640,16 +640,20 @@ func (t *test) run() { var args, flags []string wantError := false - f := strings.Fields(action) + f, err := splitQuoted(action) + if err != nil { + t.err = fmt.Errorf("invalid test recipe: %v", err) + return + } if len(f) > 0 { action = f[0] args = f[1:] } - // GOPHERJS: For now, only run with "run", "cmpout" actions, in "fixedbugs" dir. Skip all others. + // GOPHERJS: For now, only run with "run", "cmpout" actions, in "fixedbugs" and "typeparam" dirs. Skip all others. switch action { case "run", "cmpout": - if filepath.Clean(t.dir) != "fixedbugs" { + if d := filepath.Clean(t.dir); d != "fixedbugs" && d != "typeparam" { action = "skip" } default: @@ -699,6 +703,19 @@ func (t *test) run() { os.Setenv("GOARCH", goarch) } + { + // GopherJS: we don't support any of -gcflags, but for the most part they + // are not too relevant to the outcome of the test. + supportedArgs := []string{} + for _, a := range args { + if strings.HasPrefix(a, "-gcflags") { + continue + } + supportedArgs = append(supportedArgs, a) + } + args = supportedArgs + } + useTmp := true runcmd := func(args ...string) ([]byte, error) { cmd := exec.Command(args[0], args[1:]...) @@ -1263,3 +1280,65 @@ func getenv(key, def string) string { } return def } + +// splitQuoted splits the string s around each instance of one or more consecutive +// white space characters while taking into account quotes and escaping, and +// returns an array of substrings of s or an empty list if s contains only white space. +// Single quotes and double quotes are recognized to prevent splitting within the +// quoted region, and are removed from the resulting substrings. If a quote in s +// isn't closed err will be set and r will have the unclosed argument as the +// last element. The backslash is used for escaping. +// +// For example, the following string: +// +// a b:"c d" 'e''f' "g\"" +// +// Would be parsed as: +// +// []string{"a", "b:c d", "ef", `g"`} +// +// [copied from src/go/build/build.go] +func splitQuoted(s string) (r []string, err error) { + var args []string + arg := make([]rune, len(s)) + escaped := false + quoted := false + quote := '\x00' + i := 0 + for _, rune := range s { + switch { + case escaped: + escaped = false + case rune == '\\': + escaped = true + continue + case quote != '\x00': + if rune == quote { + quote = '\x00' + continue + } + case rune == '"' || rune == '\'': + quoted = true + quote = rune + continue + case unicode.IsSpace(rune): + if quoted || i > 0 { + quoted = false + args = append(args, string(arg[:i])) + i = 0 + } + continue + } + arg[i] = rune + i++ + } + if quoted || i > 0 { + args = append(args, string(arg[:i])) + } + if quote != 0 { + err = errors.New("unclosed quote") + } else if escaped { + err = errors.New("unfinished escaping") + } + return args, err +}