diff --git a/compiler/analysis/info.go b/compiler/analysis/info.go index c984b726f..aa302edb4 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 { @@ -92,10 +93,23 @@ func (info *Info) newFuncInfo(n ast.Node) *FuncInfo { return funcInfo } +// IsBlocking returns true if the function may contain blocking calls or operations. func (info *Info) IsBlocking(fun *types.Func) bool { return len(info.FuncDeclInfos[fun].Blocking) > 0 } +// VarsWithInitializers returns a set of package-level variables that have +// explicit initializers. +func (info *Info) VarsWithInitializers() map[*types.Var]bool { + result := map[*types.Var]bool{} + for _, init := range info.InitOrder { + for _, o := range init.Lhs { + result[o] = true + } + } + return result +} + func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(*types.Func) bool) *Info { info := &Info{ Info: typesInfo, @@ -354,6 +368,8 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { func (fi *FuncInfo) callToNamedFunc(callee types.Object) { switch o := callee.(type) { case *types.Func: + // For generic methods we want the generic version of the function. + 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 30febe1cb..4f1be3fbb 100644 --- a/compiler/astutil/astutil.go +++ b/compiler/astutil/astutil.go @@ -3,11 +3,13 @@ package astutil import ( "fmt" "go/ast" + "go/constant" "go/token" "go/types" "strings" ) +// RemoveParens removed parens around an expression, if any. func RemoveParens(e ast.Expr) ast.Expr { for { p, isParen := e.(*ast.ParenExpr) @@ -18,19 +20,34 @@ func RemoveParens(e ast.Expr) ast.Expr { } } +// SetType of the expression e to type t. func SetType(info *types.Info, t types.Type, e ast.Expr) ast.Expr { info.Types[e] = types.TypeAndValue{Type: t} return e } -func NewIdent(name string, t types.Type, info *types.Info, pkg *types.Package) *ast.Ident { - ident := ast.NewIdent(name) - info.Types[ident] = types.TypeAndValue{Type: t} - obj := types.NewVar(0, pkg, name, t) +// SetTypeAndValue of the expression e to type t. +func SetTypeAndValue(info *types.Info, t types.Type, val constant.Value, e ast.Expr) ast.Expr { + info.Types[e] = types.TypeAndValue{Type: t, Value: val} + return e +} + +// NewVarIdent creates a new variable object with the given name and type. +func NewVarIdent(name string, t types.Type, info *types.Info, pkg *types.Package) *ast.Ident { + obj := types.NewVar(token.NoPos, pkg, name, t) + return NewIdentFor(info, obj) +} + +// NewIdentFor creates a new identifier referencing the given object. +func NewIdentFor(info *types.Info, obj types.Object) *ast.Ident { + ident := ast.NewIdent(obj.Name()) + ident.NamePos = obj.Pos() info.Uses[ident] = obj + SetType(info, obj.Type(), ident) return ident } +// IsTypeExpr returns true if expr denotes a type. func IsTypeExpr(expr ast.Expr, info *types.Info) bool { switch e := expr.(type) { case *ast.ArrayType, *ast.ChanType, *ast.FuncType, *ast.InterfaceType, *ast.MapType, *ast.StructType: @@ -43,6 +60,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: @@ -50,6 +81,7 @@ func IsTypeExpr(expr ast.Expr, info *types.Info) bool { } } +// ImportsUnsafe returns true of the source imports package "unsafe". func ImportsUnsafe(file *ast.File) bool { for _, imp := range file.Imports { if imp.Path.Value == `"unsafe"` { @@ -65,10 +97,17 @@ func FuncKey(d *ast.FuncDecl) string { if d.Recv == nil || len(d.Recv.List) == 0 { return d.Name.Name } + // Each if-statement progressively unwraps receiver type expression. recv := d.Recv.List[0].Type if star, ok := recv.(*ast.StarExpr); ok { recv = star.X } + if index, ok := recv.(*ast.IndexExpr); ok { + recv = index.X + } + if index, ok := recv.(*ast.IndexListExpr); ok { + recv = index.X + } return recv.(*ast.Ident).Name + "." + d.Name.Name } @@ -167,3 +206,93 @@ func EndsWithReturn(stmts []ast.Stmt) bool { return false } } + +// TypeCast wraps expression e into an AST of type conversion to a type denoted +// by typeExpr. The new AST node is associated with the appropriate type. +func TypeCast(info *types.Info, e ast.Expr, typeExpr ast.Expr) *ast.CallExpr { + cast := &ast.CallExpr{ + Fun: typeExpr, + Lparen: e.Pos(), + Args: []ast.Expr{e}, + Rparen: e.End(), + } + SetType(info, info.TypeOf(typeExpr), cast) + return cast +} + +// TakeAddress wraps expression e into an AST of address-taking operator &e. The +// new AST node is associated with pointer to the type of e. +func TakeAddress(info *types.Info, e ast.Expr) *ast.UnaryExpr { + exprType := info.TypeOf(e) + ptrType := types.NewPointer(exprType) + addrOf := &ast.UnaryExpr{ + OpPos: e.Pos(), + Op: token.AND, + X: e, + } + SetType(info, ptrType, addrOf) + return addrOf +} + +// MakeTypedConstant takes an untyped constant value and makes an AST expression +// representing it with a concrete type that can represent the constant precisely. +func MakeTypedConstant(info *types.Info, val constant.Value) ast.Expr { + switch val.Kind() { + case constant.String: + e := &ast.BasicLit{ + Kind: token.STRING, + Value: val.ExactString(), + } + + return SetTypeAndValue(info, types.Typ[types.String], val, e) + case constant.Float: + e := &ast.BasicLit{ + Kind: token.FLOAT, + Value: val.ExactString(), + } + + return SetTypeAndValue(info, types.Typ[types.Float64], val, e) + case constant.Int: + bits := constant.BitLen(val) + sign := constant.Sign(val) + + var t types.Type + if bits <= 32 && sign >= 0 { + t = types.Typ[types.Uint32] + } else if bits <= 32 && sign < 0 { + t = types.Typ[types.Int32] + } else if sign >= 0 { + t = types.Typ[types.Uint64] + } else { + t = types.Typ[types.Int64] + } + + e := &ast.BasicLit{ + Kind: token.INT, + Value: val.ExactString(), + } + return SetTypeAndValue(info, t, val, e) + case constant.Complex: + e := &ast.BasicLit{ + Kind: token.IMAG, + // Cheat: don't bother with generating a plausible complex expression. + // We would have to construct a complicated expression to construct a + // complex value. However, when dealing with constants, GopherJS doesn't + // actually inspect the AST, only the types.TypeAndValue object it gets + // from type analyzer. + // + // All we really need here is an ast.Expr we can associate with a + // types.TypeAndValue instance, so we just do a token effort to return + // something. + Value: "", + } + return SetTypeAndValue(info, types.Typ[types.Complex128], val, e) + case constant.Bool: + e := &ast.Ident{ + Name: val.ExactString(), + } + return SetTypeAndValue(info, types.Typ[types.Bool], val, e) + default: + panic(fmt.Errorf("unexpected constant kind %s: %v", val.Kind(), val)) + } +} diff --git a/compiler/astutil/astutil_test.go b/compiler/astutil/astutil_test.go index a996ae73f..30e9ea09c 100644 --- a/compiler/astutil/astutil_test.go +++ b/compiler/astutil/astutil_test.go @@ -1,7 +1,10 @@ package astutil import ( + "go/ast" + "go/constant" "go/token" + "go/types" "testing" "github.com/gopherjs/gopherjs/internal/srctesting" @@ -181,3 +184,41 @@ func TestEndsWithReturn(t *testing.T) { }) } } + +func TestMakeTypedConstant(t *testing.T) { + tests := []struct { + value constant.Value + want types.Type + }{{ + value: constant.MakeString("abc"), + want: types.Typ[types.String], + }, { + value: constant.MakeInt64(0xFFFFFFFF), + want: types.Typ[types.Uint32], + }, { + value: constant.MakeInt64(-0x80000000), + want: types.Typ[types.Int32], + }, { + value: constant.MakeUint64(0xFFFFFFFFFFFFFFFF), + want: types.Typ[types.Uint64], + }, { + value: constant.MakeInt64(-0x8000000000000000), + want: types.Typ[types.Int64], + }} + + for _, test := range tests { + t.Run(test.value.ExactString(), func(t *testing.T) { + info := &types.Info{Types: map[ast.Expr]types.TypeAndValue{}} + e := MakeTypedConstant(info, test.value) + tv := info.Types[e] + + if tv.Type != test.want { + t.Errorf("Got: constant %s assigned type %s. Want: %s", test.value, tv.Type, test.want) + } + + if tv.Value != test.value { + t.Errorf("Got: associated constant value is %s. Want: %s (the same as original).", tv.Value, test.value) + } + }) + } +} diff --git a/compiler/compiler.go b/compiler/compiler.go index 0588a923c..c9bb77422 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -32,22 +32,6 @@ func init() { } } -type ErrorList []error - -func (err ErrorList) Error() string { - if len(err) == 0 { - return "" - } - return fmt.Sprintf("%s (and %d more errors)", err[0].Error(), len(err[1:])) -} - -func (err ErrorList) Normalize() error { - if len(err) == 0 { - return nil - } - return err -} - // Archive contains intermediate build outputs of a single package. // // This is a logical equivalent of an object file in traditional compilers. @@ -100,52 +84,6 @@ func (a *Archive) RegisterTypes(packages map[string]*types.Package) error { return err } -// Decl represents a package-level symbol (e.g. a function, variable or type). -// -// It contains code generated by the compiler for this specific symbol, which is -// grouped by the execution stage it belongs to in the JavaScript runtime. -type Decl struct { - // The package- or receiver-type-qualified name of function or method obj. - // See go/types.Func.FullName(). - FullName string - // A logical equivalent of a symbol name in an object file in the traditional - // 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 - // A list of package-level JavaScript variable names this symbol needs to declare. - Vars []string - // NamedRecvType is method named recv declare. - NamedRecvType string - // JavaScript code that declares basic information about a symbol. For a type - // it configures basic information about the type and its identity. For a function - // or method it contains its compiled body. - DeclCode []byte - // JavaScript code that initializes reflection metadata about type's method list. - MethodListCode []byte - // JavaScript code that initializes the rest of reflection metadata about a type - // (e.g. struct fields, array type sizes, element types, etc.). - TypeInitCode []byte - // JavaScript code that needs to be executed during the package init phase to - // set the symbol up (e.g. initialize package-level variable value). - InitCode []byte - // Symbol's identifier used by the dead-code elimination logic, not including - // package path. If empty, the symbol is assumed to be alive and will not be - // eliminated. For methods it is the same as its receiver type identifier. - DceObjectFilter string - // The second part of the identified used by dead-code elimination for methods. - // Empty for other types of symbols. - DceMethodFilter string - // List of fully qualified (including package path) DCE symbol identifiers the - // symbol depends on for dead code elimination purposes. - DceDeps []string - // Set to true if a function performs a blocking operation (I/O or - // synchronization). The compiler will have to generate function code such - // that it can be resumed after a blocking operation completes without - // blocking the main thread in the meantime. - Blocking bool -} - type Dependency struct { Pkg string Type string @@ -303,7 +241,7 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameS if _, err := w.Write(removeWhitespace([]byte(fmt.Sprintf("$packages[\"%s\"] = (function() {\n", pkg.ImportPath)), minify)); err != nil { return err } - vars := []string{"$pkg = {}", "$init"} + vars := []string{"$pkg = {}", "$typeInstances = new $Map()", "$init"} var filteredDecls []*Decl for _, d := range pkg.Declarations { if _, ok := dceSelection[d]; ok { @@ -314,6 +252,13 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameS if _, err := w.Write(removeWhitespace([]byte(fmt.Sprintf("\tvar %s;\n", strings.Join(vars, ", "))), minify)); err != nil { return err } + + for _, d := range filteredDecls { + if _, err := w.Write(d.GenericFactoryCode); err != nil { + return err + } + } + for _, d := range filteredDecls { if _, err := w.Write(d.DeclCode); err != nil { return err diff --git a/compiler/decls.go b/compiler/decls.go new file mode 100644 index 000000000..a94f99a34 --- /dev/null +++ b/compiler/decls.go @@ -0,0 +1,630 @@ +package compiler + +import ( + "fmt" + "go/ast" + "go/constant" + "go/token" + "go/types" + "sort" + "strings" + + "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/typesutil" +) + +// Decl represents a package-level symbol (e.g. a function, variable or type). +// +// It contains code generated by the compiler for this specific symbol, which is +// grouped by the execution stage it belongs to in the JavaScript runtime. +type Decl struct { + // The package- or receiver-type-qualified name of function or method obj. + // See go/types.Func.FullName(). + FullName string + // A logical equivalent of a symbol name in an object file in the traditional + // 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 + // A list of package-level JavaScript variable names this symbol needs to declare. + Vars []string + // NamedRecvType is method named recv declare. + NamedRecvType string + // JavaScript code that declares basic information about a symbol. For a type + // it configures basic information about the type and its identity. For a function + // or method it contains its compiled body. + DeclCode []byte + // JavaScript code that declared a factory function for a generic type or + // function, which can be called to create specific instanced of the type or + // function. + GenericFactoryCode []byte + // JavaScript code that initializes reflection metadata about type's method list. + MethodListCode []byte + // JavaScript code that initializes the rest of reflection metadata about a type + // (e.g. struct fields, array type sizes, element types, etc.). + TypeInitCode []byte + // JavaScript code that needs to be executed during the package init phase to + // set the symbol up (e.g. initialize package-level variable value). + InitCode []byte + // Symbol's identifier used by the dead-code elimination logic, not including + // package path. If empty, the symbol is assumed to be alive and will not be + // eliminated. For methods it is the same as its receiver type identifier. + DceObjectFilter string + // The second part of the identifier used by dead-code elimination for methods. + // Empty for other types of symbols. + DceMethodFilter string + // List of fully qualified (including package path) DCE symbol identifiers the + // symbol depends on for dead code elimination purposes. + DceDeps []string + // Set to true if a function performs a blocking operation (I/O or + // synchronization). The compiler will have to generate function code such + // that it can be resumed after a blocking operation completes without + // blocking the main thread in the meantime. + Blocking bool +} + +// minify returns a copy of Decl with unnecessary whitespace removed from the +// JS code. +func (d Decl) minify() Decl { + d.DeclCode = removeWhitespace(d.DeclCode, true) + d.MethodListCode = removeWhitespace(d.MethodListCode, true) + d.TypeInitCode = removeWhitespace(d.TypeInitCode, true) + d.InitCode = removeWhitespace(d.InitCode, true) + return d +} + +// topLevelObjects extracts package-level variables, functions and named types +// from the package AST. +func (fc *funcContext) topLevelObjects(srcs sources) (vars []*types.Var, functions []*ast.FuncDecl, typeNames []*types.TypeName) { + if !fc.IsRoot() { + panic(bailout(fmt.Errorf("functionContext.discoverObjects() must be only called on the package-level context"))) + } + + for _, file := range srcs.Files { + for _, decl := range file.Decls { + switch d := decl.(type) { + case *ast.FuncDecl: + sig := fc.pkgCtx.Defs[d.Name].(*types.Func).Type().(*types.Signature) + if sig.Recv() == nil { + fc.objectName(fc.pkgCtx.Defs[d.Name]) // register toplevel name + } + if !isBlank(d.Name) { + functions = append(functions, d) + } + case *ast.GenDecl: + switch d.Tok { + case token.TYPE: + for _, spec := range d.Specs { + o := fc.pkgCtx.Defs[spec.(*ast.TypeSpec).Name].(*types.TypeName) + typeNames = append(typeNames, o) + fc.objectName(o) // register toplevel name + } + case token.VAR: + for _, spec := range d.Specs { + for _, name := range spec.(*ast.ValueSpec).Names { + if !isBlank(name) { + o := fc.pkgCtx.Defs[name].(*types.Var) + vars = append(vars, o) + fc.objectName(o) // register toplevel name + } + } + } + case token.CONST: + // skip, constants are inlined + } + } + } + } + + return vars, functions, typeNames +} + +// importDecls processes import declarations. +// +// For each imported package: +// - A new package-level variable is reserved to refer to symbols from that +// package. +// - A Decl instance is generated to be included in the Archive. +// +// Lists of imported package paths and corresponding Decls is returned to the caller. +func (fc *funcContext) importDecls() (importedPaths []string, importDecls []*Decl) { + if !fc.IsRoot() { + panic(bailout(fmt.Errorf("functionContext.importDecls() must be only called on the package-level context"))) + } + + imports := []*types.Package{} + for _, pkg := range fc.pkgCtx.Pkg.Imports() { + if pkg == types.Unsafe { + // Prior to Go 1.9, unsafe import was excluded by Imports() method, + // but now we do it here to maintain previous behavior. + continue + } + imports = append(imports, pkg) + } + + // Deterministic processing order. + sort.Slice(imports, func(i, j int) bool { return imports[i].Path() < imports[j].Path() }) + + for _, pkg := range imports { + importedPaths = append(importedPaths, pkg.Path()) + importDecls = append(importDecls, fc.newImportDecl(pkg)) + } + + return importedPaths, importDecls +} + +// newImportDecl registers the imported package and returns a Decl instance for it. +func (fc *funcContext) newImportDecl(importedPkg *types.Package) *Decl { + pkgVar := fc.importedPkgVar(importedPkg) + return &Decl{ + Vars: []string{pkgVar}, + DeclCode: []byte(fmt.Sprintf("\t%s = $packages[\"%s\"];\n", pkgVar, importedPkg.Path())), + InitCode: fc.CatchOutput(1, func() { fc.translateStmt(fc.importInitializer(importedPkg.Path()), nil) }), + } +} + +// importInitializer calls the imported package $init() function to ensure it is +// initialized before any code in the importer package runs. +func (fc *funcContext) importInitializer(impPath string) ast.Stmt { + pkgVar := fc.pkgCtx.pkgVars[impPath] + id := fc.newIdent(fmt.Sprintf(`%s.$init`, pkgVar), types.NewSignature(nil, nil, nil, false)) + call := &ast.CallExpr{Fun: id} + fc.Blocking[call] = true + fc.Flattened[call] = true + + return &ast.ExprStmt{X: call} +} + +// varDecls translates all package-level variables. +// +// `vars` argument must contain all package-level variables found in the package. +// The method returns corresponding Decls that declare and initialize the vars +// as appropriate. Decls are returned in order necessary to correctly initialize +// the variables, considering possible dependencies between them. +func (fc *funcContext) varDecls(vars []*types.Var) []*Decl { + if !fc.IsRoot() { + panic(bailout(fmt.Errorf("functionContext.varDecls() must be only called on the package-level context"))) + } + + var varDecls []*Decl + varsWithInit := fc.pkgCtx.VarsWithInitializers() + + initializers := []*types.Initializer{} + + // For implicitly-initialized vars we generate synthetic zero-value + // initializers and then process them the same way as explicitly initialized. + for _, o := range vars { + if varsWithInit[o] { + continue + } + initializer := &types.Initializer{ + Lhs: []*types.Var{o}, + Rhs: fc.zeroValue(o.Type()), + } + initializers = append(initializers, initializer) + } + + // Add explicitly-initialized variables to the list. Implicitly-initialized + // variables should be declared first in case explicit initializers depend on + // them. + initializers = append(initializers, fc.pkgCtx.InitOrder...) + + for _, init := range initializers { + varDecls = append(varDecls, fc.newVarDecl(init)) + } + + return varDecls +} + +// newVarDecl creates a new Decl describing a variable, given an explicit +// initializer. +func (fc *funcContext) newVarDecl(init *types.Initializer) *Decl { + var d Decl + + assignLHS := []ast.Expr{} + for _, o := range init.Lhs { + assignLHS = append(assignLHS, fc.newIdentFor(o)) + + // For non-exported package-level variables we need to declared a local JS + // variable. Exported variables are represented as properties of the $pkg + // JS object. + if !o.Exported() { + d.Vars = append(d.Vars, fc.objectName(o)) + } + if fc.pkgCtx.HasPointer[o] && !o.Exported() { + d.Vars = append(d.Vars, fc.varPtrName(o)) + } + } + + d.DceDeps = fc.CollectDCEDeps(func() { + fc.localVars = nil + d.InitCode = fc.CatchOutput(1, func() { + fc.translateStmt(&ast.AssignStmt{ + Lhs: assignLHS, + Tok: token.DEFINE, + Rhs: []ast.Expr{init.Rhs}, + }, nil) + }) + + // Initializer code may have introduced auxiliary variables (e.g. for + // handling multi-assignment or blocking calls), add them to the decl too. + d.Vars = append(d.Vars, fc.localVars...) + fc.localVars = nil // Clean up after ourselves. + }) + + if len(init.Lhs) == 1 { + if !analysis.HasSideEffect(init.Rhs, fc.pkgCtx.Info.Info) { + d.DceObjectFilter = init.Lhs[0].Name() + } + } + return &d +} + +// funcDecls translates all package-level function and methods. +// +// `functions` must contain all package-level function and method declarations +// found in the AST. The function returns Decls that define corresponding JS +// functions at runtime. For special functions like init() and main() decls will +// also contain code necessary to invoke them. +func (fc *funcContext) funcDecls(functions []*ast.FuncDecl) ([]*Decl, error) { + if !fc.IsRoot() { + panic(bailout(fmt.Errorf("functionContext.funcDecls() must be only called on the package-level context"))) + } + + var mainFunc *types.Func + funcDecls := []*Decl{} + for _, fun := range functions { + funcDecls = append(funcDecls, fc.newFuncDecl(fun)) + + if o := fc.pkgCtx.Defs[fun.Name].(*types.Func); o.Name() == "main" { + mainFunc = o // main() function candidate. + } + } + if fc.pkgCtx.IsMain() { + if mainFunc == nil { + return nil, fmt.Errorf("missing main function") + } + // Add a special Decl for invoking main() function after the program has + // been initialized. It must come after all other functions, especially all + // init() functions, otherwise main() will be invoked too early. + funcDecls = append(funcDecls, &Decl{ + InitCode: fc.CatchOutput(1, func() { fc.translateStmt(fc.callMainFunc(mainFunc), nil) }), + }) + } + return funcDecls, nil +} + +// newFuncDecl returns a Decl that defines a package-level function or a method. +func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl) *Decl { + o := fc.pkgCtx.Defs[fun.Name].(*types.Func) + + d := &Decl{ + FullName: o.FullName(), + Blocking: fc.pkgCtx.IsBlocking(o), + LinkingName: newSymName(o), + } + + sig := typesutil.Signature{Sig: o.Type().(*types.Signature)} + if typesutil.IsMethod(o) { + namedRecvType := sig.RecvType() + + d.NamedRecvType = fc.objectName(namedRecvType.Obj()) + d.DceObjectFilter = namedRecvType.Obj().Name() + if !fun.Name.IsExported() { + d.DceMethodFilter = o.Name() + "~" + } + } else { + d.Vars = []string{fc.objectName(o)} + switch o.Name() { + case "main": + if fc.pkgCtx.IsMain() { // Found main() function of the program. + d.DceObjectFilter = "" // Always reachable. + } + case "init": + d.InitCode = fc.CatchOutput(1, func() { fc.translateStmt(fc.callInitFunc(o), nil) }) + d.DceObjectFilter = "" // init() function is always reachable. + default: + d.DceObjectFilter = o.Name() + } + } + + d.DceDeps = fc.CollectDCEDeps(func() { + code := fc.translateTopLevelFunction(fun) + if sig.IsGeneric() { + d.GenericFactoryCode = code + } else { + d.DeclCode = code + } + }) + return d +} + +// callInitFunc returns an AST statement for calling the given instance of the +// package's init() function. +func (fc *funcContext) callInitFunc(init *types.Func) ast.Stmt { + id := fc.newIdentFor(init) + call := &ast.CallExpr{Fun: id} + if fc.pkgCtx.IsBlocking(init) { + fc.Blocking[call] = true + } + return &ast.ExprStmt{X: call} +} + +// callMainFunc returns an AST statement for calling the main() function of the +// program, which should be included in the $init() function of the main package. +func (fc *funcContext) callMainFunc(main *types.Func) ast.Stmt { + id := fc.newIdentFor(main) + call := &ast.CallExpr{Fun: id} + ifStmt := &ast.IfStmt{ + Cond: fc.newIdent("$pkg === $mainPkg", types.Typ[types.Bool]), + Body: &ast.BlockStmt{ + List: []ast.Stmt{ + &ast.ExprStmt{X: call}, + &ast.AssignStmt{ + Lhs: []ast.Expr{fc.newIdent("$mainFinished", types.Typ[types.Bool])}, + Tok: token.ASSIGN, + Rhs: []ast.Expr{fc.newConst(types.Typ[types.Bool], constant.MakeBool(true))}, + }, + }, + }, + } + if fc.pkgCtx.IsBlocking(main) { + fc.Blocking[call] = true + fc.Flattened[ifStmt] = true + } + + return ifStmt +} + +// namedTypeDecls returns Decls that define all names Go types. +// +// `typeNames` must contain all named types defined in the package, including +// those defined inside function bodies. +func (fc *funcContext) namedTypeDecls(typeNames []*types.TypeName) []*Decl { + if !fc.IsRoot() { + panic(bailout(fmt.Errorf("functionContext.namedTypeDecls() must be only called on the package-level context"))) + } + + decls := []*Decl{} + for _, o := range typeNames { + if o.IsAlias() { + continue + } + + decls = append(decls, fc.newNamedTypeDecl(o)) + } + return decls +} + +// newNamedTypeDecl returns a Decl that represents a named Go type. +func (fc *funcContext) newNamedTypeDecl(o *types.TypeName) *Decl { + typeName := fc.objectName(o) + d := &Decl{ + Vars: []string{typeName}, + DceObjectFilter: o.Name(), + } + + extraIndent := 0 // Additional indentation for the type initialization code. + instanceVar := typeName // JS variable the type instance is assigned to. + typeString := fc.typeString(o) // Type string for reflect. + + typeParams := typesutil.TypeParams(o.Type()) + if typeParams != nil { + fc.genericCtx = &genericCtx{} + defer func() { fc.genericCtx = nil }() + extraIndent++ // For generic factory function. + instanceVar = fc.newLocalVariable("instance") // Avoid conflict with factory function name. + } + + d.DceDeps = fc.CollectDCEDeps(func() { + // Code that declares a JS type (i.e. prototype) for each Go type. + d.DeclCode = fc.CatchOutput(extraIndent, func() { + size := int64(0) + constructor := "null" + + switch t := o.Type().Underlying().(type) { + case *types.Struct: + constructor = fc.structConstructor(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()" + } + } + fc.Printf(`%s = $newType(%d, %s, %s, %t, "%s", %t, %s);`, + instanceVar, size, typeKind(o.Type()), typeString, o.Name() != "", o.Pkg().Path(), o.Exported(), constructor) + }) + + // Reflection metadata about methods the type has. + d.MethodListCode = fc.CatchOutput(extraIndent, 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++ { + entry, isPtr := fc.methodListEntry(named.Method(i)) + if isPtr { + ptrMethods = append(ptrMethods, entry) + } else { + methods = append(methods, entry) + } + } + if len(methods) > 0 { + fc.Printf("%s.methods = [%s];", instanceVar, strings.Join(methods, ", ")) + } + if len(ptrMethods) > 0 { + fc.Printf("$ptrType(%s).methods = [%s];", instanceVar, strings.Join(ptrMethods, ", ")) + } + }) + + // Certain types need to run additional type-specific logic to fully + // initialize themselves. + 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 = fc.CatchOutput(extraIndent, func() { + fc.Printf("%s.init(%s);", instanceVar, fc.initArgs(t)) + }) + } + }) + + if typeParams != nil { + // Generic types are instantiated at runtime by calling an intermediate + // generic factory function. This function combines together all code + // from a regular type Decl (e.g. DeclCode, TypeInitCode, MethodListCode) + // and is defined at DeclCode time. + + typeParamNames := fc.typeParamVars(typeParams) + + d.GenericFactoryCode = fc.CatchOutput(0, func() { + // Begin generic factory function. + fc.Printf("%s = function(%s) {", typeName, strings.Join(typeParamNames, ", ")) + + fc.Indented(func() { + // If this instance has been instantiated already, reuse the object. + fc.Printf("var %s = $typeInstances.get(%s);", instanceVar, typeString) + fc.Printf("if (%[1]s) { return %[1]s; }", instanceVar) + + // Forward-declare variables for the synthesized type names so that they + // could be captured by the type constructor closure. + if fc.genericCtx.anonTypes.Len() != 0 { + fc.Printf("var %s;", strings.Join(fc.genericCtx.anonTypes.Names(), ", ")) + } + + // Construct type instance. + fmt.Fprint(fc, string(d.DeclCode)) + fc.Printf("$typeInstances.set(%s, %s);", typeString, instanceVar) + + // Construct anonymous types which depend on type parameters. It must be + // done after the type instance has been cached to avoid infinite + // recursion in case the type depends on itself. + for _, t := range fc.genericCtx.anonTypes.Ordered() { + fc.Printf("%s = $%sType(%s);", t.Name(), strings.ToLower(typeKind(t.Type())[5:]), fc.initArgs(t.Type())) + } + + fc.Printf("$instantiateMethods(%s, %s, %s)", instanceVar, fmt.Sprintf("%s.methods", typeName), strings.Join(typeParamNames, ", ")) + fc.Printf("$instantiateMethods(%s, %s, %s)", fmt.Sprintf("$ptrType(%s)", instanceVar), fmt.Sprintf("%s.ptrMethods", typeName), strings.Join(typeParamNames, ", ")) + fmt.Fprint(fc, string(d.TypeInitCode)) + fmt.Fprint(fc, string(d.MethodListCode)) + + fc.Printf("return %s;", instanceVar) + }) + + // End generic factory function. + fc.Printf("}") + fc.Printf("%[1]s.methods = {}; %[1]s.ptrMethods = {};", typeName) + }) + + // Clean out code that has been absorbed by the generic factory function. + d.TypeInitCode = nil + d.MethodListCode = nil + d.DeclCode = nil + } + + if getVarLevel(o) == varPackage { + // Package-level types are also accessible as properties on the package + // object. + exported := fc.CatchOutput(0, func() { + fc.Printf("$pkg.%s = %s;", encodeIdent(o.Name()), typeName) + }) + d.DeclCode = append(d.DeclCode, []byte(exported)...) + } + return d +} + +// typeString returns a string with a JavaScript string expression that +// constructs the type string for the provided object. For a generic type this +// will be a template literal that substitutes type parameters at runtime. +func (fc *funcContext) typeString(o types.Object) string { + typeParams := typesutil.TypeParams(o.Type()) + if typeParams == nil { + return fmt.Sprintf(`"%s.%s"`, o.Pkg().Name(), o.Name()) + } + + args := []string{} + for i := 0; i < typeParams.Len(); i++ { + args = append(args, fmt.Sprintf("${%s.string}", fc.typeName(typeParams.At(i)))) + } + + return fmt.Sprintf("`%s.%s[%s]`", o.Pkg().Name(), o.Name(), strings.Join(args, ",")) +} + +// structConstructor returns JS constructor function for a struct type. +func (fc *funcContext) structConstructor(t *types.Struct) string { + constructor := &strings.Builder{} + + ctrArgs := make([]string, t.NumFields()) + for i := 0; i < t.NumFields(); i++ { + ctrArgs[i] = fieldName(t, i) + "_" + } + + fmt.Fprintf(constructor, "function(%s) {\n", strings.Join(ctrArgs, ", ")) + fmt.Fprintf(constructor, "%sthis.$val = this;\n", fc.Indentation(1)) + + // If no arguments were passed, zero-initialize all fields. + fmt.Fprintf(constructor, "%sif (arguments.length === 0) {\n", fc.Indentation(1)) + for i := 0; i < t.NumFields(); i++ { + fmt.Fprintf(constructor, "%sthis.%s = %s;\n", fc.Indentation(2), fieldName(t, i), fc.translateExpr(fc.zeroValue(t.Field(i).Type())).String()) + } + fmt.Fprintf(constructor, "%sreturn;\n", fc.Indentation(2)) + fmt.Fprintf(constructor, "%s}\n", fc.Indentation(1)) + + // Otherwise initialize fields with the provided values. + for i := 0; i < t.NumFields(); i++ { + fmt.Fprintf(constructor, "%sthis.%[2]s = %[2]s_;\n", fc.Indentation(1), fieldName(t, i)) + } + fmt.Fprintf(constructor, "%s}", fc.Indentation(0)) + return constructor.String() +} + +// methodListEntry returns a JS code fragment that describes the given method +// function for runtime reflection. It returns isPtr=true if the method belongs +// to the pointer-receiver method list. +func (fc *funcContext) methodListEntry(method *types.Func) (entry string, isPtr bool) { + 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, fc.initArgs(t)) + _, isPtr = t.Recv().Type().(*types.Pointer) + return entry, isPtr +} + +// anonTypeDecls returns a list of Decls corresponding to anonymous Go types +// encountered in the package. +// +// `anonTypes` must contain an ordered list of anonymous types with the +// identifiers that were auto-assigned to them. They must be sorted in the +// topological initialization order (e.g. `[]int` is before `struct{f []int}`). +// +// See also typesutil.AnonymousTypes. +func (fc *funcContext) anonTypeDecls(anonTypes []*types.TypeName) []*Decl { + if !fc.IsRoot() { + panic(bailout(fmt.Errorf("functionContext.anonTypeDecls() must be only called on the package-level context"))) + } + + decls := []*Decl{} + for _, t := range anonTypes { + d := Decl{ + Vars: []string{t.Name()}, + DceObjectFilter: t.Name(), + } + d.DceDeps = fc.CollectDCEDeps(func() { + d.DeclCode = []byte(fmt.Sprintf("\t%s = $%sType(%s);\n", t.Name(), strings.ToLower(typeKind(t.Type())[5:]), fc.initArgs(t.Type()))) + }) + decls = append(decls, &d) + } + + return decls +} diff --git a/compiler/errors.go b/compiler/errors.go new file mode 100644 index 000000000..838e3ece8 --- /dev/null +++ b/compiler/errors.go @@ -0,0 +1,68 @@ +package compiler + +import ( + "errors" + "fmt" +) + +// ErrTooManyErrors is added to the ErrorList by the Trim method. +var ErrTooManyErrors = errors.New("too many errors") + +// ErrorList wraps multiple errors as a single error. +type ErrorList []error + +func (errs ErrorList) Error() string { + if len(errs) == 0 { + return "" + } + return fmt.Sprintf("%s (and %d more errors)", errs[0].Error(), len(errs[1:])) +} + +// ErrOrNil returns nil if ErrorList is empty, or the error otherwise. +func (errs ErrorList) ErrOrNil() error { + if len(errs) == 0 { + return nil + } + return errs +} + +// Append an error to the list. +// +// If err is an instance of ErrorList, the lists are concatenated together, +// otherwise err is appended at the end of the list. If err is nil, the list is +// returned unmodified. +// +// err := DoStuff() +// errList := errList.Append(err) +func (errs ErrorList) Append(err error) ErrorList { + if err == nil { + return errs + } + if err, ok := err.(ErrorList); ok { + return append(errs, err...) + } + return append(errs, err) +} + +// AppendDistinct is similar to Append, but doesn't append the error if it has +// the same message as the last error on the list. +func (errs ErrorList) AppendDistinct(err error) ErrorList { + if l := len(errs); l > 0 { + if prev := errs[l-1]; prev != nil && err.Error() == prev.Error() { + return errs // The new error is the same as the last one, skip it. + } + } + + return errs.Append(err) +} + +// Trim the error list if it has more than limit errors. If the list is trimmed, +// all extraneous errors are replaced with a single ErrTooManyErrors, making the +// returned ErrorList length of limit+1. +func (errs ErrorList) Trim(limit int) ErrorList { + if len(errs) <= limit { + return errs + } + + return append(errs[:limit], ErrTooManyErrors) +} diff --git a/compiler/expressions.go b/compiler/expressions.go index db08cc31f..a4069bf74 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -35,6 +35,19 @@ func (e *expression) StringWithParens() string { func (fc *funcContext) translateExpr(expr ast.Expr) *expression { exprType := fc.pkgCtx.TypeOf(expr) if value := fc.pkgCtx.Types[expr].Value; value != nil { + if tParam, ok := exprType.(*types.TypeParam); ok { + // If we are dealing with a type param, we don't know which concrete type + // it will be instantiated with, so we don't know how to represent the + // constant value ahead of time. Instead, generate a typed constant and + // perform type conversion to the instantiated type at runtime. + return fc.translateExpr( + fc.typeCastExpr( + astutil.MakeTypedConstant(fc.pkgCtx.Info.Info, value), + fc.newIdentFor(tParam.Obj()), + ), + ) + } + basic := exprType.Underlying().(*types.Basic) switch { case isBoolean(basic): @@ -100,18 +113,26 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { switch e := expr.(type) { case *ast.CompositeLit: + if exprType, ok := exprType.(*types.TypeParam); ok { + // Composite literals can be used with a type parameter if it has a core + // type. However, because at compile time we don't know the concrete type + // the type parameter will take, we initialize the literal with the core + // type and cast it to the a type denoted by the type param at runtime. + litValue := fc.setType(&ast.CompositeLit{ + Elts: e.Elts, + }, typesutil.CoreType(exprType)) + cast := fc.typeCastExpr(litValue, fc.newIdentFor(exprType.Obj())) + return fc.translateExpr(cast) + } + if ptrType, isPointer := exprType.Underlying().(*types.Pointer); isPointer { // Go automatically treats `[]*T{{}}` as `[]*T{&T{}}`, in which case the // inner composite literal `{}` would has a pointer type. To make sure the // type conversion is handled correctly, we generate the explicit AST for // this. - var rewritten ast.Expr = fc.setType(&ast.UnaryExpr{ - OpPos: e.Pos(), - Op: token.AND, - X: fc.setType(&ast.CompositeLit{ - Elts: e.Elts, - }, ptrType.Elem()), - }, ptrType) + var rewritten ast.Expr = fc.takeAddressExpr(fc.setType(&ast.CompositeLit{ + Elts: e.Elts, + }, ptrType.Elem())) if exprType, ok := exprType.(*types.Named); ok { // Handle a special case when the pointer type is named, e.g.: @@ -119,13 +140,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { // _ = []PS{{}} // In that case the value corresponding to the inner literal `{}` is // initialized as `&S{}` and then converted to `PS`: `[]PS{PS(&S{})}`. - typeCast := fc.setType(&ast.CallExpr{ - Fun: fc.newTypeIdent(exprType.String(), exprType.Obj()), - Lparen: e.Lbrace, - Args: []ast.Expr{rewritten}, - Rparen: e.Rbrace, - }, exprType) - rewritten = typeCast + rewritten = fc.typeCastExpr(rewritten, fc.newIdentFor(exprType.Obj())) } return fc.translateExpr(rewritten) } @@ -203,7 +218,7 @@ 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 := fc.literalFuncContext(e).translateFunctionBody(e.Type, nil, e.Body) if len(fc.pkgCtx.escapingVars) != 0 { names := make([]string, 0, len(fc.pkgCtx.escapingVars)) for obj := range fc.pkgCtx.escapingVars { @@ -304,175 +319,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } case *ast.BinaryExpr: - if e.Op == token.NEQ { - return fc.formatExpr("!(%s)", fc.translateExpr(&ast.BinaryExpr{ - X: e.X, - Op: token.EQL, - Y: e.Y, - })) - } - - t := fc.pkgCtx.TypeOf(e.X) - t2 := fc.pkgCtx.TypeOf(e.Y) - _, isInterface := t2.Underlying().(*types.Interface) - if isInterface || types.Identical(t, types.Typ[types.UntypedNil]) { - t = t2 - } - - if basic, isBasic := t.Underlying().(*types.Basic); isBasic && isNumeric(basic) { - if is64Bit(basic) { - switch e.Op { - case token.MUL: - return fc.formatExpr("$mul64(%e, %e)", e.X, e.Y) - case token.QUO: - return fc.formatExpr("$div64(%e, %e, false)", e.X, e.Y) - case token.REM: - return fc.formatExpr("$div64(%e, %e, true)", e.X, e.Y) - case token.SHL: - return fc.formatExpr("$shiftLeft64(%e, %f)", e.X, e.Y) - case token.SHR: - return fc.formatExpr("$shiftRight%s(%e, %f)", toJavaScriptType(basic), e.X, e.Y) - case token.EQL: - return fc.formatExpr("(%1h === %2h && %1l === %2l)", e.X, e.Y) - case token.LSS: - return fc.formatExpr("(%1h < %2h || (%1h === %2h && %1l < %2l))", e.X, e.Y) - case token.LEQ: - return fc.formatExpr("(%1h < %2h || (%1h === %2h && %1l <= %2l))", e.X, e.Y) - case token.GTR: - return fc.formatExpr("(%1h > %2h || (%1h === %2h && %1l > %2l))", e.X, e.Y) - case token.GEQ: - return fc.formatExpr("(%1h > %2h || (%1h === %2h && %1l >= %2l))", e.X, e.Y) - case token.ADD, token.SUB: - return fc.formatExpr("new %3s(%1h %4t %2h, %1l %4t %2l)", e.X, e.Y, fc.typeName(t), e.Op) - case token.AND, token.OR, token.XOR: - return fc.formatExpr("new %3s(%1h %4t %2h, (%1l %4t %2l) >>> 0)", e.X, e.Y, fc.typeName(t), e.Op) - case token.AND_NOT: - return fc.formatExpr("new %3s(%1h & ~%2h, (%1l & ~%2l) >>> 0)", e.X, e.Y, fc.typeName(t)) - default: - panic(e.Op) - } - } - - if isComplex(basic) { - switch e.Op { - case token.EQL: - return fc.formatExpr("(%1r === %2r && %1i === %2i)", e.X, e.Y) - case token.ADD, token.SUB: - return fc.formatExpr("new %3s(%1r %4t %2r, %1i %4t %2i)", e.X, e.Y, fc.typeName(t), e.Op) - case token.MUL: - return fc.formatExpr("new %3s(%1r * %2r - %1i * %2i, %1r * %2i + %1i * %2r)", e.X, e.Y, fc.typeName(t)) - case token.QUO: - return fc.formatExpr("$divComplex(%e, %e)", e.X, e.Y) - default: - panic(e.Op) - } - } - - switch e.Op { - case token.EQL: - return fc.formatParenExpr("%e === %e", e.X, e.Y) - case token.LSS, token.LEQ, token.GTR, token.GEQ: - return fc.formatExpr("%e %t %e", e.X, e.Op, e.Y) - case token.ADD, token.SUB: - return fc.fixNumber(fc.formatExpr("%e %t %e", e.X, e.Op, e.Y), basic) - case token.MUL: - switch basic.Kind() { - case types.Int32, types.Int: - return fc.formatParenExpr("$imul(%e, %e)", e.X, e.Y) - case types.Uint32, types.Uintptr: - return fc.formatParenExpr("$imul(%e, %e) >>> 0", e.X, e.Y) - } - return fc.fixNumber(fc.formatExpr("%e * %e", e.X, e.Y), basic) - case token.QUO: - if isInteger(basic) { - // cut off decimals - shift := ">>" - if isUnsigned(basic) { - shift = ">>>" - } - return fc.formatExpr(`(%1s = %2e / %3e, (%1s === %1s && %1s !== 1/0 && %1s !== -1/0) ? %1s %4s 0 : $throwRuntimeError("integer divide by zero"))`, fc.newVariable("_q"), e.X, e.Y, shift) - } - if basic.Kind() == types.Float32 { - return fc.fixNumber(fc.formatExpr("%e / %e", e.X, e.Y), basic) - } - return fc.formatExpr("%e / %e", e.X, e.Y) - case token.REM: - return fc.formatExpr(`(%1s = %2e %% %3e, %1s === %1s ? %1s : $throwRuntimeError("integer divide by zero"))`, fc.newVariable("_r"), e.X, e.Y) - case token.SHL, token.SHR: - op := e.Op.String() - if e.Op == token.SHR && isUnsigned(basic) { - op = ">>>" - } - if v := fc.pkgCtx.Types[e.Y].Value; v != nil { - i, _ := constant.Uint64Val(constant.ToInt(v)) - if i >= 32 { - return fc.formatExpr("0") - } - return fc.fixNumber(fc.formatExpr("%e %s %s", e.X, op, strconv.FormatUint(i, 10)), basic) - } - if e.Op == token.SHR && !isUnsigned(basic) { - return fc.fixNumber(fc.formatParenExpr("%e >> $min(%f, 31)", e.X, e.Y), basic) - } - y := fc.newVariable("y") - return fc.fixNumber(fc.formatExpr("(%s = %f, %s < 32 ? (%e %s %s) : 0)", y, e.Y, y, e.X, op, y), basic) - case token.AND, token.OR: - if isUnsigned(basic) { - return fc.formatParenExpr("(%e %t %e) >>> 0", e.X, e.Op, e.Y) - } - return fc.formatParenExpr("%e %t %e", e.X, e.Op, e.Y) - case token.AND_NOT: - return fc.fixNumber(fc.formatParenExpr("%e & ~%e", e.X, e.Y), basic) - case token.XOR: - return fc.fixNumber(fc.formatParenExpr("%e ^ %e", e.X, e.Y), basic) - default: - panic(e.Op) - } - } - - switch e.Op { - case token.ADD, token.LSS, token.LEQ, token.GTR, token.GEQ: - return fc.formatExpr("%e %t %e", e.X, e.Op, e.Y) - case token.LAND: - if fc.Blocking[e.Y] { - skipCase := fc.caseCounter - fc.caseCounter++ - resultVar := fc.newVariable("_v") - fc.Printf("if (!(%s)) { %s = false; $s = %d; continue s; }", fc.translateExpr(e.X), resultVar, skipCase) - fc.Printf("%s = %s; case %d:", resultVar, fc.translateExpr(e.Y), skipCase) - return fc.formatExpr("%s", resultVar) - } - return fc.formatExpr("%e && %e", e.X, e.Y) - case token.LOR: - if fc.Blocking[e.Y] { - skipCase := fc.caseCounter - fc.caseCounter++ - resultVar := fc.newVariable("_v") - fc.Printf("if (%s) { %s = true; $s = %d; continue s; }", fc.translateExpr(e.X), resultVar, skipCase) - fc.Printf("%s = %s; case %d:", resultVar, fc.translateExpr(e.Y), skipCase) - return fc.formatExpr("%s", resultVar) - } - return fc.formatExpr("%e || %e", e.X, e.Y) - case token.EQL: - switch u := t.Underlying().(type) { - case *types.Array, *types.Struct: - return fc.formatExpr("$equal(%e, %e, %s)", e.X, e.Y, fc.typeName(t)) - case *types.Interface: - return fc.formatExpr("$interfaceIsEqual(%s, %s)", fc.translateImplicitConversion(e.X, t), fc.translateImplicitConversion(e.Y, t)) - case *types.Basic: - if isBoolean(u) { - if b, ok := analysis.BoolValue(e.X, fc.pkgCtx.Info.Info); ok && b { - return fc.translateExpr(e.Y) - } - if b, ok := analysis.BoolValue(e.Y, fc.pkgCtx.Info.Info); ok && b { - return fc.translateExpr(e.X) - } - } - } - return fc.formatExpr("%s === %s", fc.translateImplicitConversion(e.X, t), fc.translateImplicitConversion(e.Y, t)) - default: - panic(e.Op) - } - + return fc.translateBinaryExpr(e) case *ast.ParenExpr: return fc.formatParenExpr("%e", e.X) @@ -505,7 +352,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if _, isTuple := exprType.(*types.Tuple); isTuple { return fc.formatExpr( `(%1s = $mapIndex(%2e,%3s), %1s !== undefined ? [%1s.v, true] : [%4e, false])`, - fc.newVariable("_entry"), + fc.newLocalVariable("_entry"), e.X, key, fc.zeroValue(t.Elem()), @@ -513,17 +360,25 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } return fc.formatExpr( `(%1s = $mapIndex(%2e,%3s), %1s !== undefined ? %1s.v : %4e)`, - fc.newVariable("_entry"), + fc.newLocalVariable("_entry"), e.X, key, fc.zeroValue(t.Elem()), ) case *types.Basic: return fc.formatExpr("%e.charCodeAt(%f)", e.X, e.Index) + case *types.Signature: + return fc.translateGenericInstance(e) default: - panic(fmt.Sprintf("Unhandled IndexExpr: %T\n", t)) + panic(fmt.Errorf("unhandled IndexExpr: %T", t)) + } + case *ast.IndexListExpr: + switch t := fc.pkgCtx.TypeOf(e.X).Underlying().(type) { + case *types.Signature: + return fc.translateGenericInstance(e) + 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) { switch { @@ -576,7 +431,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { return fc.formatExpr(`$methodVal(%s, "%s")`, fc.makeReceiver(e), sel.Obj().(*types.Func).Name()) case types.MethodExpr: if !sel.Obj().Exported() { - fc.pkgCtx.dependencies[sel.Obj()] = true + fc.DeclareDCEDep(sel.Obj()) } if _, ok := sel.Recv().Underlying().(*types.Interface); ok { return fc.formatExpr(`$ifaceMethodExpr("%s")`, sel.Obj().(*types.Func).Name()) @@ -670,13 +525,13 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case "Call": if id, ok := fc.identifierConstant(e.Args[0]); ok { if e.Ellipsis.IsValid() { - objVar := fc.newVariable("obj") + objVar := fc.newLocalVariable("obj") return fc.formatExpr("(%s = %s, %s.%s.apply(%s, %s))", objVar, recv, objVar, id, objVar, externalizeExpr(e.Args[1])) } return fc.formatExpr("%s(%s)", globalRef(id), externalizeArgs(e.Args[1:])) } if e.Ellipsis.IsValid() { - objVar := fc.newVariable("obj") + objVar := fc.newLocalVariable("obj") return fc.formatExpr("(%s = %s, %s[$externalize(%e, $String)].apply(%s, %s))", objVar, recv, objVar, e.Args[0], objVar, externalizeExpr(e.Args[1])) } return fc.formatExpr("%s[$externalize(%e, $String)](%s)", recv, e.Args[0], externalizeArgs(e.Args[1:])) @@ -711,10 +566,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } } - methodName := sel.Obj().Name() - if reservedKeywords[methodName] { - methodName += "$" - } + methodName := fc.methodName(sel.Obj().(*types.Func)) return fc.translateCall(e, sig, fc.formatExpr("%s.%s", recv, methodName)) case types.FieldVal: @@ -777,6 +629,10 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case *types.Var, *types.Const: return fc.formatExpr("%s", fc.objectName(o)) case *types.Func: + if _, ok := fc.pkgCtx.Info.Instances[e]; ok { + // Generic function call with auto-inferred types. + return fc.translateGenericInstance(e) + } return fc.formatExpr("%s", fc.objectName(o)) case *types.TypeName: return fc.formatExpr("%s", fc.typeName(o.Type())) @@ -784,10 +640,13 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if typesutil.IsJsObject(exprType) { return fc.formatExpr("null") } + if typesutil.IsGeneric(exprType) { + return fc.formatExpr("%s.zero()", fc.typeName(exprType)) + } switch t := exprType.Underlying().(type) { case *types.Basic: if t.Kind() != types.UnsafePointer { - panic("unexpected basic type") + panic(fmt.Errorf("unexpected basic type: %v", t)) } return fc.formatExpr("0") case *types.Slice, *types.Pointer: @@ -816,6 +675,246 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } } +func (fc *funcContext) translateBinaryExpr(e *ast.BinaryExpr) *expression { + if e.Op == token.NEQ { + return fc.formatExpr("!(%s)", fc.translateExpr(&ast.BinaryExpr{ + X: e.X, + Op: token.EQL, + Y: e.Y, + })) + } + + t := fc.pkgCtx.TypeOf(e.X) + t2 := fc.pkgCtx.TypeOf(e.Y) + + // Exact type param instantiations are not known at compile time, so + // some operators require special handling. + if (typesutil.IsTypeParam(t) || typesutil.IsTypeParam(t2)) && + !(typesutil.IsInterface(t) || typesutil.IsInterface(t2)) { // == operator between an interface and other types is handled below. + if !types.Identical(t, t2) { + // This should never happen. + panic(bailout(fmt.Errorf("%s: binary operator %v is applied to different type param types %s and %s", fc.pkgCtx.fileSet.Position(e.Pos()), e.Op, t, t2))) + } + + switch e.Op { + case token.EQL: + return fc.formatExpr("$equal(%e, %e, %s)", e.X, e.Y, fc.typeName(t)) + case token.ADD: + return fc.formatExpr("%s.add(%e, %e)", fc.typeName(t), e.X, e.Y) + case token.SUB: + return fc.formatExpr("%s.sub(%e, %e)", fc.typeName(t), e.X, e.Y) + case token.MUL: + return fc.formatExpr("%s.mul(%e, %e)", fc.typeName(t), e.X, e.Y) + case token.QUO: + return fc.formatExpr("%s.div(%e, %e)", fc.typeName(t), e.X, e.Y) + case token.REM: + return fc.formatExpr("%s.rem(%e, %e)", fc.typeName(t), e.X, e.Y) + case token.AND: + return fc.formatExpr("%s.and(%e, %e)", fc.typeName(t), e.X, e.Y) + case token.OR: + return fc.formatExpr("%s.or(%e, %e)", fc.typeName(t), e.X, e.Y) + case token.XOR: + return fc.formatExpr("%s.xor(%e, %e)", fc.typeName(t), e.X, e.Y) + case token.AND_NOT: + return fc.formatExpr("%s.andNot(%e, %e)", fc.typeName(t), e.X, e.Y) + case token.SHL: + return fc.formatExpr("%s.shl(%e, $flatten64(%e, %s))", fc.typeName(t), e.X, e.Y, fc.typeName(t2)) + case token.SHR: + return fc.formatExpr("%s.shr(%e, $flatten64(%e, %s))", fc.typeName(t), e.X, e.Y, fc.typeName(t2)) + } + } + + if typesutil.IsInterface(t2) || types.Identical(t, types.Typ[types.UntypedNil]) { + t = t2 + } + + if basic, isBasic := t.Underlying().(*types.Basic); isBasic && isNumeric(basic) { + if is64Bit(basic) { + switch e.Op { + case token.MUL: + return fc.formatExpr("$mul64(%e, %e)", e.X, e.Y) + case token.QUO: + return fc.formatExpr("$div64(%e, %e, false)", e.X, e.Y) + case token.REM: + return fc.formatExpr("$div64(%e, %e, true)", e.X, e.Y) + case token.SHL: + return fc.formatExpr("$shiftLeft64(%e, %f)", e.X, e.Y) + case token.SHR: + return fc.formatExpr("$shiftRight%s(%e, %f)", toJavaScriptType(basic), e.X, e.Y) + case token.EQL: + return fc.formatExpr("(%1h === %2h && %1l === %2l)", e.X, e.Y) + case token.LSS: + return fc.formatExpr("(%1h < %2h || (%1h === %2h && %1l < %2l))", e.X, e.Y) + case token.LEQ: + return fc.formatExpr("(%1h < %2h || (%1h === %2h && %1l <= %2l))", e.X, e.Y) + case token.GTR: + return fc.formatExpr("(%1h > %2h || (%1h === %2h && %1l > %2l))", e.X, e.Y) + case token.GEQ: + return fc.formatExpr("(%1h > %2h || (%1h === %2h && %1l >= %2l))", e.X, e.Y) + case token.ADD, token.SUB: + return fc.formatExpr("new %3s(%1h %4t %2h, %1l %4t %2l)", e.X, e.Y, fc.typeName(t), e.Op) + case token.AND, token.OR, token.XOR: + return fc.formatExpr("new %3s(%1h %4t %2h, (%1l %4t %2l) >>> 0)", e.X, e.Y, fc.typeName(t), e.Op) + case token.AND_NOT: + return fc.formatExpr("new %3s(%1h & ~%2h, (%1l & ~%2l) >>> 0)", e.X, e.Y, fc.typeName(t)) + default: + panic(e.Op) + } + } + + if isComplex(basic) { + switch e.Op { + case token.EQL: + return fc.formatExpr("(%1r === %2r && %1i === %2i)", e.X, e.Y) + case token.ADD, token.SUB: + return fc.formatExpr("new %3s(%1r %4t %2r, %1i %4t %2i)", e.X, e.Y, fc.typeName(t), e.Op) + case token.MUL: + return fc.formatExpr("new %3s(%1r * %2r - %1i * %2i, %1r * %2i + %1i * %2r)", e.X, e.Y, fc.typeName(t)) + case token.QUO: + return fc.formatExpr("$divComplex(%e, %e)", e.X, e.Y) + default: + panic(e.Op) + } + } + + switch e.Op { + case token.EQL: + return fc.formatParenExpr("%e === %e", e.X, e.Y) + case token.LSS, token.LEQ, token.GTR, token.GEQ: + return fc.formatExpr("%e %t %e", e.X, e.Op, e.Y) + case token.ADD, token.SUB: + return fc.fixNumber(fc.formatExpr("%e %t %e", e.X, e.Op, e.Y), basic) + case token.MUL: + switch basic.Kind() { + case types.Int32, types.Int: + return fc.formatParenExpr("$imul(%e, %e)", e.X, e.Y) + case types.Uint32, types.Uintptr: + return fc.formatParenExpr("$imul(%e, %e) >>> 0", e.X, e.Y) + } + return fc.fixNumber(fc.formatExpr("%e * %e", e.X, e.Y), basic) + case token.QUO: + if isInteger(basic) { + // cut off decimals + shift := ">>" + if isUnsigned(basic) { + shift = ">>>" + } + return fc.formatExpr(`(%1s = %2e / %3e, (%1s === %1s && %1s !== 1/0 && %1s !== -1/0) ? %1s %4s 0 : $throwRuntimeError("integer divide by zero"))`, fc.newLocalVariable("_q"), e.X, e.Y, shift) + } + if basic.Kind() == types.Float32 { + return fc.fixNumber(fc.formatExpr("%e / %e", e.X, e.Y), basic) + } + return fc.formatExpr("%e / %e", e.X, e.Y) + case token.REM: + return fc.formatExpr(`(%1s = %2e %% %3e, %1s === %1s ? %1s : $throwRuntimeError("integer divide by zero"))`, fc.newLocalVariable("_r"), e.X, e.Y) + case token.SHL, token.SHR: + op := e.Op.String() + if e.Op == token.SHR && isUnsigned(basic) { + op = ">>>" + } + if v := fc.pkgCtx.Types[e.Y].Value; v != nil { + i, _ := constant.Uint64Val(constant.ToInt(v)) + if i >= 32 { + return fc.formatExpr("0") + } + return fc.fixNumber(fc.formatExpr("%e %s %s", e.X, op, strconv.FormatUint(i, 10)), basic) + } + if e.Op == token.SHR && !isUnsigned(basic) { + return fc.fixNumber(fc.formatParenExpr("%e >> $min(%f, 31)", e.X, e.Y), basic) + } + y := fc.newLocalVariable("y") + return fc.fixNumber(fc.formatExpr("(%s = %f, %s < 32 ? (%e %s %s) : 0)", y, e.Y, y, e.X, op, y), basic) + case token.AND, token.OR: + if isUnsigned(basic) { + return fc.formatParenExpr("(%e %t %e) >>> 0", e.X, e.Op, e.Y) + } + return fc.formatParenExpr("%e %t %e", e.X, e.Op, e.Y) + case token.AND_NOT: + return fc.fixNumber(fc.formatParenExpr("%e & ~%e", e.X, e.Y), basic) + case token.XOR: + return fc.fixNumber(fc.formatParenExpr("%e ^ %e", e.X, e.Y), basic) + default: + panic(e.Op) + } + } + + switch e.Op { + case token.ADD, token.LSS, token.LEQ, token.GTR, token.GEQ: + return fc.formatExpr("%e %t %e", e.X, e.Op, e.Y) + case token.LAND: + if fc.Blocking[e.Y] { + skipCase := fc.caseCounter + fc.caseCounter++ + resultVar := fc.newLocalVariable("_v") + fc.Printf("if (!(%s)) { %s = false; $s = %d; continue s; }", fc.translateExpr(e.X), resultVar, skipCase) + fc.Printf("%s = %s; case %d:", resultVar, fc.translateExpr(e.Y), skipCase) + return fc.formatExpr("%s", resultVar) + } + return fc.formatExpr("%e && %e", e.X, e.Y) + case token.LOR: + if fc.Blocking[e.Y] { + skipCase := fc.caseCounter + fc.caseCounter++ + resultVar := fc.newLocalVariable("_v") + fc.Printf("if (%s) { %s = true; $s = %d; continue s; }", fc.translateExpr(e.X), resultVar, skipCase) + fc.Printf("%s = %s; case %d:", resultVar, fc.translateExpr(e.Y), skipCase) + return fc.formatExpr("%s", resultVar) + } + return fc.formatExpr("%e || %e", e.X, e.Y) + case token.EQL: + switch u := t.Underlying().(type) { + case *types.Array, *types.Struct: + return fc.formatExpr("$equal(%e, %e, %s)", e.X, e.Y, fc.typeName(t)) + case *types.Interface: + return fc.formatExpr("$interfaceIsEqual(%s, %s)", fc.translateImplicitConversion(e.X, t), fc.translateImplicitConversion(e.Y, t)) + case *types.Basic: + if isBoolean(u) { + if b, ok := analysis.BoolValue(e.X, fc.pkgCtx.Info.Info); ok && b { + return fc.translateExpr(e.Y) + } + if b, ok := analysis.BoolValue(e.Y, fc.pkgCtx.Info.Info); ok && b { + return fc.translateExpr(e.X) + } + } + } + return fc.formatExpr("%s === %s", fc.translateImplicitConversion(e.X, t), fc.translateImplicitConversion(e.Y, t)) + default: + panic(e.Op) + } +} + +// translateGenericInstance translates a generic function instantiation. +// +// The returned JS expression evaluates into a callable function with type params +// substituted. +func (fc *funcContext) translateGenericInstance(e ast.Expr) *expression { + var identifier *ast.Ident + switch e := e.(type) { + case *ast.Ident: + identifier = e + case *ast.IndexExpr: + identifier = e.X.(*ast.Ident) + case *ast.IndexListExpr: + identifier = e.X.(*ast.Ident) + default: + err := bailout(fmt.Errorf("unexpected generic instantiation expression type %T at %s", e, fc.pkgCtx.fileSet.Position(e.Pos()))) + panic(err) + } + + instance, ok := fc.pkgCtx.Info.Instances[identifier] + if !ok { + err := fmt.Errorf("no matching generic instantiation for %q at %s", identifier, fc.pkgCtx.fileSet.Position(identifier.Pos())) + bailout(err) + } + typeParams := []string{} + for i := 0; i < instance.TypeArgs.Len(); i++ { + t := instance.TypeArgs.At(i) + typeParams = append(typeParams, fc.typeName(t)) + } + o := fc.pkgCtx.Uses[identifier] + return fc.formatExpr("%s(%s)", fc.objectName(o), strings.Join(typeParams, ", ")) +} + func (fc *funcContext) translateCall(e *ast.CallExpr, sig *types.Signature, fun *expression) *expression { args := fc.translateArgs(sig, e.Args, e.Ellipsis.IsValid()) if fc.Blocking[e] { @@ -823,7 +922,7 @@ func (fc *funcContext) translateCall(e *ast.CallExpr, sig *types.Signature, fun fc.caseCounter++ returnVar := "$r" if sig.Results().Len() != 0 { - returnVar = fc.newVariable("_r") + returnVar = fc.newLocalVariable("_r") } fc.Printf("%[1]s = %[2]s(%[3]s); /* */ $s = %[4]d; case %[4]d: if($c) { $c = false; %[1]s = %[1]s.$blk(); } if (%[1]s && %[1]s.$blk !== undefined) { break s; }", returnVar, fun, strings.Join(args, ", "), resumeCase) if sig.Results().Len() != 0 { @@ -855,7 +954,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, isJs = typesutil.IsJsPackage(fc.pkgCtx.Uses[fun.Sel].Pkg()) } sig := fc.pkgCtx.TypeOf(expr.Fun).Underlying().(*types.Signature) - sigTypes := signatureTypes{Sig: sig} + sigTypes := typesutil.Signature{Sig: sig} args := fc.translateArgs(sig, expr.Args, expr.Ellipsis.IsValid()) if !isBuiltin && !isJs { @@ -873,7 +972,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, ellipsis := expr.Ellipsis for i := range expr.Args { - v := fc.newVariable("_arg") + v := fc.newLocalVariable("_arg") vars[i] = v // Subtle: the proxy lambda argument needs to be assigned with the type // that the original function expects, and not with the argument @@ -893,7 +992,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression { sel, _ := fc.pkgCtx.SelectionOf(e) if !sel.Obj().Exported() { - fc.pkgCtx.dependencies[sel.Obj()] = true + fc.DeclareDCEDep(sel.Obj()) } x := e.X @@ -922,7 +1021,7 @@ func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression { _, pointerExpected := methodsRecvType.(*types.Pointer) if !isPointer && pointerExpected { recvType = types.NewPointer(recvType) - x = fc.setType(&ast.UnaryExpr{Op: token.AND, X: x}, recvType) + x = fc.takeAddressExpr(x) } if isPointer && !pointerExpected { x = fc.setType(x, methodsRecvType) @@ -950,26 +1049,16 @@ 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) { - case *types.Slice: - t := fc.typeName(fc.pkgCtx.TypeOf(args[0])) - if len(args) == 3 { - return fc.formatExpr("$makeSlice(%s, %f, %f)", t, args[1], args[2]) - } - return fc.formatExpr("$makeSlice(%s, %f)", t, args[1]) - case *types.Map: - if len(args) == 2 && fc.pkgCtx.Types[args[1]].Value == nil { - return fc.formatExpr(`((%1f < 0 || %1f > 2147483647) ? $throwRuntimeError("makemap: size out of range") : new $global.Map())`, args[1]) - } - return fc.formatExpr("new $global.Map()") - case *types.Chan: - length := "0" - 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) + typeName := fc.typeName(fc.pkgCtx.TypeOf(args[0])) + switch len(args) { + case 1: + return fc.formatExpr("%s.$make()", typeName) + case 2: + return fc.formatExpr("%s.$make(%f)", typeName, args[1]) + case 3: + return fc.formatExpr("%s.$make(%f, %f)", typeName, args[1], args[2]) default: - panic(fmt.Sprintf("Unhandled make type: %T\n", argType)) + panic(fmt.Errorf("builtin make(): invalid number of arguments: %d", len(args))) } case "len": switch argType := fc.pkgCtx.TypeOf(args[0]).Underlying().(type) { @@ -983,6 +1072,8 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args return fc.formatExpr("(%e ? %e.size : 0)", args[0], args[0]) case *types.Chan: return fc.formatExpr("%e.$buffer.length", args[0]) + case *types.Interface: // *types.TypeParam has interface as underlying type. + return fc.formatExpr("%s.$len(%e)", fc.typeName(fc.pkgCtx.TypeOf(args[0])), args[0]) // length of array is constant default: panic(fmt.Sprintf("Unhandled len type: %T\n", argType)) @@ -993,9 +1084,13 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args return fc.formatExpr("%e.$capacity", args[0]) case *types.Pointer: return fc.formatExpr("(%e, %d)", args[0], argType.Elem().(*types.Array).Len()) - // capacity of array is constant + case *types.Array: + // This should never happen™ + panic(fmt.Errorf("array capacity should have been inlined as constant")) + case *types.Interface: // *types.TypeParam has interface as underlying type. + return fc.formatExpr("%s.$cap(%e)", fc.typeName(fc.pkgCtx.TypeOf(args[0])), args[0]) default: - panic(fmt.Sprintf("Unhandled cap type: %T\n", argType)) + panic(fmt.Errorf("unhandled cap type: %T", argType)) } case "panic": return fc.formatExpr("$panic(%s)", fc.translateImplicitConversion(args[0], types.NewInterface(nil, nil))) @@ -1004,11 +1099,11 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args argStr := fc.translateArgs(sig, args, ellipsis) return fc.formatExpr("$appendSlice(%s, %s)", argStr[0], argStr[1]) } - sliceType := sig.Results().At(0).Type().Underlying().(*types.Slice) - return fc.formatExpr("$append(%e, %s)", args[0], strings.Join(fc.translateExprSlice(args[1:], sliceType.Elem()), ", ")) + elType := sig.Params().At(1).Type().(*types.Slice).Elem() + return fc.formatExpr("$append(%e, %s)", args[0], strings.Join(fc.translateExprSlice(args[1:], elType), ", ")) case "delete": args = fc.expandTupleArgs(args) - keyType := fc.pkgCtx.TypeOf(args[0]).Underlying().(*types.Map).Key() + keyType := sig.Params().At(1).Type() return fc.formatExpr( `$mapDelete(%1e, %2s.keyFor(%3s))`, args[0], @@ -1017,10 +1112,8 @@ 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) { - return fc.formatExpr("$copyString(%e, %e)", args[0], args[1]) - } - return fc.formatExpr("$copySlice(%e, %e)", args[0], args[1]) + dst, src := args[0], args[1] + return fc.formatExpr("%s.$copy(%e, %e)", fc.typeName(fc.pkgCtx.TypeOf(src)), dst, src) case "print": args = fc.expandTupleArgs(args) return fc.formatExpr("$print(%s)", strings.Join(fc.translateExprSlice(args, nil), ", ")) @@ -1089,13 +1182,24 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type } } - switch t := desiredType.Underlying().(type) { + _, fromTypeParam := exprType.(*types.TypeParam) + _, toTypeParam := desiredType.(*types.TypeParam) + if fromTypeParam || toTypeParam { + if t, ok := exprType.Underlying().(*types.Basic); ok && t.Kind() == types.UntypedNil { + return fc.formatExpr("%s.zero()", fc.typeName(desiredType)) + } + // Conversion from or to a type param can only be done at runtime, since the + // concrete type is not known to the compiler at compile time. + return fc.formatExpr("%s.convertFrom(%s.wrap(%e))", fc.typeName(desiredType), fc.typeName(exprType), expr) + } + + switch dst := desiredType.Underlying().(type) { case *types.Basic: switch { - case isInteger(t): + case isInteger(dst): basicExprType := exprType.Underlying().(*types.Basic) switch { - case is64Bit(t): + case is64Bit(dst): if !is64Bit(basicExprType) { if basicExprType.Kind() == types.Uintptr { // this might be an Object returned from reflect.Value.Pointer() return fc.formatExpr("new %1s(0, %2e.constructor === Number ? %2e : 1)", fc.typeName(desiredType), expr) @@ -1104,25 +1208,25 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type } return fc.formatExpr("new %1s(%2h, %2l)", fc.typeName(desiredType), expr) case is64Bit(basicExprType): - if !isUnsigned(t) && !isUnsigned(basicExprType) { - return fc.fixNumber(fc.formatParenExpr("%1l + ((%1h >> 31) * 4294967296)", expr), t) + if !isUnsigned(dst) && !isUnsigned(basicExprType) { + return fc.fixNumber(fc.formatParenExpr("%1l + ((%1h >> 31) * 4294967296)", expr), dst) } - return fc.fixNumber(fc.formatExpr("%s.$low", fc.translateExpr(expr)), t) + return fc.fixNumber(fc.formatExpr("%s.$low", fc.translateExpr(expr)), dst) case isFloat(basicExprType): return fc.formatParenExpr("%e >> 0", expr) case types.Identical(exprType, types.Typ[types.UnsafePointer]): return fc.translateExpr(expr) default: - return fc.fixNumber(fc.translateExpr(expr), t) + return fc.fixNumber(fc.translateExpr(expr), dst) } - case isFloat(t): - if t.Kind() == types.Float32 && exprType.Underlying().(*types.Basic).Kind() == types.Float64 { + case isFloat(dst): + if dst.Kind() == types.Float32 && exprType.Underlying().(*types.Basic).Kind() == types.Float64 { return fc.formatExpr("$fround(%e)", expr) } return fc.formatExpr("%f", expr) - case isComplex(t): + case isComplex(dst): return fc.formatExpr("new %1s(%2r, %2i)", fc.typeName(desiredType), expr) - case isString(t): + case isString(dst): value := fc.translateExpr(expr) switch et := exprType.Underlying().(type) { case *types.Basic: @@ -1141,7 +1245,7 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type default: panic(fmt.Sprintf("Unhandled conversion: %v\n", et)) } - case t.Kind() == types.UnsafePointer: + case dst.Kind() == types.UnsafePointer: if unary, isUnary := expr.(*ast.UnaryExpr); isUnary && unary.Op == token.AND { if indexExpr, isIndexExpr := unary.X.(*ast.IndexExpr); isIndexExpr { return fc.formatExpr("$sliceToNativeArray(%s)", fc.translateConversionToSlice(indexExpr.X, types.NewSlice(types.Typ[types.Uint8]))) @@ -1152,8 +1256,8 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type } if ptr, isPtr := fc.pkgCtx.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") + array := fc.newLocalVariable("_array") + target := fc.newLocalVariable("_struct") fc.Printf("%s = new Uint8Array(%d);", array, sizes32.Sizeof(s)) fc.Delayed(func() { fc.Printf("%s = %s, %s;", target, fc.translateExpr(expr), fc.loadStruct(array, target, s)) @@ -1172,7 +1276,7 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type switch et := exprType.Underlying().(type) { case *types.Basic: if isString(et) { - if types.Identical(t.Elem().Underlying(), types.Typ[types.Rune]) { + if types.Identical(dst.Elem().Underlying(), types.Typ[types.Rune]) { return fc.formatExpr("new %s($stringToRunes(%e))", fc.typeName(desiredType), expr) } return fc.formatExpr("new %s($stringToBytes(%e))", fc.typeName(desiredType), expr) @@ -1188,7 +1292,7 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type break } - switch ptrElType := t.Elem().Underlying().(type) { + switch ptrElType := dst.Elem().Underlying().(type) { case *types.Array: // (*[N]T)(expr) — converting expr to a pointer to an array. if _, ok := exprType.Underlying().(*types.Slice); ok { return fc.formatExpr("$sliceToGoArray(%e, %s)", expr, fc.typeName(desiredType)) @@ -1201,9 +1305,9 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type // struct pointer when handling syscalls. // TODO(nevkontakte): Add a runtime assertion that the unsafe.Pointer is // indeed pointing at a byte array. - array := fc.newVariable("_array") - target := fc.newVariable("_struct") - return fc.formatExpr("(%s = %e, %s = %e, %s, %s)", array, expr, target, fc.zeroValue(t.Elem()), fc.loadStruct(array, target, ptrElType), target) + array := fc.newLocalVariable("_array") + target := fc.newLocalVariable("_struct") + return fc.formatExpr("(%s = %e, %s = %e, %s, %s)", array, expr, target, fc.zeroValue(dst.Elem()), fc.loadStruct(array, target, ptrElType), target) } // Convert between structs of different types but identical layouts, // for example: @@ -1224,9 +1328,9 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type // type iPtr *int; var c int = 42; println((iPtr)(&c)); // TODO(nevkontakte): Are there any other cases that fall into this case? exprTypeElem := exprType.Underlying().(*types.Pointer).Elem() - ptrVar := fc.newVariable("_ptr") - getterConv := fc.translateConversion(fc.setType(&ast.StarExpr{X: fc.newIdent(ptrVar, exprType)}, exprTypeElem), t.Elem()) - setterConv := fc.translateConversion(fc.newIdent("$v", t.Elem()), exprTypeElem) + ptrVar := fc.newLocalVariable("_ptr") + getterConv := fc.translateConversion(fc.setType(&ast.StarExpr{X: fc.newIdent(ptrVar, exprType)}, exprTypeElem), dst.Elem()) + setterConv := fc.translateConversion(fc.newIdent("$v", dst.Elem()), exprTypeElem) return fc.formatExpr("(%1s = %2e, new %3s(function() { return %4s; }, function($v) { %1s.$set(%5s); }, %1s.$target))", ptrVar, expr, fc.typeName(desiredType), getterConv, setterConv) case *types.Interface: @@ -1271,7 +1375,9 @@ func (fc *funcContext) translateImplicitConversion(expr ast.Expr, desiredType ty // wrap JS object into js.Object struct when converting to interface return fc.formatExpr("new $jsObjectPtr(%e)", expr) } - if isWrapped(exprType) { + if typesutil.IsGeneric(exprType) { + return fc.formatExpr("%s.wrap(%e)", fc.typeName(exprType), expr) + } else if isWrapped(exprType) { return fc.formatExpr("new %s(%e)", fc.typeName(exprType), expr) } if _, isStruct := exprType.Underlying().(*types.Struct); isStruct { @@ -1291,7 +1397,7 @@ func (fc *funcContext) translateConversionToSlice(expr ast.Expr, desiredType typ } func (fc *funcContext) loadStruct(array, target string, s *types.Struct) string { - view := fc.newVariable("_view") + view := fc.newLocalVariable("_view") code := fmt.Sprintf("%s = new DataView(%s.buffer, %s.byteOffset)", view, array, array) var fields []*types.Var var collectFields func(s *types.Struct, path string) @@ -1421,7 +1527,7 @@ func (fc *funcContext) formatExprInternal(format string, a []interface{}, parens out.WriteByte('(') parens = false } - v := fc.newVariable("x") + v := fc.newLocalVariable("x") out.WriteString(v + " = " + fc.translateExpr(e.(ast.Expr)).String() + ", ") vars[i] = v } @@ -1461,11 +1567,16 @@ 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 t, ok := fc.pkgCtx.TypeOf(e).Underlying().(*types.Basic); ok && is64Bit(t) { out.WriteString("$flatten64(") writeExpr("") out.WriteString(")") return + } else if t, ok := fc.pkgCtx.TypeOf(e).(*types.TypeParam); ok { + out.WriteString("$flatten64(") + writeExpr("") + fmt.Fprintf(out, ", %s)", fc.typeName(t)) + return } writeExpr("") case 'h': diff --git a/compiler/filter/assign.go b/compiler/filter/assign.go index 2681d4c6a..281cedf87 100644 --- a/compiler/filter/assign.go +++ b/compiler/filter/assign.go @@ -71,7 +71,7 @@ func Assign(stmt ast.Stmt, info *types.Info, pkg *types.Package) ast.Stmt { return e default: - tmpVar := astutil.NewIdent(name, info.TypeOf(e), info, pkg) + tmpVar := astutil.NewVarIdent(name, info.TypeOf(e), info, pkg) list = append(list, &ast.AssignStmt{ Lhs: []ast.Expr{tmpVar}, Tok: token.DEFINE, diff --git a/compiler/functions.go b/compiler/functions.go new file mode 100644 index 000000000..2625a02b1 --- /dev/null +++ b/compiler/functions.go @@ -0,0 +1,399 @@ +package compiler + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + "go/types" + "sort" + "strings" + + "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/typesutil" +) + +// functions.go contains logic responsible for translating top-level functions +// and function literals. + +// newFunctionContext creates a new nested context for a function corresponding +// to the provided info. +func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, o *types.Func) *funcContext { + if info == nil { + panic(errors.New("missing *analysis.FuncInfo")) + } + if o == nil { + panic(errors.New("missing *types.Func")) + } + + c := &funcContext{ + FuncInfo: info, + funcObject: o, + pkgCtx: fc.pkgCtx, + genericCtx: fc.genericCtx, + parent: fc, + sigTypes: &typesutil.Signature{Sig: o.Type().(*types.Signature)}, + allVars: make(map[string]int, len(fc.allVars)), + localVars: []string{}, + flowDatas: map[*types.Label]*flowData{nil: {}}, + caseCounter: 1, + labelCases: make(map[*types.Label]int), + } + // Register all variables from the parent context to avoid shadowing. + for k, v := range fc.allVars { + c.allVars[k] = v + } + + // Synthesize an identifier by which the function may reference itself. Since + // it appears in the stack trace, it's useful to include the receiver type in + // it. + funcRef := o.Name() + if typeName := c.sigTypes.RecvTypeName(); typeName != "" { + funcRef = typeName + midDot + funcRef + } + c.funcRef = c.newVariable(funcRef, varPackage) + + // If the function has type parameters, create a new generic context for it. + if c.sigTypes.IsGeneric() { + c.genericCtx = &genericCtx{} + } + + return c +} + +// namedFuncContext creates a new funcContext for a named Go function +// (standalone or method). +func (fc *funcContext) namedFuncContext(fun *ast.FuncDecl) *funcContext { + o := fc.pkgCtx.Defs[fun.Name].(*types.Func) + info := fc.pkgCtx.FuncDeclInfos[o] + c := fc.nestedFunctionContext(info, o) + + return c +} + +// literalFuncContext creates a new funcContext for a function literal. Since +// go/types doesn't generate *types.Func objects for function literals, we +// generate a synthetic one for it. +func (fc *funcContext) literalFuncContext(fun *ast.FuncLit) *funcContext { + info := fc.pkgCtx.FuncLitInfos[fun] + sig := fc.pkgCtx.TypeOf(fun).(*types.Signature) + o := types.NewFunc(fun.Pos(), fc.pkgCtx.Pkg, fc.newLitFuncName(), sig) + + c := fc.nestedFunctionContext(info, o) + return c +} + +// translateTopLevelFunction translates a top-level function declaration +// (standalone function or method) into a corresponding JS function. +// +// Returns a string with JavaScript statements that define the function or +// method. For generic functions it returns a generic factory function, which +// instantiates the actual function at runtime given type parameters. For +// methods it returns declarations for both value- and pointer-receiver (if +// appropriate). +func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl) []byte { + if fun.Recv == nil { + return fc.translateStandaloneFunction(fun) + } + return fc.translateMethod(fun) +} + +// translateStandaloneFunction translates a package-level function. +// +// It returns JS statements which define the corresponding function in a +// package context. Exported functions are also assigned to the `$pkg` object. +func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl) []byte { + o := fc.pkgCtx.Defs[fun.Name].(*types.Func) + if fun.Recv != nil { + panic(fmt.Errorf("expected standalone function, got method: %s", o)) + } + + lvalue := fc.objectName(o) + if fun.Body == nil { + return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fc.unimplementedFunction(o))) + } + body := fc.namedFuncContext(fun).translateFunctionBody(fun.Type, nil, fun.Body) + + code := &bytes.Buffer{} + fmt.Fprintf(code, "\t%s = %s;\n", lvalue, body) + if fun.Name.IsExported() { + fmt.Fprintf(code, "\t$pkg.%s = %s;\n", encodeIdent(fun.Name.Name), lvalue) + } + return code.Bytes() +} + +// translateMethod translates a named type method. +// +// It returns one or more JS statements, which define the methods. Methods with +// non-pointer receiver are automatically defined for the pointer-receiver type. +func (fc *funcContext) translateMethod(fun *ast.FuncDecl) []byte { + if fun.Recv == nil { + panic(fmt.Errorf("expected a method, got %v", fun)) + } + + o := fc.pkgCtx.Defs[fun.Name].(*types.Func) + var recv *ast.Ident + if fun.Recv.List[0].Names != nil { + recv = fun.Recv.List[0].Names[0] + } + nestedFC := fc.namedFuncContext(fun) + + // primaryFunction generates a JS function equivalent of the current Go function + // and assigns it to the JS expression defined by lvalue. + primaryFunction := func(lvalue string) []byte { + if fun.Body == nil { + return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fc.unimplementedFunction(o))) + } + + funDef := nestedFC.translateFunctionBody(fun.Type, recv, fun.Body) + return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, funDef)) + } + + recvType := nestedFC.sigTypes.Sig.Recv().Type() + ptr, isPointer := recvType.(*types.Pointer) + namedRecvType, _ := recvType.(*types.Named) + if isPointer { + namedRecvType = ptr.Elem().(*types.Named) + } + typeName := fc.objectName(namedRecvType.Obj()) + funName := fc.methodName(o) + + // Objects the method should be assigned to. + prototypeVar := fmt.Sprintf("%s.prototype.%s", typeName, funName) + ptrPrototypeVar := fmt.Sprintf("$ptrType(%s).prototype.%s", typeName, funName) + isGeneric := nestedFC.sigTypes.IsGeneric() + if isGeneric { + // Generic method factories are assigned to the generic type factory + // properties, to be invoked at type construction time rather than method + // call time. + prototypeVar = fmt.Sprintf("%s.methods.%s", typeName, funName) + ptrPrototypeVar = fmt.Sprintf("%s.ptrMethods.%s", typeName, funName) + } + + // Methods with pointer receivers are defined only on the pointer type. + if isPointer { + return primaryFunction(ptrPrototypeVar) + } + + // Methods with non-pointer receivers must be defined both for the pointer + // and non-pointer types. To minimize generated code size, we generate a + // complete implementation for only one receiver (non-pointer for most types) + // and define a proxy function on the other, which converts the receiver type + // and forwards the call to the primary implementation. + proxyFunction := func(lvalue, receiver string) []byte { + params := strings.Join(nestedFC.funcParamVars(fun.Type), ", ") + fun := fmt.Sprintf("function(%s) { return %s.%s(%s); }", params, receiver, funName, params) + if isGeneric { + // For a generic function, we wrap the proxy function in a trivial generic + // factory function for consistency. It is the same for any possible type + // arguments, so we simply ignore them. + fun = fmt.Sprintf("function() { return %s; }", fun) + } + return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fun)) + } + + // Structs are a special case: because of the JS's reference semantics, the + // actual JS objects correspond to pointer-to-struct types and struct value + // types are emulated via cloning. Because of that, the real method + // implementation is defined on the pointer type. + if _, isStruct := namedRecvType.Underlying().(*types.Struct); isStruct { + code := bytes.Buffer{} + code.Write(primaryFunction(ptrPrototypeVar)) + code.Write(proxyFunction(prototypeVar, "this.$val")) + return code.Bytes() + } + + proxyRecvExpr := "this.$get()" + if typesutil.IsGeneric(recvType) { + proxyRecvExpr = fmt.Sprintf("%s.wrap(%s)", typeName, proxyRecvExpr) + } else if isWrapped(recvType) { + proxyRecvExpr = fmt.Sprintf("new %s(%s)", typeName, proxyRecvExpr) + } + code := bytes.Buffer{} + code.Write(primaryFunction(prototypeVar)) + code.Write(proxyFunction(ptrPrototypeVar, proxyRecvExpr)) + return code.Bytes() +} + +// unimplementedFunction returns a JS function expression for a Go function +// without a body, which would throw an exception if called. +// +// In Go such functions are either used with a //go:linkname directive or with +// assembler intrinsics, only former of which is supported by GopherJS. +func (fc *funcContext) unimplementedFunction(o *types.Func) string { + return fmt.Sprintf("function() {\n\t\t$throwRuntimeError(\"native function not implemented: %s\");\n\t}", o.FullName()) +} + +func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt) string { + prevEV := fc.pkgCtx.escapingVars + + params := fc.funcParamVars(typ) + + bodyOutput := string(fc.CatchOutput(fc.bodyIndent(), func() { + if len(fc.Blocking) != 0 { + fc.pkgCtx.Scopes[body] = fc.pkgCtx.Scopes[typ] + fc.handleEscapingVars(body) + } + + if fc.sigTypes != nil && fc.sigTypes.HasNamedResults() { + fc.resultNames = make([]ast.Expr, fc.sigTypes.Sig.Results().Len()) + for i := 0; i < fc.sigTypes.Sig.Results().Len(); i++ { + result := fc.sigTypes.Sig.Results().At(i) + fc.Printf("%s = %s;", fc.objectName(result), fc.translateExpr(fc.zeroValue(result.Type())).String()) + id := ast.NewIdent("") + fc.pkgCtx.Uses[id] = result + fc.resultNames[i] = fc.setType(id, result.Type()) + } + } + + if recv != nil && !isBlank(recv) { + this := "this" + if isWrapped(fc.pkgCtx.TypeOf(recv)) { + this = "this.$val" // Unwrap receiver value. + } + fc.Printf("%s = %s;", fc.translateExpr(recv), this) + } + + fc.translateStmtList(body.List) + if len(fc.Flattened) != 0 && !astutil.EndsWithReturn(body.List) { + fc.translateStmt(&ast.ReturnStmt{}, nil) + } + })) + + sort.Strings(fc.localVars) + + var prefix, suffix string + + if len(fc.Flattened) != 0 { + fc.localVars = append(fc.localVars, "$s") + prefix = prefix + " $s = $s || 0;" + } + + if fc.HasDefer { + fc.localVars = append(fc.localVars, "$deferred") + suffix = " }" + suffix + if len(fc.Blocking) != 0 { + suffix = " }" + suffix + } + } + + localVarDefs := "" // Function-local var declaration at the top. + + if len(fc.Blocking) != 0 { + localVars := append([]string{}, fc.localVars...) + // There are several special variables involved in handling blocking functions: + // $r is sometimes used as a temporary variable to store blocking call result. + // $c indicates that a function is being resumed after a blocking call when set to true. + // $f is an object used to save and restore function context for blocking calls. + localVars = append(localVars, "$r") + // funcRef identifies the function object itself, so it doesn't need to be saved + // or restored. + localVars = removeMatching(localVars, fc.funcRef) + // If a blocking function is being resumed, initialize local variables from the saved context. + localVarDefs = fmt.Sprintf("var {%s, $c} = $restore(this, {%s});\n", strings.Join(localVars, ", "), strings.Join(params, ", ")) + // If the function gets blocked, save local variables for future. + saveContext := fmt.Sprintf("var $f = {$blk: %s, $c: true, %s};", fc.funcRef, strings.Join(localVars, ", ")) + + suffix = " " + saveContext + "return $f;" + suffix + } else if len(fc.localVars) > 0 { + // Non-blocking functions simply declare local variables with no need for restore support. + localVarDefs = fmt.Sprintf("var %s;\n", strings.Join(fc.localVars, ", ")) + } + + if fc.HasDefer { + prefix = prefix + " var $err = null; try {" + deferSuffix := " } catch(err) { $err = err;" + if len(fc.Blocking) != 0 { + deferSuffix += " $s = -1;" + } + if fc.resultNames == nil && fc.sigTypes.HasResults() { + deferSuffix += fmt.Sprintf(" return%s;", fc.translateResults(nil)) + } + deferSuffix += " } finally { $callDeferred($deferred, $err);" + if fc.resultNames != nil { + deferSuffix += fmt.Sprintf(" if (!$curGoroutine.asleep) { return %s; }", fc.translateResults(fc.resultNames)) + } + if len(fc.Blocking) != 0 { + deferSuffix += " if($curGoroutine.asleep) {" + } + suffix = deferSuffix + suffix + } + + if len(fc.Flattened) != 0 { + prefix = prefix + " s: while (true) { switch ($s) { case 0:" + suffix = " } return; }" + suffix + } + + if fc.HasDefer { + prefix = prefix + " $deferred = []; $curGoroutine.deferStack.push($deferred);" + } + + if prefix != "" { + bodyOutput = fc.Indentation(fc.bodyIndent()) + "/* */" + prefix + "\n" + bodyOutput + } + if suffix != "" { + bodyOutput = bodyOutput + fc.Indentation(fc.bodyIndent()) + "/* */" + suffix + "\n" + } + if localVarDefs != "" { + bodyOutput = fc.Indentation(fc.bodyIndent()) + localVarDefs + bodyOutput + } + + fc.pkgCtx.escapingVars = prevEV + + if !fc.sigTypes.IsGeneric() { + return fmt.Sprintf("function %s(%s) {\n%s%s}", fc.funcRef, strings.Join(params, ", "), bodyOutput, fc.Indentation(0)) + } + + // For generic function, funcRef refers to the generic factory function, + // allocate a separate variable for a function instance. + instanceVar := fc.newVariable("instance", varGenericFactory) + + // Generic functions are generated as factories to allow passing type parameters + // from the call site. + // TODO(nevkontakte): Cache function instances for a given combination of type + // parameters. + typeParams := fc.typeParamVars(fc.sigTypes.Sig.TypeParams()) + typeParams = append(typeParams, fc.typeParamVars(fc.sigTypes.Sig.RecvTypeParams())...) + + // anonymous types + typesInit := strings.Builder{} + for _, t := range fc.genericCtx.anonTypes.Ordered() { + fmt.Fprintf(&typesInit, "%svar %s = $%sType(%s);\n", fc.Indentation(1), t.Name(), strings.ToLower(typeKind(t.Type())[5:]), fc.initArgs(t.Type())) + } + + code := &strings.Builder{} + fmt.Fprintf(code, "function(%s){\n", strings.Join(typeParams, ", ")) + fmt.Fprintf(code, "%s", typesInit.String()) + fmt.Fprintf(code, "%sconst %s = function %s(%s) {\n", fc.Indentation(1), instanceVar, fc.funcRef, strings.Join(params, ", ")) + fmt.Fprintf(code, "%s", bodyOutput) + fmt.Fprintf(code, "%s};\n", fc.Indentation(1)) + fmt.Fprintf(code, "%sreturn %s;\n", fc.Indentation(1), instanceVar) + fmt.Fprintf(code, "%s}", fc.Indentation(0)) + return code.String() +} + +// funcParamVars returns a list of JS variables corresponding to the function +// parameters in the order they are defined in the signature. Unnamed or blank +// parameters are assigned unique synthetic names. +// +// Note that JS doesn't allow blank or repeating function argument names, so +// we must assign unique names to all such blank variables. +func (fc *funcContext) funcParamVars(typ *ast.FuncType) []string { + var params []string + for _, param := range typ.Params.List { + if len(param.Names) == 0 { + params = append(params, fc.newBlankVariable(param.Pos())) + continue + } + for _, ident := range param.Names { + if isBlank(ident) { + params = append(params, fc.newBlankVariable(ident.Pos())) + continue + } + params = append(params, fc.objectName(fc.pkgCtx.Defs[ident])) + } + } + + return params +} diff --git a/compiler/linkname.go b/compiler/linkname.go index ae1e3ea2b..58b19f0f9 100644 --- a/compiler/linkname.go +++ b/compiler/linkname.go @@ -166,7 +166,7 @@ func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]Go } } - return directives, errs.Normalize() + return directives, errs.ErrOrNil() } // goLinknameSet is a utility that enables quick lookup of whether a decl is diff --git a/compiler/natives/src/reflect/reflect.go b/compiler/natives/src/reflect/reflect.go index 3261d70ca..d97094e03 100644 --- a/compiler/natives/src/reflect/reflect.go +++ b/compiler/natives/src/reflect/reflect.go @@ -1737,13 +1737,14 @@ func methodNameSkip() string { } // Function name extracted from the call stack can be different from vanilla // Go. Here we try to fix stuff like "Object.$packages.reflect.Q.ptr.SetIterKey" - // into "Value.SetIterKey". + // or plain "SetIterKey" into "Value.SetIterKey". // This workaround may become obsolete after https://github.com/gopherjs/gopherjs/issues/1085 // is resolved. name := f.Name() idx := len(name) - 1 for idx > 0 { if name[idx] == '.' { + idx++ break } idx-- @@ -1751,7 +1752,7 @@ func methodNameSkip() string { if idx < 0 { return name } - return "Value" + name[idx:] + return "Value." + name[idx:] } func verifyNotInHeapPtr(p uintptr) bool { diff --git a/compiler/package.go b/compiler/package.go index 93c22f1c5..b9a61e14e 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -5,19 +5,14 @@ import ( "encoding/json" "fmt" "go/ast" - "go/constant" - "go/scanner" "go/token" "go/types" - "sort" "strings" "time" "github.com/gopherjs/gopherjs/compiler/analysis" - "github.com/gopherjs/gopherjs/compiler/astutil" - "github.com/neelance/astrewrite" + "github.com/gopherjs/gopherjs/compiler/typesutil" "golang.org/x/tools/go/gcexportdata" - "golang.org/x/tools/go/types/typeutil" ) // pkgContext maintains compiler context for a specific package. @@ -25,18 +20,39 @@ type pkgContext struct { *analysis.Info additionalSelections map[*ast.SelectorExpr]selection - typeNames []*types.TypeName - pkgVars map[string]string - objectNames map[types.Object]string - varPtrNames map[*types.Var]string - anonTypes []*types.TypeName - anonTypeMap typeutil.Map - escapingVars map[*types.Var]bool - indentation int - dependencies map[types.Object]bool - minify bool - fileSet *token.FileSet - errList ErrorList + // List of type names declared in the package, including those defined inside + // functions. + typeNames []*types.TypeName + // Mapping from package import paths to JS variables that were assigned to an + // imported package and can be used to access it. + pkgVars map[string]string + // Mapping from a named Go object (e.g. type, func, var...) to a JS variable + // name assigned to them. + objectNames map[types.Object]string + varPtrNames map[*types.Var]string + // Mapping from a `_` variable to a synthetic JS variable name representing + // it. Map is keyed by the variable position (we can't use *ast.Ident because + // nameless function parameters may not have it). + blankVarNames map[token.Pos]string + // Mapping between methods' type parameters and their canonical counterparts + // on the receiver types. This ensures we always use the same identifier for + // the type parameter even if a method declaration gives it a different name + // compared to the receiver type declaration: + // type A[T any] struct{} + // func (a A[T1]) M() {} + canonicalTypeParams typesutil.CanonicalTypeParamMap + anonTypes typesutil.AnonymousTypes + escapingVars map[*types.Var]bool + indentation int + dependencies map[types.Object]bool + minify bool + fileSet *token.FileSet + errList ErrorList +} + +// IsMain returns true if this is the main package of the program. +func (pc *pkgContext) IsMain() bool { + return pc.Pkg.Name() == "main" } func (p *pkgContext) SelectionOf(e *ast.SelectorExpr) (selection, bool) { @@ -49,6 +65,14 @@ func (p *pkgContext) SelectionOf(e *ast.SelectorExpr) (selection, bool) { return nil, false } +// genericCtx contains compiler context for a generic function or type. +// +// It is used to accumulate information about types and objects that depend on +// type parameters and must be constructed in a generic factory function. +type genericCtx struct { + anonTypes typesutil.AnonymousTypes +} + type selection interface { Kind() types.SelectionKind Recv() types.Type @@ -71,22 +95,96 @@ 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?). +// funcContext maintains compiler context for a specific function. +// +// An instance of this type roughly corresponds to a lexical scope for generated +// JavaScript code (as defined for `var` declarations). type funcContext struct { *analysis.FuncInfo - pkgCtx *pkgContext - parent *funcContext - sig *types.Signature - allVars map[string]int - localVars []string - resultNames []ast.Expr - flowDatas map[*types.Label]*flowData - caseCounter int - labelCases map[*types.Label]int - output []byte + // Function object this context corresponds to, or nil if the context is + // top-level or doesn't correspond to a function. For function literals, this + // is a synthetic object that assigns a unique identity to the function. + funcObject *types.Func + // JavaScript identifier assigned to the function object (the word after the + // "function" keyword in the generated code). This identifier can be used + // within the function scope to reference the function object. It will also + // appear in the stack trace. + funcRef string + // Surrounding package context. + pkgCtx *pkgContext + // Surrounding generic function context. nil if non-generic code. + genericCtx *genericCtx + // Function context, surrounding this function definition. For package-level + // functions or methods it is the package-level function context (even though + // it technically doesn't correspond to a function). nil for the package-level + // function context. + parent *funcContext + // Information about function signature types. nil for the package-level + // function context. + sigTypes *typesutil.Signature + // All variable names available in the current function scope. The key is a Go + // variable name and the value is the number of synonymous variable names + // visible from this scope (e.g. due to shadowing). This number is used to + // avoid conflicts when assigning JS variable names for Go variables. + allVars map[string]int + // Local JS variable names defined within this function context. This list + // contains JS variable names assigned to Go variables, as well as other + // auxiliary variables the compiler needs. It is used to generate `var` + // declaration at the top of the function, as well as context save/restore. + localVars []string + // AST expressions representing function's named return values. nil if the + // function has no return values or they are not named. + resultNames []ast.Expr + // Function's internal control flow graph used for generation of a "flattened" + // version of the function when the function is blocking or uses goto. + // TODO(nevkontakte): Describe the exact semantics of this map. + flowDatas map[*types.Label]*flowData + // Number of control flow blocks in a "flattened" function. + caseCounter int + // A mapping from Go labels statements (e.g. labelled loop) to the flow block + // id corresponding to it. + labelCases map[*types.Label]int + // Generated code buffer for the current function. + output []byte + // Generated code that should be emitted at the end of the JS statement (?). delayedOutput []byte - posAvailable bool - pos token.Pos + // Set to true if source position is available and should be emitted for the + // source map. + posAvailable bool + // Current position in the Go source code. + pos token.Pos + // Number of function literals encountered within the current function context. + funcLitCounter int +} + +// newRootCtx creates a new package-level instance of a functionContext object. +func newRootCtx(srcs sources, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(*types.Func) bool, minify bool) *funcContext { + pkgInfo := analysis.AnalyzePkg(srcs.Files, srcs.FileSet, typesInfo, typesPkg, isBlocking) + rootCtx := &funcContext{ + parent: nil, // Package-level context has no parent. + FuncInfo: pkgInfo.InitFuncInfo, + pkgCtx: &pkgContext{ + Info: pkgInfo, + additionalSelections: make(map[*ast.SelectorExpr]selection), + + pkgVars: make(map[string]string), + objectNames: make(map[types.Object]string), + varPtrNames: make(map[*types.Var]string), + blankVarNames: make(map[token.Pos]string), + escapingVars: make(map[*types.Var]bool), + indentation: 1, + minify: minify, + fileSet: srcs.FileSet, + }, + allVars: make(map[string]int), + flowDatas: map[*types.Label]*flowData{nil: {}}, + caseCounter: 1, + labelCases: make(map[*types.Label]int), + } + for name := range reservedKeywords { + rootCtx.allVars[name] = 1 + } + return rootCtx } type flowData struct { @@ -95,34 +193,41 @@ type flowData struct { endCase int } +// ImportContext provides access to information about imported packages. type ImportContext struct { + // Mapping for an absolute import path to the package type information. Packages map[string]*types.Package - Import func(string) (*Archive, error) -} - -// packageImporter implements go/types.Importer interface. -type packageImporter struct { - importContext *ImportContext - importError *error // A pointer to importError in Compile. + // Import returns a previously compiled Archive for a dependency package. If + // the Import() call was successful, the corresponding entry must be added to + // the Packages map. + Import func(importPath string) (*Archive, error) } -func (pi packageImporter) Import(path string) (*types.Package, error) { - if path == "unsafe" { - return types.Unsafe, nil - } - - a, err := pi.importContext.Import(path) +// isBlocking returns true if an _imported_ function is blocking. It will panic +// if the function decl is not found in the imported package or the package +// hasn't been compiled yet. +// +// Note: see analysis.FuncInfo.Blocking if you need to determine if a function +// in the _current_ package is blocking. Usually available via functionContext +// object. +func (ic *ImportContext) isBlocking(f *types.Func) bool { + archive, err := ic.Import(f.Pkg().Path()) if err != nil { - if *pi.importError == nil { - // If import failed, show first error of import only (https://github.com/gopherjs/gopherjs/issues/119). - *pi.importError = err + panic(err) + } + fullName := f.FullName() + for _, d := range archive.Declarations { + if string(d.FullName) == fullName { + return d.Blocking } - return nil, err } - - return pi.importContext.Packages[a.ImportPath], nil + panic(bailout(fmt.Errorf("can't determine if function %s is blocking: decl not found in package archive", fullName))) } +// Compile the provided Go sources as a single package. +// +// Import path must be the absolute import path for a package. Provided sources +// are always sorted by name to ensure reproducible JavaScript output. func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, importContext *ImportContext, minify bool) (_ *Archive, err error) { defer func() { e := recover() @@ -138,473 +243,78 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor err = bailout(fmt.Errorf("unexpected compiler panic while building package %q: %v", importPath, e)) }() - // Files must be in the same order to get reproducible JS - sort.Slice(files, func(i, j int) bool { - return fileSet.File(files[i].Pos()).Name() > fileSet.File(files[j].Pos()).Name() - }) - - typesInfo := &types.Info{ - Types: make(map[ast.Expr]types.TypeAndValue), - Defs: make(map[*ast.Ident]types.Object), - Uses: make(map[*ast.Ident]types.Object), - Implicits: make(map[ast.Node]types.Object), - Selections: make(map[*ast.SelectorExpr]*types.Selection), - Scopes: make(map[ast.Node]*types.Scope), - } - - var errList ErrorList + srcs := sources{ + ImportPath: importPath, + Files: files, + FileSet: fileSet, + }.Sort() - // Extract all go:linkname compiler directives from the package source. - goLinknames := []GoLinkname{} - for _, file := range files { - found, err := parseGoLinknames(fileSet, importPath, file) - if err != nil { - if errs, ok := err.(ErrorList); ok { - errList = append(errList, errs...) - } else { - errList = append(errList, err) - } - } - goLinknames = append(goLinknames, found...) - } - - var importError error - var previousErr error - config := &types.Config{ - Importer: packageImporter{ - importContext: importContext, - importError: &importError, - }, - Sizes: sizes32, - Error: func(err error) { - if previousErr != nil && previousErr.Error() == err.Error() { - return - } - errList = append(errList, err) - previousErr = err - }, - } - typesPkg, err := config.Check(importPath, fileSet, files, typesInfo) - if importError != nil { - return nil, importError - } - if errList != nil { - if len(errList) > 10 { - pos := token.NoPos - if last, ok := errList[9].(types.Error); ok { - pos = last.Pos - } - errList = append(errList[:10], types.Error{Fset: fileSet, Pos: pos, Msg: "too many errors"}) - } - return nil, errList - } + typesInfo, typesPkg, err := srcs.TypeCheck(importContext) if err != nil { return nil, err } - importContext.Packages[importPath] = typesPkg + importContext.Packages[srcs.ImportPath] = typesPkg - exportData := new(bytes.Buffer) - if err := gcexportdata.Write(exportData, nil, typesPkg); err != nil { - return nil, fmt.Errorf("failed to write export data: %v", err) - } - encodedFileSet := new(bytes.Buffer) - if err := fileSet.Write(json.NewEncoder(encodedFileSet).Encode); err != nil { + // Extract all go:linkname compiler directives from the package source. + goLinknames, err := srcs.ParseGoLinknames() + if err != nil { return nil, err } - simplifiedFiles := make([]*ast.File, len(files)) - for i, file := range files { - simplifiedFiles[i] = astrewrite.Simplify(file, typesInfo, false) - } + srcs = srcs.Simplified(typesInfo) - isBlocking := func(f *types.Func) bool { - archive, err := importContext.Import(f.Pkg().Path()) - if err != nil { - panic(err) - } - fullName := f.FullName() - for _, d := range archive.Declarations { - if string(d.FullName) == fullName { - return d.Blocking - } - } - panic(fullName) - } - pkgInfo := analysis.AnalyzePkg(simplifiedFiles, fileSet, typesInfo, typesPkg, isBlocking) - funcCtx := &funcContext{ - FuncInfo: pkgInfo.InitFuncInfo, - pkgCtx: &pkgContext{ - Info: pkgInfo, - additionalSelections: make(map[*ast.SelectorExpr]selection), + rootCtx := newRootCtx(srcs, typesInfo, typesPkg, importContext.isBlocking, minify) - 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, - }, - allVars: make(map[string]int), - flowDatas: map[*types.Label]*flowData{nil: {}}, - caseCounter: 1, - labelCases: make(map[*types.Label]int), - } - for name := range reservedKeywords { - funcCtx.allVars[name] = 1 - } + importedPaths, importDecls := rootCtx.importDecls() - // imports - var importDecls []*Decl - var importedPaths []string - for _, importedPkg := range typesPkg.Imports() { - if importedPkg == types.Unsafe { - // Prior to Go 1.9, unsafe import was excluded by Imports() method, - // but now we do it here to maintain previous behavior. - continue - } - funcCtx.pkgCtx.pkgVars[importedPkg.Path()] = funcCtx.newVariableWithLevel(importedPkg.Name(), true) - importedPaths = append(importedPaths, importedPkg.Path()) - } - sort.Strings(importedPaths) - for _, impPath := range importedPaths { - id := funcCtx.newIdent(fmt.Sprintf(`%s.$init`, funcCtx.pkgCtx.pkgVars[impPath]), types.NewSignature(nil, nil, nil, false)) - call := &ast.CallExpr{Fun: id} - funcCtx.Blocking[call] = true - funcCtx.Flattened[call] = true - importDecls = append(importDecls, &Decl{ - Vars: []string{funcCtx.pkgCtx.pkgVars[impPath]}, - DeclCode: []byte(fmt.Sprintf("\t%s = $packages[\"%s\"];\n", funcCtx.pkgCtx.pkgVars[impPath], impPath)), - InitCode: funcCtx.CatchOutput(1, func() { funcCtx.translateStmt(&ast.ExprStmt{X: call}, nil) }), - }) - } + vars, functions, typeNames := rootCtx.topLevelObjects(srcs) + rootCtx.pkgCtx.canonicalTypeParams = typesutil.NewCanonicalTypeParamMap(functions, typesInfo) - var functions []*ast.FuncDecl - var vars []*types.Var - for _, file := range simplifiedFiles { - for _, decl := range file.Decls { - switch d := decl.(type) { - case *ast.FuncDecl: - sig := funcCtx.pkgCtx.Defs[d.Name].(*types.Func).Type().(*types.Signature) - var recvType types.Type - if sig.Recv() != nil { - recvType = sig.Recv().Type() - if ptr, isPtr := recvType.(*types.Pointer); isPtr { - recvType = ptr.Elem() - } - } - if sig.Recv() == nil { - funcCtx.objectName(funcCtx.pkgCtx.Defs[d.Name].(*types.Func)) // register toplevel name - } - if !isBlank(d.Name) { - functions = append(functions, d) - } - case *ast.GenDecl: - switch d.Tok { - 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.objectName(o) // register toplevel name - } - case token.VAR: - for _, spec := range d.Specs { - for _, name := range spec.(*ast.ValueSpec).Names { - if !isBlank(name) { - o := funcCtx.pkgCtx.Defs[name].(*types.Var) - vars = append(vars, o) - funcCtx.objectName(o) // register toplevel name - } - } - } - case token.CONST: - // skip, constants are inlined - } - } - } - } + // More named types may be added to the list when function bodies are processed. + rootCtx.pkgCtx.typeNames = typeNames - collectDependencies := func(f func()) []string { - funcCtx.pkgCtx.dependencies = make(map[types.Object]bool) - f() - var deps []string - for o := range funcCtx.pkgCtx.dependencies { - qualifiedName := o.Pkg().Path() + "." + o.Name() - if f, ok := o.(*types.Func); ok && f.Type().(*types.Signature).Recv() != nil { - deps = append(deps, qualifiedName+"~") - continue - } - deps = append(deps, qualifiedName) - } - sort.Strings(deps) - return deps + // Translate functions and variables. + varDecls := rootCtx.varDecls(vars) + funcDecls, err := rootCtx.funcDecls(functions) + if err != nil { + return nil, err } - // variables - var varDecls []*Decl - varsWithInit := make(map[*types.Var]bool) - for _, init := range funcCtx.pkgCtx.InitOrder { - for _, o := range init.Lhs { - varsWithInit[o] = true - } - } - for _, o := range vars { - var d Decl - if !o.Exported() { - d.Vars = []string{funcCtx.objectName(o)} - } - if funcCtx.pkgCtx.HasPointer[o] && !o.Exported() { - d.Vars = append(d.Vars, funcCtx.varPtrName(o)) - } - if _, ok := varsWithInit[o]; !ok { - d.DceDeps = collectDependencies(func() { - d.InitCode = []byte(fmt.Sprintf("\t\t%s = %s;\n", funcCtx.objectName(o), funcCtx.translateExpr(funcCtx.zeroValue(o.Type())).String())) - }) - } - d.DceObjectFilter = o.Name() - varDecls = append(varDecls, &d) - } - for _, init := range funcCtx.pkgCtx.InitOrder { - lhs := make([]ast.Expr, len(init.Lhs)) - for i, o := range init.Lhs { - ident := ast.NewIdent(o.Name()) - ident.NamePos = o.Pos() - funcCtx.pkgCtx.Defs[ident] = o - lhs[i] = funcCtx.setType(ident, o.Type()) - varsWithInit[o] = true - } - var d Decl - d.DceDeps = collectDependencies(func() { - funcCtx.localVars = nil - d.InitCode = funcCtx.CatchOutput(1, func() { - funcCtx.translateStmt(&ast.AssignStmt{ - Lhs: lhs, - Tok: token.DEFINE, - Rhs: []ast.Expr{init.Rhs}, - }, nil) - }) - d.Vars = append(d.Vars, funcCtx.localVars...) - }) - if len(init.Lhs) == 1 { - if !analysis.HasSideEffect(init.Rhs, funcCtx.pkgCtx.Info.Info) { - d.DceObjectFilter = init.Lhs[0].Name() - } - } - varDecls = append(varDecls, &d) - } + // It is important that we translate types *after* we've processed all + // functions to make sure we've discovered all types declared inside function + // bodies. + typeDecls := rootCtx.namedTypeDecls(rootCtx.pkgCtx.typeNames) - // functions - var funcDecls []*Decl - var mainFunc *types.Func - for _, fun := range functions { - o := funcCtx.pkgCtx.Defs[fun.Name].(*types.Func) + // Finally, anonymous types are translated the last, to make sure we've + // discovered all of them referenced in functions, variable and type + // declarations. + typeDecls = append(typeDecls, rootCtx.anonTypeDecls(rootCtx.pkgCtx.anonTypes.Ordered())...) - 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, - } - 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.NewSignature(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) - }) - 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() + "~" - } - } + // Combine all decls in a single list in the order they must appear in the + // final program. + allDecls := append(append(append(importDecls, typeDecls...), varDecls...), funcDecls...) - d.DceDeps = collectDependencies(func() { - d.DeclCode = funcCtx.translateToplevelFunction(fun, funcInfo) - }) - funcDecls = append(funcDecls, &d) - } - if typesPkg.Name() == "main" { - if mainFunc == nil { - return nil, fmt.Errorf("missing main function") - } - id := funcCtx.newIdent("", types.NewSignature(nil, nil, nil, false)) - funcCtx.pkgCtx.Uses[id] = mainFunc - call := &ast.CallExpr{Fun: id} - ifStmt := &ast.IfStmt{ - Cond: funcCtx.newIdent("$pkg === $mainPkg", types.Typ[types.Bool]), - Body: &ast.BlockStmt{ - List: []ast.Stmt{ - &ast.ExprStmt{X: call}, - &ast.AssignStmt{ - Lhs: []ast.Expr{funcCtx.newIdent("$mainFinished", types.Typ[types.Bool])}, - Tok: token.ASSIGN, - Rhs: []ast.Expr{funcCtx.newConst(types.Typ[types.Bool], constant.MakeBool(true))}, - }, - }, - }, - } - if len(funcCtx.pkgCtx.FuncDeclInfos[mainFunc].Blocking) != 0 { - funcCtx.Blocking[call] = true - funcCtx.Flattened[ifStmt] = true + if minify { + for _, d := range allDecls { + *d = d.minify() } - funcDecls = append(funcDecls, &Decl{ - InitCode: funcCtx.CatchOutput(1, func() { - funcCtx.translateStmt(ifStmt, nil) - }), - }) } - // named types - var typeDecls []*Decl - for _, o := range funcCtx.pkgCtx.typeNames { - if o.IsAlias() { - continue - } - 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()), - } - } - - d := Decl{ - Vars: []string{typeName}, - DceObjectFilter: o.Name(), - } - d.DceDeps = collectDependencies(func() { - d.DeclCode = funcCtx.CatchOutput(0, func() { - typeName := funcCtx.objectName(o) - lhs := typeName - if isPkgLevel(o) { - lhs += " = $pkg." + encodeIdent(o.Name()) - } - 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()) - } - 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) - } - 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, "%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 += "$" - } - 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) - } - 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 := 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) + if len(rootCtx.pkgCtx.errList) != 0 { + return nil, rootCtx.pkgCtx.errList } - // anonymous types - for _, t := range funcCtx.pkgCtx.anonTypes { - d := Decl{ - Vars: []string{t.Name()}, - DceObjectFilter: t.Name(), - } - d.DceDeps = collectDependencies(func() { - d.DeclCode = []byte(fmt.Sprintf("\t%s = $%sType(%s);\n", t.Name(), strings.ToLower(typeKind(t.Type())[5:]), funcCtx.initArgs(t.Type()))) - }) - typeDecls = append(typeDecls, &d) - } - - var allDecls []*Decl - for _, d := range append(append(append(importDecls, typeDecls...), varDecls...), funcDecls...) { - d.DeclCode = removeWhitespace(d.DeclCode, minify) - d.MethodListCode = removeWhitespace(d.MethodListCode, minify) - d.TypeInitCode = removeWhitespace(d.TypeInitCode, minify) - d.InitCode = removeWhitespace(d.InitCode, minify) - allDecls = append(allDecls, d) + exportData := new(bytes.Buffer) + if err := gcexportdata.Write(exportData, nil, typesPkg); err != nil { + return nil, fmt.Errorf("failed to write export data: %w", err) } - - if len(funcCtx.pkgCtx.errList) != 0 { - return nil, funcCtx.pkgCtx.errList + encodedFileSet := new(bytes.Buffer) + if err := srcs.FileSet.Write(json.NewEncoder(encodedFileSet).Encode); err != nil { + return nil, err } return &Archive{ - ImportPath: importPath, + ImportPath: srcs.ImportPath, Name: typesPkg.Name(), Imports: importedPaths, ExportData: exportData.Bytes(), @@ -665,221 +375,3 @@ func (fc *funcContext) initArgs(ty types.Type) string { panic(err) } } - -func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analysis.FuncInfo) []byte { - o := fc.pkgCtx.Defs[fun.Name].(*types.Func) - sig := o.Type().(*types.Signature) - var recv *ast.Ident - if fun.Recv != nil && fun.Recv.List[0].Names != nil { - recv = fun.Recv.List[0].Names[0] - } - - var joinedParams string - primaryFunction := func(funcRef string) []byte { - if fun.Body == nil { - 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) - joinedParams = strings.Join(params, ", ") - return []byte(fmt.Sprintf("\t%s = %s;\n", funcRef, fun)) - } - - code := bytes.NewBuffer(nil) - - if fun.Recv == nil { - funcRef := fc.objectName(o) - code.Write(primaryFunction(funcRef)) - if fun.Name.IsExported() { - fmt.Fprintf(code, "\t$pkg.%s = %s;\n", encodeIdent(fun.Name.Name), funcRef) - } - 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()) - 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) - return code.Bytes() - } - - if 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) - return code.Bytes() - } - return primaryFunction(fmt.Sprintf("$ptrType(%s).prototype.%s", typeName, funName)) - } - - value := "this.$get()" - if isWrapped(recvType) { - value = fmt.Sprintf("new %s(%s)", typeName, 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) - 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) { - 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), - } - for k, v := range outerContext.allVars { - c.allVars[k] = v - } - prevEV := c.pkgCtx.escapingVars - - var params []string - for _, param := range typ.Params.List { - if len(param.Names) == 0 { - params = append(params, c.newVariable("param")) - continue - } - for _, ident := range param.Names { - if isBlank(ident) { - params = append(params, c.newVariable("param")) - continue - } - params = append(params, c.objectName(c.pkgCtx.Defs[ident])) - } - } - - bodyOutput := string(c.CatchOutput(1, func() { - if len(c.Blocking) != 0 { - c.pkgCtx.Scopes[body] = c.pkgCtx.Scopes[typ] - c.handleEscapingVars(body) - } - - if c.sig != nil && c.sig.Results().Len() != 0 && c.sig.Results().At(0).Name() != "" { - 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()) - id := ast.NewIdent("") - c.pkgCtx.Uses[id] = result - c.resultNames[i] = c.setType(id, result.Type()) - } - } - - if recv != nil && !isBlank(recv) { - this := "this" - if isWrapped(c.pkgCtx.TypeOf(recv)) { - this = "this.$val" // Unwrap receiver value. - } - c.Printf("%s = %s;", c.translateExpr(recv), this) - } - - c.translateStmtList(body.List) - if len(c.Flattened) != 0 && !astutil.EndsWithReturn(body.List) { - c.translateStmt(&ast.ReturnStmt{}, nil) - } - })) - - sort.Strings(c.localVars) - - var prefix, suffix, functionName string - - if len(c.Flattened) != 0 { - c.localVars = append(c.localVars, "$s") - prefix = prefix + " $s = $s || 0;" - } - - if c.HasDefer { - c.localVars = append(c.localVars, "$deferred") - suffix = " }" + suffix - if len(c.Blocking) != 0 { - suffix = " }" + suffix - } - } - - localVarDefs := "" // Function-local var declaration at the top. - - if len(c.Blocking) != 0 { - if funcRef == "" { - funcRef = "$b" - functionName = " $b" - } - - localVars := append([]string{}, c.localVars...) - // There are several special variables involved in handling blocking functions: - // $r is sometimes used as a temporary variable to store blocking call result. - // $c indicates that a function is being resumed after a blocking call when set to true. - // $f is an object used to save and restore function context for blocking calls. - localVars = append(localVars, "$r") - // If a blocking function is being resumed, initialize local variables from the saved context. - localVarDefs = fmt.Sprintf("var {%s, $c} = $restore(this, {%s});\n", strings.Join(localVars, ", "), strings.Join(params, ", ")) - // If the function gets blocked, save local variables for future. - saveContext := fmt.Sprintf("var $f = {$blk: "+funcRef+", $c: true, $r, %s};", strings.Join(c.localVars, ", ")) - - suffix = " " + saveContext + "return $f;" + suffix - } else if len(c.localVars) > 0 { - // Non-blocking functions simply declare local variables with no need for restore support. - localVarDefs = fmt.Sprintf("var %s;\n", strings.Join(c.localVars, ", ")) - } - - if c.HasDefer { - prefix = prefix + " var $err = null; try {" - deferSuffix := " } catch(err) { $err = err;" - if len(c.Blocking) != 0 { - deferSuffix += " $s = -1;" - } - if c.resultNames == nil && c.sig.Results().Len() > 0 { - deferSuffix += fmt.Sprintf(" return%s;", c.translateResults(nil)) - } - deferSuffix += " } finally { $callDeferred($deferred, $err);" - if c.resultNames != nil { - deferSuffix += fmt.Sprintf(" if (!$curGoroutine.asleep) { return %s; }", c.translateResults(c.resultNames)) - } - if len(c.Blocking) != 0 { - deferSuffix += " if($curGoroutine.asleep) {" - } - suffix = deferSuffix + suffix - } - - if len(c.Flattened) != 0 { - prefix = prefix + " s: while (true) { switch ($s) { case 0:" - suffix = " } return; }" + suffix - } - - if c.HasDefer { - prefix = prefix + " $deferred = []; $curGoroutine.deferStack.push($deferred);" - } - - if prefix != "" { - bodyOutput = strings.Repeat("\t", c.pkgCtx.indentation+1) + "/* */" + prefix + "\n" + bodyOutput - } - if suffix != "" { - bodyOutput = bodyOutput + strings.Repeat("\t", c.pkgCtx.indentation+1) + "/* */" + suffix + "\n" - } - if localVarDefs != "" { - bodyOutput = strings.Repeat("\t", c.pkgCtx.indentation+1) + localVarDefs + bodyOutput - } - - c.pkgCtx.escapingVars = prevEV - - return params, fmt.Sprintf("function%s(%s) {\n%s%s}", functionName, strings.Join(params, ", "), bodyOutput, strings.Repeat("\t", c.pkgCtx.indentation)) -} diff --git a/compiler/prelude/jsmapping.js b/compiler/prelude/jsmapping.js index b22454bc3..2225d9fa4 100644 --- a/compiler/prelude/jsmapping.js +++ b/compiler/prelude/jsmapping.js @@ -1,4 +1,4 @@ -var $jsObjectPtr, $jsErrorPtr; +var $jsObjectPtr, $jsErrorPtr; // Initialized by init() in the runtime package. var $needsExternalization = t => { switch (t.kind) { diff --git a/compiler/prelude/numeric.js b/compiler/prelude/numeric.js index 5cfd7644d..4eb2730e4 100644 --- a/compiler/prelude/numeric.js +++ b/compiler/prelude/numeric.js @@ -30,7 +30,10 @@ var $floatKey = f => { return String(f); }; -var $flatten64 = x => { +var $flatten64 = (x, typ) => { + if (typ && typ.kind != $kindInt64 && typ.kind != $kindUint64) { + return x; // Not a 64-bit number, no need to flatten. + } return x.$high * 4294967296 + x.$low; }; @@ -116,6 +119,24 @@ var $mul64 = (x, y) => { return r; }; +const $idiv = (x, y) => { + const result = x / y; + if (result === result && result != 1/0 && result != -1/0) { + return result; + } else { + $throwRuntimeError("integer divide by zero"); + } +}; + +const $irem = (x, y) => { + const result = x % y; + if (result === result) { + return result; + } else { + $throwRuntimeError("integer divide by zero"); + } +}; + var $div64 = (x, y, returnRemainder) => { if (y.$high === 0 && y.$low === 0) { $throwRuntimeError("integer divide by zero"); diff --git a/compiler/prelude/prelude.js b/compiler/prelude/prelude.js index d35de6b01..a2c420f9b 100644 --- a/compiler/prelude/prelude.js +++ b/compiler/prelude/prelude.js @@ -73,7 +73,8 @@ if (($global.process !== undefined) && $global.require) { // Failed to require util module, keep using console.log(). } } -var $println = console.log +var $println = console.log; +var $Map = $global.Map; var $initAllLinknames = () => { var names = $keys($packages); diff --git a/compiler/prelude/types.js b/compiler/prelude/types.js index 61475454e..e9f89f16c 100644 --- a/compiler/prelude/types.js +++ b/compiler/prelude/types.js @@ -59,7 +59,7 @@ var $idKey = x => { }; // Creates constructor functions for array pointer types. Returns a new function -// instace each time to make sure each type is independent of the other. +// instance each time to make sure each type is independent of the other. var $arrayPtrCtor = () => { return function (array) { this.$get = () => { return array; }; @@ -84,19 +84,24 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { case $kindUnsafePointer: typ = function (v) { this.$val = v; }; typ.wrapped = true; + typ.wrap = (v) => new typ(v); typ.keyFor = $identity; break; case $kindString: typ = function (v) { this.$val = v; }; typ.wrapped = true; + typ.wrap = (v) => new typ(v); typ.keyFor = x => { return "$" + x; }; + typ.$len = (v) => v.length; + typ.$copy = (dst, src) => $copyString(dst, src); break; case $kindFloat32: case $kindFloat64: typ = function (v) { this.$val = v; }; typ.wrapped = true; + typ.wrap = (v) => new typ(v); typ.keyFor = x => { return $floatKey(x); }; break; @@ -106,6 +111,7 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { this.$low = low >>> 0; this.$val = this; }; + typ.wrap = (v) => v; typ.keyFor = x => { return x.$high + "$" + x.$low; }; break; @@ -115,6 +121,7 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { this.$low = low >>> 0; this.$val = this; }; + typ.wrap = (v) => v; typ.keyFor = x => { return x.$high + "$" + x.$low; }; break; @@ -124,6 +131,7 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { this.$imag = $fround(imag); this.$val = this; }; + typ.wrap = (v) => v; typ.keyFor = x => { return x.$real + "$" + x.$imag; }; break; @@ -133,12 +141,14 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { this.$imag = imag; this.$val = this; }; + typ.wrap = (v) => v; typ.keyFor = x => { return x.$real + "$" + x.$imag; }; break; case $kindArray: typ = function (v) { this.$val = v; }; typ.wrapped = true; + typ.wrap = (v) => new typ(v); typ.ptr = $newType(4, $kindPtr, "*" + string, false, "", false, $arrayPtrCtor()); typ.init = (elem, len) => { typ.elem = elem; @@ -155,22 +165,29 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { typ.ptr.init(typ); Object.defineProperty(typ.ptr.nil, "nilCheck", { get: $throwNilPointerError }); }; + typ.$len = (v) => typ.len; + typ.$cap = (v) => typ.len; break; case $kindChan: typ = function (v) { this.$val = v; }; typ.wrapped = true; + typ.wrap = (v) => new typ(v); typ.keyFor = $idKey; typ.init = (elem, sendOnly, recvOnly) => { typ.elem = elem; typ.sendOnly = sendOnly; typ.recvOnly = recvOnly; }; + typ.$make = (bufsize) => new $Chan(typ.elem, bufsize ? bufsize : 0); + typ.$len = (v) => v.$buffer.length; + typ.$cap = (v) => v.$capacity; break; case $kindFunc: typ = function (v) { this.$val = v; }; typ.wrapped = true; + typ.wrap = (v) => new typ(v); typ.init = (params, results, variadic) => { typ.params = params; typ.results = results; @@ -181,6 +198,7 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { case $kindInterface: typ = { implementedBy: {}, missingMethodFor: {} }; + typ.wrap = (v) => v; typ.keyFor = $ifaceKeyFor; typ.init = methods => { typ.methods = methods; @@ -193,11 +211,18 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { case $kindMap: typ = function (v) { this.$val = v; }; typ.wrapped = true; + typ.wrap = (v) => new typ(v); typ.init = (key, elem) => { typ.key = key; typ.elem = elem; typ.comparable = false; }; + typ.$make = (size) => { + if (size === undefined) { size = 0; } + if (size < 0 || size > 2147483647) { $throwRuntimeError("makemap: size out of range"); } + return new $global.Map(); + }; + typ.$len = (v) => v ? v.size : 0; break; case $kindPtr: @@ -207,12 +232,19 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { this.$target = target; this.$val = this; }; + typ.wrap = (v) => v; + typ.wrapped = false; typ.keyFor = $idKey; typ.init = elem => { typ.elem = elem; - typ.wrapped = (elem.kind === $kindArray); + if (elem.kind === $kindArray) { + typ.wrapped = true; + typ.wrap = (v) => ((v === typ.nil) ? v : new typ(v)); + } typ.nil = new typ($throwNilPointerError, $throwNilPointerError); }; + typ.$len = (v) => typ.elem.$len(typ.wrapped ? v : v.$get()); + typ.$cap = (v) => typ.elem.$cap(typ.wrapped ? v : v.$get()); break; case $kindSlice: @@ -226,18 +258,33 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { this.$capacity = array.length; this.$val = this; }; + typ.wrap = (v) => v; typ.init = elem => { typ.elem = elem; typ.comparable = false; typ.nativeArray = $nativeArray(elem.kind); typ.nil = new typ([]); }; + typ.$make = (size, capacity) => $makeSlice(typ, size, capacity); + typ.$len = (v) => v.$length; + typ.$cap = (v) => v.$capacity; + typ.$copy = (dst, src) => $copySlice(dst, src); break; case $kindStruct: typ = function (v) { this.$val = v; }; typ.wrapped = true; + typ.wrap = (v) => new typ(v); typ.ptr = $newType(4, $kindPtr, "*" + string, false, pkg, exported, constructor); + if (string === "js.Object" && pkg === "github.com/gopherjs/gopherjs/js") { + // *js.Object is a special case because unlike other pointers it + // passes around a raw JS object without any GopherJS-specific + // metadata. As a result, it must be wrapped to preserve type + // information whenever it's used through an interface or type + // param. However, it's now a "wrapped" type in a complete sense, + // because it's handling is mostly special-cased at the compiler level. + typ.ptr.wrap = (v) => new typ.ptr(v); + } typ.ptr.elem = typ; typ.ptr.prototype.$get = function () { return this; }; typ.ptr.prototype.$set = function (v) { typ.copy(this, v); }; @@ -382,6 +429,177 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { $panic(new $String("invalid kind: " + kind)); } + // Arithmetics operations for types that support it. + // + // Each operation accepts two operands and returns one result. For wrapped types operands are + // passed as bare values and a bare value is returned. + // + // This methods will be called when the exact type is not known at code generation time, for + // example, when operands are type parameters. + switch (kind) { + case $kindInt8: + case $kindInt16: + typ.add = (x, y) => $truncateNumber(x + y, typ); + typ.sub = (x, y) => $truncateNumber(x - y, typ); + typ.mul = (x, y) => $truncateNumber(x * y, typ); + typ.div = (x, y) => $idiv(x, y) >> 0; + typ.rem = $irem; + typ.and = (x, y) => (x & y); + typ.or = (x, y) => (x | y); + typ.xor = (x, y) => $truncateNumber(x ^ y, typ); + typ.andNot = (x, y) => $truncateNumber(x & ~y, typ); + typ.shl = (x, y) => (y < 32) ? $truncateNumber(x << y, typ) : 0; + typ.shr = (x, y) => $truncateNumber(x >> $min(y, 31), typ); + break; + case $kindUint8: + case $kindUint16: + typ.add = (x, y) => $truncateNumber(x + y, typ); + typ.sub = (x, y) => $truncateNumber(x - y, typ); + typ.mul = (x, y) => $truncateNumber(x * y, typ); + typ.div = (x, y) => $idiv(x, y) >>> 0; + typ.rem = $irem; + typ.and = (x, y) => (x & y) >>> 0; + typ.or = (x, y) => (x | y) >>> 0; + typ.xor = (x, y) => $truncateNumber(x ^ y, typ); + typ.andNot = (x, y) => $truncateNumber(x & ~y, typ); + typ.shl = (x, y) => (y < 32) ? $truncateNumber(x << y, typ) : 0; + typ.shr = (x, y) => (y < 32) ? $truncateNumber(x >>> y, typ) : 0; + break; + case $kindUint: + case $kindUint32: + case $kindUintptr: + typ.add = (x, y) => $truncateNumber(x + y, typ); + typ.sub = (x, y) => $truncateNumber(x - y, typ); + typ.mul = (x, y) => $imul(x, y) >>> 0; + typ.div = (x, y) => $idiv(x, y) >>> 0; + typ.rem = $irem; + typ.and = (x, y) => (x & y) >>> 0; + typ.or = (x, y) => (x | y) >>> 0; + typ.xor = (x, y) => $truncateNumber(x ^ y, typ); + typ.andNot = (x, y) => $truncateNumber(x & ~y, typ); + typ.shl = (x, y) => (y < 32) ? $truncateNumber(x << y, typ) : 0; + typ.shr = (x, y) => (y < 32) ? $truncateNumber(x >>> y, typ) : 0; + break; + case $kindInt: + case $kindInt32: + typ.add = (x, y) => $truncateNumber(x + y, typ); + typ.sub = (x, y) => $truncateNumber(x - y, typ); + typ.mul = (x, y) => $imul(x, y); + typ.div = (x, y) => $idiv(x, y) >> 0; + typ.rem = $irem; + typ.and = (x, y) => (x & y); + typ.or = (x, y) => (x | y); + typ.xor = (x, y) => $truncateNumber(x ^ y, typ); + typ.andNot = (x, y) => $truncateNumber(x & ~y, typ); + typ.shl = (x, y) => (y < 32) ? $truncateNumber(x << y, typ) : 0; + typ.shr = (x, y) => $truncateNumber(x >> $min(y, 31), typ); + break; + case $kindInt64: + case $kindUint64: + typ.add = (x, y) => new typ(x.$high + y.$high, x.$low + y.$low); + typ.sub = (x, y) => new typ(x.$high - y.$high, x.$low - y.$low); + typ.mul = (x, y) => $mul64(x, y); + typ.div = (x, y) => $div64(x, y, false); + typ.rem = (x, y) => $div64(x, y, true); + typ.and = (x, y) => new typ(x.$high & y.$high, (x.$low & y.$low) >>> 0); + typ.or = (x, y) => new typ(x.$high | y.$high, (x.$low | y.$low) >>> 0); + typ.xor = (x, y) => new typ(x.$high ^ y.$high, (x.$low ^ y.$low) >>> 0); + typ.andNot = (x, y) => new typ(x.$high & ~y.$high, (x.$low & ~y.$low) >>> 0); + typ.shl = $shiftLeft64; + typ.shr = (kind === $kindInt64) ? $shiftRightInt64 : $shiftRightUint64; + break; + case $kindFloat32: + case $kindFloat64: + typ.add = (x, y) => $truncateNumber(x + y, typ); + typ.sub = (x, y) => $truncateNumber(x - y, typ); + typ.mul = (x, y) => $truncateNumber(x * y, typ); + typ.div = (x, y) => $truncateNumber(x / y, typ); + break; + case $kindComplex64: + case $kindComplex128: + typ.add = (x, y) => new typ(x.$real + y.$real, x.$imag + y.$imag); + typ.sub = (x, y) => new typ(x.$real - y.$real, x.$imag - y.$imag); + typ.mul = (x, y) => new typ(x.$real * y.$real - x.$imag * y.$imag, x.$real * y.$imag + x.$imag * y.$real); + typ.div = (x, y) => $divComplex(x, y); + break; + case $kindString: + typ.add = (x, y) => x + y; + } + + /** + * convertFrom converts value src to the type typ. + * + * For wrapped types src must be a wrapped value, e.g. for int32 this must be an instance of + * the $Int32 class, rather than the bare JavaScript number. This is required to determine + * the original Go type to convert from. + * + * The returned value will be a representation of typ; for wrapped values it will be unwrapped; + * for example, conversion to int32 will return a bare JavaScript number. This is required + * to make results of type conversion expression consistent with any other expressions of the + * same type. + */ + typ.convertFrom = (src) => $convertIdentity(src, typ); + switch (kind) { + case $kindInt64: + case $kindUint64: + typ.convertFrom = (src) => $convertToInt64(src, typ); + break; + case $kindInt8: + case $kindInt16: + case $kindInt32: + case $kindInt: + case $kindUint8: + case $kindUint16: + case $kindUint32: + case $kindUint: + case $kindUintptr: + typ.convertFrom = (src) => $convertToNativeInt(src, typ); + break; + case $kindFloat32: + case $kindFloat64: + typ.convertFrom = (src) => $convertToFloat(src, typ); + break; + case $kindComplex128: + case $kindComplex64: + typ.convertFrom = (src) => $convertToComplex(src, typ); + break; + case $kindString: + typ.convertFrom = (src) => $convertToString(src, typ); + break; + case $kindUnsafePointer: + typ.convertFrom = (src) => $convertToUnsafePtr(src, typ); + break; + case $kindBool: + typ.convertFrom = (src) => $convertToBool(src, typ); + break; + case $kindInterface: + typ.convertFrom = (src) => $convertToInterface(src, typ); + break; + case $kindSlice: + typ.convertFrom = (src) => $convertToSlice(src, typ); + break; + case $kindPtr: + typ.convertFrom = (src) => $convertToPointer(src, typ); + break; + case $kindArray: + typ.convertFrom = (src) => $convertToArray(src, typ); + break; + case $kindStruct: + typ.convertFrom = (src) => $convertToStruct(src, typ); + break; + case $kindMap: + typ.convertFrom = (src) => $convertToMap(src, typ); + break; + case $kindChan: + typ.convertFrom = (src) => $convertToChan(src, typ); + break; + case $kindFunc: + typ.convertFrom = (src) => $convertToFunc(src, typ); + break; + default: + $panic(new $String("invalid kind: " + kind)); + } + typ.id = $typeIDCounter; $typeIDCounter++; typ.size = size; @@ -462,6 +680,12 @@ var $methodSet = typ => { return typ.methodSetCache; }; +var $instantiateMethods = function(typeInstance, methodFactories, ...typeArgs) { + for (let [method, factory] of Object.entries(methodFactories)) { + typeInstance.prototype[method] = factory(...typeArgs); + } + }; + var $Bool = $newType(1, $kindBool, "bool", true, "", false, null); var $Int = $newType(4, $kindInt, "int", true, "", false, null); var $Int8 = $newType(1, $kindInt8, "int8", true, "", false, null); @@ -633,13 +857,17 @@ var $ptrType = elem => { }; var $newDataPointer = (data, constructor) => { - if (constructor.elem.kind === $kindStruct) { + if (constructor.elem.kind === $kindStruct || constructor.elem.kind === $kindArray) { return data; } return new constructor(() => { return data; }, v => { data = v; }); }; var $indexPtr = (array, index, constructor) => { + if (constructor.kind == $kindPtr && constructor.elem.kind == $kindStruct) { + // Pointer to a struct is represented by the underlying object itself, no wrappers needed. + return array[index] + } if (array.buffer) { // Pointers to the same underlying ArrayBuffer share cache. var cache = array.buffer.$ptr = array.buffer.$ptr || {}; @@ -767,3 +995,359 @@ var $assertType = (value, type, returnTuple) => { } return returnTuple ? [value, true] : value; }; + +const $isSigned = (typ) => { + switch (typ.kind) { + case $kindInt: + case $kindInt8: + case $kindInt16: + case $kindInt32: + case $kindInt64: + return true; + default: + return false; + } +} + +/** + * Truncate a JavaScript number `n` according to precision of the Go type `typ` + * it is supposed to represent. + */ +const $truncateNumber = (n, typ) => { + switch (typ.kind) { + case $kindInt8: + return n << 24 >> 24; + case $kindUint8: + return n << 24 >>> 24; + case $kindInt16: + return n << 16 >> 16; + case $kindUint16: + return n << 16 >>> 16; + case $kindInt32: + case $kindInt: + return n << 0 >> 0; + case $kindUint32: + case $kindUint: + case $kindUintptr: + return n << 0 >>> 0; + case $kindFloat32: + return $fround(n); + case $kindFloat64: + return n; + default: + $panic(new $String("invalid kind: " + kind)); + } +} + +/** + * Trivial type conversion function, which only accepts destination type identical to the src + * type. + * + * For wrapped types, src value must be wrapped, and the return value will be unwrapped. + */ +const $convertIdentity = (src, dstType) => { + const srcType = src.constructor; + if (srcType === dstType) { + // Same type, no conversion necessary. + return srcType.wrapped ? src.$val : src; + } + throw new Error(`unsupported conversion from ${srcType.string} to ${dstType.string}`); +}; + +/** + * Conversion to int64 and uint64 variants. + * + * dstType.kind must be either $kindInt64 or $kindUint64. For wrapped types, src + * value must be wrapped. The returned value is an object instantiated by the + * `dstType` constructor. + */ +const $convertToInt64 = (src, dstType) => { + const srcType = src.constructor; + if (srcType === dstType) { + return src.$val; + } + + switch (srcType.kind) { + case $kindInt64: + case $kindUint64: + return new dstType(src.$val.$high, src.$val.$low); + case $kindUintptr: + // GopherJS quirk: a uintptr value may be an object converted to + // uintptr from unsafe.Pointer. Since we don't have actual pointers, + // we pretend it's value is 1. + return new dstType(0, src.$val.constructor === Number ? src.$val : 1); + default: + return new dstType(0, src.$val); + } +}; + +/** + * Conversion to int and uint types of 32 bits or less. + * + * dstType.kind must be $kindInt{8,16,32} or $kindUint{8,16,32}. For wrapped + * types, src value must be wrapped. The return value will always be a bare + * JavaScript number, since all 32-or-less integers in GopherJS are considered + * wrapped types. + */ +const $convertToNativeInt = (src, dstType) => { + const srcType = src.constructor; + // Since we are returning a bare number, identical kinds means no actual + // conversion is required. + if (srcType.kind === dstType.kind) { + return src.$val; + } + + switch (srcType.kind) { + case $kindInt64: + case $kindUint64: + if ($isSigned(srcType) && $isSigned(dstType)) { // Carry over the sign. + return $truncateNumber(src.$val.$low + ((src.$val.$high >> 31) * 4294967296), dstType); + } + return $truncateNumber(src.$val.$low, dstType); + default: + return $truncateNumber(src.$val, dstType); + } +}; + +/** + * Conversion to floating point types. + * + * dstType.kind must be $kindFloat{32,64}. For wrapped types, src value must be + * wrapped. Returned value will always be a bare JavaScript number, since all + * floating point numbers in GopherJS are considered wrapped types. + */ +const $convertToFloat = (src, dstType) => { + const srcType = src.constructor; + // Since we are returning a bare number, identical kinds means no actual + // conversion is required. + if (srcType.kind === dstType.kind) { + return src.$val; + } + + if (dstType.kind == $kindFloat32 && srcType.kind == $kindFloat64) { + return $fround(src.$val); + } + + switch (srcType.kind) { + case $kindInt64: + case $kindUint64: + const val = $flatten64(src.$val); + return (dstType.kind == $kindFloat32) ? $fround(val) : val; + case $kindFloat64: + return (dstType.kind == $kindFloat32) ? $fround(src.$val) : src.$val; + default: + return src.$val; + } +}; + +/** + * Conversion to complex types. + * + * dstType.kind must me $kindComplex{64,128}. Src must be another complex type. + * Returned value will always be an oject created by the dstType constructor. + */ +const $convertToComplex = (src, dstType) => { + const srcType = src.constructor; + if (srcType === dstType) { + return src; + } + + switch (srcType.kind) { + case $kindComplex64: + case $kindComplex128: + return new dstType(src.$real, src.$imag); + default: + // Although normally Go doesn't allow conversion from int/float types + // to complex, it does allow conversion from untyped constants. + const real = (srcType.kind === $kindUint64 || srcType.kind === $kindInt64) ? $flatten64(src) : src.$val; + return new dstType(real, 0); + } +}; + +/** + * Conversion to string types. + * + * dstType.kind must be $kindString. For wrapped types, src value must be + * wrapped. Returned value will always be a bare JavaScript string. + */ +const $convertToString = (src, dstType) => { + const srcType = src.constructor; + if (srcType === dstType) { + return src.$val; + } + + switch (srcType.kind) { + case $kindInt64: + case $kindUint64: + return $encodeRune(src.$val.$low); + case $kindInt32: + case $kindInt16: + case $kindInt8: + case $kindInt: + case $kindUint32: + case $kindUint16: + case $kindUint8: + case $kindUint: + case $kindUintptr: + return $encodeRune(src.$val); + case $kindString: + return src.$val; + case $kindSlice: + if (srcType.elem.kind === $kindInt32) { // Runes are int32. + return $runesToString(src.$val); + } else if (srcType.elem.kind === $kindUint8) { // Bytes are uint8. + return $bytesToString(src.$val); + } + break; + } + + throw new Error(`Unsupported conversion from ${srcType.string} to ${dstType.string}`); +}; + +/** + * Convert to unsafe.Pointer. + */ +const $convertToUnsafePtr = (src, dstType) => { + // Unsafe pointers are not well supported by GopherJS and whatever support + // exists is not well documented. Implementing this conversion will require + // a great deal of great reverse engineering, and I'm not sure anyone will + // ever need this. + throw new Error(`Conversion between a typeparam and unsafe.Pointer is not implemented.`) +}; + +/** + * Convert to boolean types. + * + * dstType.kind must be $kindBool. Src must be a wrapped boolean value. Returned + * value will always be a bare JavaScript boolean. + */ +const $convertToBool = (src, dstType) => { + return src.$val; +}; + +/** + * Convert any type to an interface value. + * + * dstType.kind must be $kindInterface. For wrapped types, src value must be + * wrapped. Since GopherJS represents interfaces as wrapped values of the original + * type, the returned value is always src. + */ +const $convertToInterface = (src, dstType) => { + return src; +}; + +/** + * Convert to a slice value. + * + * dstType.kind must be $kindSlice. For wrapped types, src value must be wrapped. + * The returned value is always a slice type. + */ +const $convertToSlice = (src, dstType) => { + const srcType = src.constructor; + if (srcType === dstType) { + return src; + } + + switch (srcType.kind) { + case $kindString: + if (dstType.elem.kind === $kindInt32) { // Runes are int32. + return new dstType($stringToRunes(src.$val)); + } else if (dstType.elem.kind === $kindUint8) { // Bytes are uint8. + return new dstType($stringToBytes(src.$val)); + } + break; + case $kindSlice: + return $convertSliceType(src, dstType); + break; + } + throw new Error(`Unsupported conversion from ${srcType.string} to ${dstType.string}`); +}; + +/** +* Convert to a pointer value. +* +* dstType.kind must be $kindPtr. For wrapped types (specifically, pointers +* to an array), src value must be wrapped. The returned value is a bare JS +* array (typed or untyped), or a pointer object. +*/ +const $convertToPointer = (src, dstType) => { + const srcType = src.constructor; + + if (srcType === dstType) { + return src; + } + + // []T → *[N]T + if (srcType.kind == $kindSlice && dstType.elem.kind == $kindArray) { + return $sliceToGoArray(src, dstType); + } + + if (src === srcType.nil) { + return dstType.nil; + } + + switch (dstType.elem.kind) { + case $kindArray: + // Pointers to arrays are a wrapped type, represented by a native JS array, + // which we return directly. + return src.$val; + case $kindStruct: + return $pointerOfStructConversion(src, dstType); + default: + return new dstType(src.$get, src.$set, src.$target); + } +}; + +/** + * Convert to struct types. + * + * dstType.kind must be $kindStruct. Src must be a wrapped struct value. Returned + * value will always be a bare JavaScript object representing the struct. + */ +const $convertToStruct = (src, dstType) => { + // Since structs are passed by value, the conversion result must be a copy + // of the original value, even if it is the same type. + return $clone(src.$val, dstType); +}; + +/** + * Convert to array types. + * + * dstType.kind must be $kindArray. Src must be a wrapped array value. Returned + * value will always be a bare JavaScript object representing the array. + */ +const $convertToArray = (src, dstType) => { + // Since arrays are passed by value, the conversion result must be a copy + // of the original value, even if it is the same type. + return $clone(src.$val, dstType); +}; + +/** + * Convert to map types. + * + * dstType.kind must be $kindMap. Src must be a wrapped map value. Returned + * value will always be a bare JavaScript object representing the map. + */ +const $convertToMap = (src, dstType) => { + return src.$val; +}; + +/** + * Convert to chan types. + * + * dstType.kind must be $kindChan. Src must be a wrapped chan value. Returned + * value will always be a bare $Chan object representing the channel. + */ +const $convertToChan = (src, dstType) => { + return src.$val; +}; + +/** + * Convert to function types. + * + * dstType.kind must be $kindFunc. Src must be a wrapped function value. Returned + * value will always be a bare JavaScript function. + */ +const $convertToFunc = (src, dstType) => { + return src.$val; +}; diff --git a/compiler/sources.go b/compiler/sources.go new file mode 100644 index 000000000..4224f81f0 --- /dev/null +++ b/compiler/sources.go @@ -0,0 +1,122 @@ +package compiler + +import ( + "go/ast" + "go/token" + "go/types" + "sort" + + "github.com/neelance/astrewrite" +) + +// sources is a slice of parsed Go sources. +// +// Note that the sources would normally belong to a single logical Go package, +// but they don't have to be a real Go package (i.e. found on the file system) +// or represent a complete package (i.e. it could be only a few source files +// compiled by `gopherjs build foo.go bar.go`). +type sources struct { + // ImportPath representing the sources, if exists. May be empty for "virtual" + // packages like testmain or playground-generated package. + ImportPath string + Files []*ast.File + FileSet *token.FileSet +} + +// Sort the Files slice by the original source name to ensure consistent order +// of processing. This is required for reproducible JavaScript output. +// +// Note this function mutates the original slice. +func (s sources) Sort() sources { + sort.Slice(s.Files, func(i, j int) bool { + return s.FileSet.File(s.Files[i].Pos()).Name() > s.FileSet.File(s.Files[j].Pos()).Name() + }) + return s +} + +// Simplify returns a new sources instance with each Files entry processed by +// astrewrite.Simplify. +func (s sources) Simplified(typesInfo *types.Info) sources { + simplified := sources{ + ImportPath: s.ImportPath, + FileSet: s.FileSet, + } + for _, file := range s.Files { + simplified.Files = append(simplified.Files, astrewrite.Simplify(file, typesInfo, false)) + } + return simplified +} + +// TypeCheck the sources. Returns information about declared package types and +// type information for the supplied AST. +func (s sources) TypeCheck(importContext *ImportContext) (*types.Info, *types.Package, error) { + const errLimit = 10 // Max number of type checking errors to return. + + typesInfo := &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + 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 typeErrs ErrorList + + importer := packageImporter{ImportContext: importContext} + + config := &types.Config{ + Importer: &importer, + Sizes: sizes32, + Error: func(err error) { typeErrs = typeErrs.AppendDistinct(err) }, + } + typesPkg, err := config.Check(s.ImportPath, s.FileSet, s.Files, typesInfo) + // If we encountered any import errors, it is likely that the other type errors + // are not meaningful and would be resolved by fixing imports. Return them + // separately, if any. https://github.com/gopherjs/gopherjs/issues/119. + if importer.Errors.ErrOrNil() != nil { + return nil, nil, importer.Errors.Trim(errLimit).ErrOrNil() + } + // Return any other type errors. + if typeErrs.ErrOrNil() != nil { + return nil, nil, typeErrs.Trim(errLimit).ErrOrNil() + } + // Any general errors that may have occurred during type checking. + if err != nil { + return nil, nil, err + } + return typesInfo, typesPkg, nil +} + +// ParseGoLinknames extracts all //go:linkname compiler directive from the sources. +func (s sources) ParseGoLinknames() ([]GoLinkname, error) { + goLinknames := []GoLinkname{} + var errs ErrorList + for _, file := range s.Files { + found, err := parseGoLinknames(s.FileSet, s.ImportPath, file) + errs = errs.Append(err) + goLinknames = append(goLinknames, found...) + } + return goLinknames, errs.ErrOrNil() +} + +// packageImporter implements go/types.Importer interface. +type packageImporter struct { + ImportContext *ImportContext + Errors ErrorList +} + +func (pi *packageImporter) Import(path string) (*types.Package, error) { + if path == "unsafe" { + return types.Unsafe, nil + } + + a, err := pi.ImportContext.Import(path) + if err != nil { + pi.Errors = pi.Errors.AppendDistinct(err) + return nil, err + } + + return pi.ImportContext.Packages[a.ImportPath], nil +} diff --git a/compiler/statements.go b/compiler/statements.go index f8d791948..9bedaaa15 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -100,7 +100,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { data.endCase = fc.caseCounter fc.caseCounter++ - fc.Indent(func() { + fc.Indented(func() { fc.translateStmtList(clause.Body) }) fc.Printf("case %d:", data.endCase) @@ -112,7 +112,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { fc.Printf("%s:", label.Name()) } fc.Printf("switch (0) { default:") - fc.Indent(func() { + fc.Indented(func() { fc.translateStmtList(clause.Body) }) fc.Printf("}") @@ -125,7 +125,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { if s.Init != nil { fc.translateStmt(s.Init, nil) } - refVar := fc.newVariable("_ref") + refVar := fc.newLocalVariable("_ref") var expr ast.Expr switch a := s.Assign.(type) { case *ast.AssignStmt: @@ -187,14 +187,14 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { }, label, fc.Flattened[s]) case *ast.RangeStmt: - refVar := fc.newVariable("_ref") + refVar := fc.newLocalVariable("_ref") fc.Printf("%s = %s;", refVar, fc.translateExpr(s.X)) switch t := fc.pkgCtx.TypeOf(s.X).Underlying().(type) { case *types.Basic: - iVar := fc.newVariable("_i") + iVar := fc.newLocalVariable("_i") fc.Printf("%s = 0;", iVar) - runeVar := fc.newVariable("_rune") + runeVar := fc.newLocalVariable("_rune") fc.translateLoopingStmt(func() string { return iVar + " < " + refVar + ".length" }, s.Body, func() { fc.Printf("%s = $decodeRune(%s, %s);", runeVar, refVar, iVar) if !isBlank(s.Key) { @@ -208,16 +208,16 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { }, label, fc.Flattened[s]) case *types.Map: - iVar := fc.newVariable("_i") + iVar := fc.newLocalVariable("_i") fc.Printf("%s = 0;", iVar) - keysVar := fc.newVariable("_keys") + keysVar := fc.newLocalVariable("_keys") fc.Printf("%s = %s ? %s.keys() : undefined;", keysVar, refVar, refVar) - sizeVar := fc.newVariable("_size") + sizeVar := fc.newLocalVariable("_size") fc.Printf("%s = %s ? %s.size : 0;", sizeVar, refVar, refVar) fc.translateLoopingStmt(func() string { return iVar + " < " + sizeVar }, s.Body, func() { - keyVar := fc.newVariable("_key") - entryVar := fc.newVariable("_entry") + keyVar := fc.newLocalVariable("_key") + entryVar := fc.newLocalVariable("_entry") fc.Printf("%s = %s.next().value;", keyVar, keysVar) fc.Printf("%s = %s.get(%s);", entryVar, refVar, keyVar) fc.translateStmt(&ast.IfStmt{ @@ -248,7 +248,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { length = refVar + ".$length" elemType = t2.Elem() } - iVar := fc.newVariable("_i") + iVar := fc.newLocalVariable("_i") fc.Printf("%s = 0;", iVar) fc.translateLoopingStmt(func() string { return iVar + " < " + length }, s.Body, func() { if !isBlank(s.Key) { @@ -265,7 +265,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { }, label, fc.Flattened[s]) case *types.Chan: - okVar := fc.newIdent(fc.newVariable("_ok"), types.Typ[types.Bool]) + okVar := fc.newIdent(fc.newLocalVariable("_ok"), types.Typ[types.Bool]) key := s.Key tok := s.Tok if key == nil { @@ -354,7 +354,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { if rVal != "" { // If returned expression is non empty, evaluate and store it in a // variable to avoid double-execution in case a deferred function blocks. - rVar := fc.newVariable("$r") + rVar := fc.newLocalVariable("$r") fc.Printf("%s =%s;", rVar, rVal) rVal = " " + rVar } @@ -386,7 +386,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { fc.Printf("%s", fc.translateAssign(lhs, s.Rhs[0], s.Tok == token.DEFINE)) case len(s.Lhs) > 1 && len(s.Rhs) == 1: - tupleVar := fc.newVariable("_tuple") + tupleVar := fc.newLocalVariable("_tuple") fc.Printf("%s = %s;", tupleVar, fc.translateExpr(s.Rhs[0])) tuple := fc.pkgCtx.TypeOf(s.Rhs[0]).(*types.Tuple) for i, lhs := range s.Lhs { @@ -398,7 +398,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { case len(s.Lhs) == len(s.Rhs): tmpVars := make([]string, len(s.Rhs)) for i, rhs := range s.Rhs { - tmpVars[i] = fc.newVariable("_tmp") + tmpVars[i] = fc.newLocalVariable("_tmp") if isBlank(astutil.RemoveParens(s.Lhs[i])) { fc.Printf("$unused(%s);", fc.translateExpr(rhs)) continue @@ -444,7 +444,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { 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.objectNames[o] = fc.newVariable(o.Name(), varPackage) fc.pkgCtx.dependencies[o] = true } case token.CONST: @@ -478,7 +478,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { fc.translateStmt(&ast.ExprStmt{X: call}, label) case *ast.SelectStmt: - selectionVar := fc.newVariable("_selection") + selectionVar := fc.newLocalVariable("_selection") var channels []string var caseClauses []*ast.CaseClause flattened := false @@ -614,7 +614,7 @@ func (fc *funcContext) translateBranchingStmt(caseClauses []*ast.CaseClause, def for i, clause := range caseClauses { fc.SetPos(clause.Pos()) fc.PrintCond(!flatten, fmt.Sprintf("%sif (%s) {", prefix, condStrs[i]), fmt.Sprintf("case %d:", caseOffset+i)) - fc.Indent(func() { + fc.Indented(func() { fc.translateStmtList(clause.Body) if flatten && (i < len(caseClauses)-1 || defaultClause != nil) && !astutil.EndsWithReturn(clause.Body) { fc.Printf("$s = %d; continue;", endCase) @@ -625,7 +625,7 @@ func (fc *funcContext) translateBranchingStmt(caseClauses []*ast.CaseClause, def if defaultClause != nil { fc.PrintCond(!flatten, prefix+"{", fmt.Sprintf("case %d:", caseOffset+len(caseClauses))) - fc.Indent(func() { + fc.Indented(func() { fc.translateStmtList(defaultClause.Body) }) } @@ -655,7 +655,7 @@ func (fc *funcContext) translateLoopingStmt(cond func() string, body *ast.BlockS } isTerminated := false fc.PrintCond(!flatten, "while (true) {", fmt.Sprintf("case %d:", data.beginCase)) - fc.Indent(func() { + fc.Indented(func() { condStr := cond() if condStr != "true" { fc.PrintCond(!flatten, fmt.Sprintf("if (!(%s)) { break; }", condStr), fmt.Sprintf("if(!(%s)) { $s = %d; continue; }", condStr, data.endCase)) @@ -704,7 +704,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { if typesutil.IsJsObject(fc.pkgCtx.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") + keyVar := fc.newLocalVariable("_key") return fmt.Sprintf( `%s = %s; (%s || $throwRuntimeError("assignment to entry in nil map")).set(%s.keyFor(%s), { k: %s, v: %s });`, keyVar, @@ -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.sigTypes.Sig.Results() switch tuple.Len() { case 0: return "" @@ -799,7 +799,7 @@ func (fc *funcContext) translateResults(results []ast.Expr) string { return " " + resultExpr } - tmpVar := fc.newVariable("_returncast") + tmpVar := fc.newLocalVariable("_returncast") fc.Printf("%s = %s;", tmpVar, resultExpr) // Not all the return types matched, map everything out for implicit casting diff --git a/compiler/typesutil/coretype.go b/compiler/typesutil/coretype.go new file mode 100644 index 000000000..bd366c9b8 --- /dev/null +++ b/compiler/typesutil/coretype.go @@ -0,0 +1,20 @@ +package typesutil + +import ( + "go/types" + _ "unsafe" // for go:linkname +) + +// Currently go/types doesn't offer a public API to determine type's core type. +// Instead of importing a third-party reimplementation, I opted to hook into +// the unexported implementation go/types already has. +// +// If https://github.com/golang/go/issues/60994 gets accepted, we will be able +// to switch to the official API. + +// CoreType of the given type, or nil of it has no core type. +// https://go.dev/ref/spec#Core_types +func CoreType(t types.Type) types.Type { return coreTypeImpl(t) } + +//go:linkname coreTypeImpl go/types.coreType +func coreTypeImpl(t types.Type) types.Type diff --git a/compiler/typesutil/signature.go b/compiler/typesutil/signature.go new file mode 100644 index 000000000..88cd63582 --- /dev/null +++ b/compiler/typesutil/signature.go @@ -0,0 +1,101 @@ +package typesutil + +import ( + "fmt" + "go/types" +) + +// Signature is a helper that provides convenient access to function +// signature type information. +type Signature struct { + Sig *types.Signature +} + +// RequiredParams returns the number of required parameters in the function signature. +func (st Signature) RequiredParams() int { + l := st.Sig.Params().Len() + if st.Sig.Variadic() { + return l - 1 // Last parameter is a slice of variadic params. + } + return l +} + +// VariadicType returns the slice-type corresponding to the signature's variadic +// parameter, or nil of the signature is not variadic. With the exception of +// the special-case `append([]byte{}, "string"...)`, the returned type is +// `*types.Slice` and `.Elem()` method can be used to get the type of individual +// arguments. +func (st Signature) VariadicType() types.Type { + if !st.Sig.Variadic() { + return nil + } + return st.Sig.Params().At(st.Sig.Params().Len() - 1).Type() +} + +// Returns the expected argument type for the i'th argument position. +// +// This function is able to return correct expected types for variadic calls +// both when ellipsis syntax (e.g. myFunc(requiredArg, optionalArgSlice...)) +// is used and when optional args are passed individually. +// +// The returned types may differ from the actual argument expression types if +// there is an implicit type conversion involved (e.g. passing a struct into a +// function that expects an interface). +func (st Signature) Param(i int, ellipsis bool) types.Type { + if i < st.RequiredParams() { + return st.Sig.Params().At(i).Type() + } + if !st.Sig.Variadic() { + // This should never happen if the code was type-checked successfully. + panic(fmt.Errorf("Tried to access parameter %d of a non-variadic signature %s", i, st.Sig)) + } + if ellipsis { + return st.VariadicType() + } + return st.VariadicType().(*types.Slice).Elem() +} + +// HasResults returns true if the function signature returns something. +func (st Signature) HasResults() bool { + return st.Sig.Results().Len() > 0 +} + +// HasNamedResults returns true if the function signature returns something and +// returned results are names (e.g. `func () (val int, err error)`). +func (st Signature) HasNamedResults() bool { + return st.HasResults() && st.Sig.Results().At(0).Name() != "" +} + +// IsGeneric returns true if the signature represents a generic function or a +// method of a generic type. +func (st Signature) IsGeneric() bool { + return st.Sig.TypeParams().Len() > 0 || st.Sig.RecvTypeParams().Len() > 0 +} + +// RecvType returns receiver type for a method signature. For pointer receivers +// the named type is unwrapped from the pointer type. For non-methods nil is +// returned. +func (st Signature) RecvType() *types.Named { + recv := st.Sig.Recv() + if recv == nil { + return nil + } + + typ := recv.Type() + if ptrType, ok := typ.(*types.Pointer); ok { + typ = ptrType.Elem() + } + + return typ.(*types.Named) +} + +// RecvTypeName returns receiver type name for a method signature. For pointer +// receivers the named type is unwrapped from the pointer type. For non-methods +// an empty string is returned. +func (st Signature) RecvTypeName() string { + typ := st.RecvType() + if typ == nil { + return "" + } + return typ.Obj().Name() +} diff --git a/compiler/typesutil/typesutil.go b/compiler/typesutil/typesutil.go index 600925b81..f3d63541c 100644 --- a/compiler/typesutil/typesutil.go +++ b/compiler/typesutil/typesutil.go @@ -1,11 +1,19 @@ package typesutil -import "go/types" +import ( + "fmt" + "go/ast" + "go/types" + "golang.org/x/tools/go/types/typeutil" +) + +// IsJsPackage returns is the package is github.com/gopherjs/gopherjs/js. func IsJsPackage(pkg *types.Package) bool { return pkg != nil && pkg.Path() == "github.com/gopherjs/gopherjs/js" } +// IsJsObject returns true if the type represents a pointer to github.com/gopherjs/gopherjs/js.Object. func IsJsObject(t types.Type) bool { ptr, isPtr := t.(*types.Pointer) if !isPtr { @@ -14,3 +22,178 @@ func IsJsObject(t types.Type) bool { named, isNamed := ptr.Elem().(*types.Named) return isNamed && IsJsPackage(named.Obj().Pkg()) && named.Obj().Name() == "Object" } + +// AnonymousTypes maintains a mapping between anonymous types encountered in a +// Go program to equivalent synthetic names types GopherJS generated from them. +// +// This enables a runtime performance optimization where different instances of +// the same anonymous type (e.g. in expression `x := map[int]string{}`) don't +// need to initialize type information (e.g. `$mapType($Int, $String)`) every +// time, but reuse the single synthesized type (e.g. `mapType$1`). +type AnonymousTypes struct { + m typeutil.Map + order []*types.TypeName +} + +// Get returns the synthesized type name for the provided anonymous type or nil +// if the type is not registered. +func (at *AnonymousTypes) Get(t types.Type) *types.TypeName { + s, _ := at.m.At(t).(*types.TypeName) + return s +} + +// Ordered returns synthesized named types for the registered anonymous types in +// the order they were registered. +func (at *AnonymousTypes) Ordered() []*types.TypeName { + return at.order +} + +// Names returns synthesized type name strings for the registered anonymous +// types in the order they were registered. +func (at *AnonymousTypes) Names() []string { + names := []string{} + for _, t := range at.order { + names = append(names, t.Name()) + } + return names +} + +// Len returns the number of registered anonymous types. +func (at *AnonymousTypes) Len() int { + return len(at.order) +} + +// Register a synthesized type name for an anonymous type. +func (at *AnonymousTypes) Register(name *types.TypeName, anonType types.Type) { + at.m.Set(anonType, name) + at.order = append(at.order, name) +} + +// IsGeneric returns true if the provided type is a type parameter or depends +// on a type parameter. +func IsGeneric(t types.Type) bool { + switch t := t.(type) { + case *types.Array: + return IsGeneric(t.Elem()) + case *types.Basic: + return false + case *types.Chan: + return IsGeneric(t.Elem()) + case *types.Interface: + for i := 0; i < t.NumMethods(); i++ { + if IsGeneric(t.Method(i).Type()) { + return true + } + } + for i := 0; i < t.NumEmbeddeds(); i++ { + if IsGeneric(t.Embedded(i)) { + return true + } + } + return false + case *types.Map: + return IsGeneric(t.Key()) || IsGeneric(t.Elem()) + case *types.Named: + for i := 0; i < t.TypeArgs().Len(); i++ { + if IsGeneric(t.TypeArgs().At(i)) { + return true + } + } + return false + case *types.Pointer: + return IsGeneric(t.Elem()) + case *types.Slice: + return IsGeneric(t.Elem()) + case *types.Signature: + for i := 0; i < t.Params().Len(); i++ { + if IsGeneric(t.Params().At(i).Type()) { + return true + } + } + for i := 0; i < t.Results().Len(); i++ { + if IsGeneric(t.Results().At(i).Type()) { + return true + } + } + return false + case *types.Struct: + for i := 0; i < t.NumFields(); i++ { + if IsGeneric(t.Field(i).Type()) { + return true + } + } + return false + case *types.TypeParam: + return true + default: + panic(fmt.Errorf("%v has unexpected type %T", t, t)) + } +} + +// IsTypeParam returns true if the type is a type param. +func IsTypeParam(t types.Type) bool { + _, ok := t.(*types.TypeParam) + return ok +} + +// IsInterface returns true if the type is an interface. +func IsInterface(t types.Type) bool { + _, ok := t.Underlying().(*types.Interface) + return ok && !IsTypeParam(t) +} + +// IsMethod returns true if the passed object is a method. +func IsMethod(o types.Object) bool { + f, ok := o.(*types.Func) + return ok && f.Type().(*types.Signature).Recv() != nil +} + +// TypeParams returns a list of type parameters for a parameterized type, or +// nil otherwise. +func TypeParams(t types.Type) *types.TypeParamList { + named, ok := t.(*types.Named) + if !ok { + return nil + } + return named.TypeParams() +} + +// CanonicalTypeParamMap allows mapping type parameters on method receivers +// to their canonical counterparts on the receiver types. +type CanonicalTypeParamMap map[*types.TypeParam]*types.TypeParam + +// NewCanonicalTypeParamMap creates a mapping between methods' type parameters +// for the provided function decls and their receiver types' type parameters. +// +// Non-method decls are ignored. +func NewCanonicalTypeParamMap(funcs []*ast.FuncDecl, tInfo *types.Info) CanonicalTypeParamMap { + result := CanonicalTypeParamMap{} + for _, fun := range funcs { + o := tInfo.Defs[fun.Name] + sig := Signature{Sig: o.Type().(*types.Signature)} + if sig.Sig.RecvTypeParams().Len() == 0 { + continue + } + tParams := sig.Sig.RecvTypeParams() + recvTParams := sig.RecvType().TypeParams() + if tParams.Len() != recvTParams.Len() { + // This should never happen in a type-checked program. + panic(fmt.Errorf("mismatched number of type parameters on a method %s and its receiver type %s: %d != %d", o, sig.RecvType(), tParams.Len(), recvTParams.Len())) + } + for i := 0; i < tParams.Len(); i++ { + tParam := tParams.At(i) + canonicalTParam := recvTParams.At(i) + result[tParam] = canonicalTParam + } + } + return result +} + +// Lookup returns the canonical version of the given type parameter. If there is +// no canonical version, the type parameter is returned as-is. +func (cm CanonicalTypeParamMap) Lookup(tParam *types.TypeParam) *types.TypeParam { + if canonical, ok := cm[tParam]; ok { + return canonical + } + return tParam +} diff --git a/compiler/typesutil/typesutil_test.go b/compiler/typesutil/typesutil_test.go new file mode 100644 index 000000000..f47ecd39f --- /dev/null +++ b/compiler/typesutil/typesutil_test.go @@ -0,0 +1,225 @@ +package typesutil + +import ( + "go/ast" + "go/token" + "go/types" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/gopherjs/gopherjs/internal/srctesting" +) + +func TestAnonymousTypes(t *testing.T) { + t1 := types.NewSlice(types.Typ[types.String]) + t1Name := types.NewTypeName(token.NoPos, nil, "sliceType$1", t1) + + t2 := types.NewMap(types.Typ[types.Int], t1) + t2Name := types.NewTypeName(token.NoPos, nil, "mapType$1", t2) + + typs := []struct { + typ types.Type + name *types.TypeName + }{ + {typ: t1, name: t1Name}, + {typ: t2, name: t2Name}, + } + + anonTypes := AnonymousTypes{} + for _, typ := range typs { + anonTypes.Register(typ.name, typ.typ) + } + + if want, got := 2, anonTypes.Len(); want != got { + t.Errorf("Got: anonTypes.Len() = %v. Want: %v.", got, want) + } + + for _, typ := range typs { + t.Run(typ.name.Name(), func(t *testing.T) { + got := anonTypes.Get(typ.typ) + if got != typ.name { + t.Errorf("Got: anonTypes.Get(%v) = %v. Want: %v.", typ.typ, typ.name, got) + } + }) + } + + gotNames := []string{} + for _, name := range anonTypes.Ordered() { + gotNames = append(gotNames, name.Name()) + } + wantNames := []string{"sliceType$1", "mapType$1"} + if !cmp.Equal(wantNames, gotNames) { + t.Errorf("Got: anonTypes.Ordered() = %v. Want: %v (in the order of registration)", gotNames, wantNames) + } + if !cmp.Equal(wantNames, anonTypes.Names()) { + t.Errorf("Got: anonTypes.Names() = %v. Want: %v (in the order of registration)", gotNames, wantNames) + } +} + +func TestIsGeneric(t *testing.T) { + T := types.NewTypeParam(types.NewTypeName(token.NoPos, nil, "T", nil), types.NewInterface(nil, nil)) + + tests := []struct { + typ types.Type + want bool + }{ + { + typ: T, + want: true, + }, { + typ: types.Typ[types.Int], + want: false, + }, { + typ: types.NewArray(types.Typ[types.Int], 1), + want: false, + }, { + typ: types.NewArray(T, 1), + want: true, + }, { + typ: types.NewChan(types.SendRecv, types.Typ[types.Int]), + want: false, + }, { + typ: types.NewChan(types.SendRecv, T), + want: true, + }, { + typ: types.NewInterfaceType( + []*types.Func{ + types.NewFunc(token.NoPos, nil, "X", types.NewSignatureType( + nil, nil, nil, types.NewTuple(types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int])), nil, false, + )), + }, + []types.Type{ + types.NewNamed(types.NewTypeName(token.NoPos, nil, "myInt", nil), types.Typ[types.Int], nil), + }, + ), + want: false, + }, { + typ: types.NewInterfaceType( + []*types.Func{ + types.NewFunc(token.NoPos, nil, "X", types.NewSignatureType( + nil, nil, nil, types.NewTuple(types.NewVar(token.NoPos, nil, "x", T)), nil, false, + )), + }, + []types.Type{ + types.NewNamed(types.NewTypeName(token.NoPos, nil, "myInt", nil), types.Typ[types.Int], nil), + }, + ), + want: true, + }, { + typ: types.NewMap(types.Typ[types.Int], types.Typ[types.String]), + want: false, + }, { + typ: types.NewMap(T, types.Typ[types.String]), + want: true, + }, { + typ: types.NewMap(types.Typ[types.Int], T), + want: true, + }, { + typ: types.NewNamed(types.NewTypeName(token.NoPos, nil, "myInt", nil), types.Typ[types.Int], nil), + want: false, + }, { + typ: types.NewPointer(types.Typ[types.Int]), + want: false, + }, { + typ: types.NewPointer(T), + want: true, + }, { + typ: types.NewSlice(types.Typ[types.Int]), + want: false, + }, { + typ: types.NewSlice(T), + want: true, + }, { + typ: types.NewSignatureType( + nil, nil, nil, + types.NewTuple(types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int])), // params + types.NewTuple(types.NewVar(token.NoPos, nil, "", types.Typ[types.String])), // results + false, + ), + want: false, + }, { + typ: types.NewSignatureType( + nil, nil, nil, + types.NewTuple(types.NewVar(token.NoPos, nil, "x", T)), // params + types.NewTuple(types.NewVar(token.NoPos, nil, "", types.Typ[types.String])), // results + false, + ), + want: true, + }, { + typ: types.NewSignatureType( + nil, nil, nil, + types.NewTuple(types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int])), // params + types.NewTuple(types.NewVar(token.NoPos, nil, "", T)), // results + false, + ), + want: true, + }, { + typ: types.NewStruct([]*types.Var{ + types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int]), + }, nil), + want: false, + }, { + typ: types.NewStruct([]*types.Var{ + types.NewVar(token.NoPos, nil, "x", T), + }, nil), + want: true, + }, + } + + for _, test := range tests { + t.Run(test.typ.String(), func(t *testing.T) { + got := IsGeneric(test.typ) + if got != test.want { + t.Errorf("Got: IsGeneric(%v) = %v. Want: %v.", test.typ, got, test.want) + } + }) + } +} + +func TestCanonicalTypeParamMap(t *testing.T) { + src := `package main + type A[T any] struct{} + func (a A[T1]) Method(t T1) {} + + func Func[U any](u U) {} + ` + fset := token.NewFileSet() + f := srctesting.Parse(t, fset, src) + info, _ := srctesting.Check(t, fset, f) + + // Extract relevant information about the method Method. + methodDecl := f.Decls[1].(*ast.FuncDecl) + if methodDecl.Name.String() != "Method" { + t.Fatalf("Unexpected function at f.Decls[2] position: %q. Want: Method.", methodDecl.Name.String()) + } + method := info.Defs[methodDecl.Name] + T1 := method.Type().(*types.Signature).Params().At(0).Type().(*types.TypeParam) + if T1.Obj().Name() != "T1" { + t.Fatalf("Unexpected type of the Func's first argument: %s. Want: T1.", T1.Obj().Name()) + } + + // Extract relevant information about the standalone function Func. + funcDecl := f.Decls[2].(*ast.FuncDecl) + if funcDecl.Name.String() != "Func" { + t.Fatalf("Unexpected function at f.Decls[2] position: %q. Want: Func.", funcDecl.Name.String()) + } + fun := info.Defs[funcDecl.Name] + U := fun.Type().(*types.Signature).Params().At(0).Type().(*types.TypeParam) + if U.Obj().Name() != "U" { + t.Fatalf("Unexpected type of the Func's first argument: %s. Want: U.", U.Obj().Name()) + } + + cm := NewCanonicalTypeParamMap([]*ast.FuncDecl{methodDecl, funcDecl}, info) + + // Method's type params canonicalized to their receiver type's. + got := cm.Lookup(T1) + if got.Obj().Name() != "T" { + t.Errorf("Got canonical type parameter %q for %q. Want: T.", got, T1) + } + + // Function's type params don't need canonicalization. + got = cm.Lookup(U) + if got.Obj().Name() != "U" { + t.Errorf("Got canonical type parameter %q for %q. Want: U.", got, U) + } +} diff --git a/compiler/utils.go b/compiler/utils.go index 8b18c29c1..517be76a5 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -19,9 +19,20 @@ import ( "unicode" "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/astutil" "github.com/gopherjs/gopherjs/compiler/typesutil" ) +// We use this character as a separator in synthetic identifiers instead of a +// regular dot. This character is safe for use in JS identifiers and helps to +// visually separate components of the name when it appears in a stack trace. +const midDot = "·" + +// IsRoot returns true for the package-level context. +func (fc *funcContext) IsRoot() bool { + return fc.parent == nil +} + func (fc *funcContext) Write(b []byte) (int, error) { fc.writePos() fc.output = append(fc.output, b...) @@ -29,7 +40,7 @@ func (fc *funcContext) Write(b []byte) (int, error) { } func (fc *funcContext) Printf(format string, values ...interface{}) { - fc.Write([]byte(strings.Repeat("\t", fc.pkgCtx.indentation))) + fc.Write([]byte(fc.Indentation(0))) fmt.Fprintf(fc, format, values...) fc.Write([]byte{'\n'}) fc.Write(fc.delayedOutput) @@ -57,12 +68,31 @@ func (fc *funcContext) writePos() { } } -func (fc *funcContext) Indent(f func()) { +// Indented increases generated code indentation level by 1 for the code emitted +// from the callback f. +func (fc *funcContext) Indented(f func()) { fc.pkgCtx.indentation++ f() fc.pkgCtx.indentation-- } +// Indentation returns a sequence of "\t" characters appropriate to the current +// generated code indentation level. The `extra` parameter provides relative +// indentation adjustment. +func (fc *funcContext) Indentation(extra int) string { + return strings.Repeat("\t", fc.pkgCtx.indentation+extra) +} + +// bodyIndent returns the number of indentations necessary for the function +// body code. Generic functions need deeper indentation to account for the +// surrounding factory function. +func (fc *funcContext) bodyIndent() int { + if fc.sigTypes.IsGeneric() { + return 2 + } + return 1 +} + func (fc *funcContext) CatchOutput(indent int, f func()) []byte { origoutput := fc.output fc.output = nil @@ -79,6 +109,43 @@ func (fc *funcContext) Delayed(f func()) { fc.delayedOutput = fc.CatchOutput(0, f) } +// CollectDCEDeps captures a list of Go objects (types, functions, etc.) +// the code translated inside f() depends on. The returned list of identifiers +// can be used in dead-code elimination. +// +// Note that calling CollectDCEDeps() inside another CollectDCEDeps() call is +// not allowed. +func (fc *funcContext) CollectDCEDeps(f func()) []string { + if fc.pkgCtx.dependencies != nil { + panic(bailout(fmt.Errorf("called funcContext.CollectDependencies() inside another funcContext.CollectDependencies() call"))) + } + + fc.pkgCtx.dependencies = make(map[types.Object]bool) + defer func() { fc.pkgCtx.dependencies = nil }() + + f() + + var deps []string + for o := range fc.pkgCtx.dependencies { + qualifiedName := o.Pkg().Path() + "." + o.Name() + if typesutil.IsMethod(o) { + qualifiedName += "~" + } + deps = append(deps, qualifiedName) + } + sort.Strings(deps) + return deps +} + +// DeclareDCEDep records that the code that is currently being transpiled +// depends on a given Go object. +func (fc *funcContext) DeclareDCEDep(o types.Object) { + if fc.pkgCtx.dependencies == nil { + return // Dependencies are not being collected. + } + fc.pkgCtx.dependencies[o] = true +} + // expandTupleArgs converts a function call which argument is a tuple returned // by another function into a set of individual call arguments corresponding to // tuple elements. @@ -106,7 +173,7 @@ func (fc *funcContext) expandTupleArgs(argExprs []ast.Expr) []ast.Expr { return argExprs } - tupleVar := fc.newVariable("_tuple") + tupleVar := fc.newLocalVariable("_tuple") fc.Printf("%s = %s;", tupleVar, fc.translateExpr(argExprs[0])) argExprs = make([]ast.Expr, tuple.Len()) for i := range argExprs { @@ -118,7 +185,7 @@ func (fc *funcContext) expandTupleArgs(argExprs []ast.Expr) []ast.Expr { func (fc *funcContext) translateArgs(sig *types.Signature, argExprs []ast.Expr, ellipsis bool) []string { argExprs = fc.expandTupleArgs(argExprs) - sigTypes := signatureTypes{Sig: sig} + sigTypes := typesutil.Signature{Sig: sig} if sig.Variadic() && len(argExprs) == 0 { return []string{fmt.Sprintf("%s.nil", fc.typeName(sigTypes.VariadicType()))} @@ -134,7 +201,7 @@ func (fc *funcContext) translateArgs(sig *types.Signature, argExprs []ast.Expr, arg := fc.translateImplicitConversionWithCloning(argExpr, sigTypes.Param(i, ellipsis)).String() if preserveOrder && fc.pkgCtx.Types[argExpr].Value == nil { - argVar := fc.newVariable("_arg") + argVar := fc.newLocalVariable("_arg") fc.Printf("%s = %s;", argVar, arg) arg = argVar } @@ -229,11 +296,57 @@ func (fc *funcContext) newConst(t types.Type, value constant.Value) ast.Expr { return id } -func (fc *funcContext) newVariable(name string) string { - return fc.newVariableWithLevel(name, false) +// newLocalVariable assigns a new JavaScript variable name for the given Go +// local variable name. In this context "local" means "in scope of the current" +// functionContext. +func (fc *funcContext) newLocalVariable(name string) string { + return fc.newVariable(name, varFuncLocal) +} + +// newBlankVariable assigns a new JavaScript variable name for a blank Go +// variable, such as a `_` variable or an unnamed function parameter. +// Such variables can't be referenced by the Go code anywhere aside from where +// it is defined, so position is a good key for it. +func (fc *funcContext) newBlankVariable(pos token.Pos) string { + if !pos.IsValid() { + panic("valid position is required to assign a blank variable name") + } + if name, ok := fc.pkgCtx.blankVarNames[pos]; ok { + return name + } + + name := fc.newLocalVariable("blank") + fc.pkgCtx.blankVarNames[pos] = name + return name } -func (fc *funcContext) newVariableWithLevel(name string, pkgLevel bool) string { +// varLevel specifies at which level a JavaScript variable should be declared. +type varLevel int + +const ( + // A variable defined at a function level (e.g. local variables). + varFuncLocal varLevel = iota + // A variable that should be declared in a generic type or function factory. + // This is mainly for type parameters and generic-dependent types. + varGenericFactory + // A variable that should be declared in a package factory. This user is for + // top-level functions, types, etc. + varPackage +) + +// newVariable assigns a new JavaScript variable name for the given Go variable +// or type. +// +// If there is already a variable with the same name visible in the current +// function context (e.g. due to shadowing), the returned name will be suffixed +// with a number to prevent conflict. This is necessary because Go name +// resolution scopes differ from var declarations in JS. +// +// If pkgLevel is true, the variable is declared at the package level and added +// to this functionContext, as well as all parents, but not to the list of local +// variables. If false, it is added to this context only, as well as the list of +// local vars. +func (fc *funcContext) newVariable(name string, level varLevel) string { if name == "" { panic("newVariable: empty name") } @@ -242,7 +355,7 @@ func (fc *funcContext) newVariableWithLevel(name string, pkgLevel bool) string { i := 0 for { offset := int('a') - if pkgLevel { + if level == varPackage { offset = int('A') } j := i @@ -267,9 +380,22 @@ func (fc *funcContext) newVariableWithLevel(name string, pkgLevel bool) string { varName = fmt.Sprintf("%s$%d", name, n) } - if pkgLevel { - for c2 := fc.parent; c2 != nil; c2 = c2.parent { - c2.allVars[name] = n + 1 + // Package-level variables are registered in all outer scopes. + if level == varPackage { + for c := fc.parent; c != nil; c = c.parent { + c.allVars[name] = n + 1 + } + return varName + } + + // Generic-factory level variables are registered in outer scopes up to the + // level of the generic function or method. + if level == varGenericFactory { + for c := fc; c != nil; c = c.parent { + c.allVars[name] = n + 1 + if c.parent != nil && c.parent.genericCtx != fc.genericCtx { + break + } } return varName } @@ -278,24 +404,59 @@ func (fc *funcContext) newVariableWithLevel(name string, pkgLevel bool) string { return varName } +// newIdent declares a new Go variable with the given name and type and returns +// an *ast.Ident referring to that object. func (fc *funcContext) newIdent(name string, t types.Type) *ast.Ident { - ident := ast.NewIdent(name) - fc.setType(ident, t) - obj := types.NewVar(0, fc.pkgCtx.Pkg, name, t) - fc.pkgCtx.Uses[ident] = obj + obj := types.NewVar(token.NoPos, fc.pkgCtx.Pkg, name, t) fc.pkgCtx.objectNames[obj] = name - return ident + return fc.newIdentFor(obj) +} + +// newIdentFor creates a new *ast.Ident referring to the given Go object. +func (fc *funcContext) newIdentFor(obj types.Object) *ast.Ident { + return astutil.NewIdentFor(fc.pkgCtx.Info.Info, obj) } -func (fc *funcContext) newTypeIdent(name string, obj types.Object) *ast.Ident { - ident := ast.NewIdent(name) - fc.pkgCtx.Info.Uses[ident] = obj - return ident +// newLitFuncName generates a new synthetic name for a function literal. +func (fc *funcContext) newLitFuncName() string { + fc.funcLitCounter++ + name := &strings.Builder{} + + // If function literal is defined inside another function, qualify its + // synthetic name with the outer function to make it easier to identify. + if fc.funcObject != nil { + if recvType := fc.sigTypes.RecvTypeName(); recvType != "" { + name.WriteString(recvType) + name.WriteString(midDot) + } + name.WriteString(fc.funcObject.Name()) + name.WriteString(midDot) + } + fmt.Fprintf(name, "func%d", fc.funcLitCounter) + return name.String() +} + +// typeParamVars returns a list of JS variable names representing type given +// parameters. +func (fc *funcContext) typeParamVars(params *types.TypeParamList) []string { + vars := []string{} + for i := 0; i < params.Len(); i++ { + vars = append(vars, fc.typeName(params.At(i))) + } + + return vars } func (fc *funcContext) setType(e ast.Expr, t types.Type) ast.Expr { - fc.pkgCtx.Types[e] = types.TypeAndValue{Type: t} - return e + return astutil.SetType(fc.pkgCtx.Info.Info, t, e) +} + +func (fc *funcContext) typeCastExpr(e ast.Expr, typeExpr ast.Expr) *ast.CallExpr { + return astutil.TypeCast(fc.pkgCtx.Info.Info, e, typeExpr) +} + +func (fc *funcContext) takeAddressExpr(e ast.Expr) *ast.UnaryExpr { + return astutil.TakeAddress(fc.pkgCtx.Info.Info, e) } func (fc *funcContext) pkgVar(pkg *types.Package) string { @@ -318,13 +479,30 @@ func isVarOrConst(o types.Object) bool { return false } -func isPkgLevel(o types.Object) bool { - return o.Parent() != nil && o.Parent().Parent() == types.Universe +func isTypeParameterName(o types.Object) bool { + _, isTypeName := o.(*types.TypeName) + _, isTypeParam := o.Type().(*types.TypeParam) + return isTypeName && isTypeParam +} + +// getVarLevel returns at which level a JavaScript variable for the given object +// should be defined. The object can represent any named Go object: variable, +// type, function, etc. +func getVarLevel(o types.Object) varLevel { + if isTypeParameterName(o) { + return varGenericFactory + } + if o.Parent() != nil && o.Parent().Parent() == types.Universe { + return varPackage + } + return varFuncLocal } +// objectName returns a JS identifier corresponding to the given types.Object. +// Repeated calls for the same object will return the same name. func (fc *funcContext) objectName(o types.Object) string { - if isPkgLevel(o) { - fc.pkgCtx.dependencies[o] = true + if getVarLevel(o) == varPackage { + fc.DeclareDCEDep(o) if o.Pkg() != fc.pkgCtx.Pkg || (isVarOrConst(o) && o.Exported()) { return fc.pkgVar(o.Pkg()) + "." + o.Name() @@ -333,7 +511,7 @@ func (fc *funcContext) objectName(o types.Object) string { name, ok := fc.pkgCtx.objectNames[o] if !ok { - name = fc.newVariableWithLevel(o.Name(), isPkgLevel(o)) + name = fc.newVariable(o.Name(), getVarLevel(o)) fc.pkgCtx.objectNames[o] = name } @@ -343,19 +521,39 @@ func (fc *funcContext) objectName(o types.Object) string { return name } +// methodName returns a JS identifier (specifically, object property name) +// corresponding to the given method. +func (fc *funcContext) methodName(fun *types.Func) string { + if fun.Type().(*types.Signature).Recv() == nil { + panic(fmt.Errorf("expected a method, got a standalone function %v", fun)) + } + name := fun.Name() + // Method names are scoped to their receiver type and guaranteed to be + // unique within that, so we only need to make sure it's not a reserved keyword + if reservedKeywords[name] { + name += "$" + } + return name +} + func (fc *funcContext) varPtrName(o *types.Var) string { - if isPkgLevel(o) && o.Exported() { + if getVarLevel(o) == varPackage && o.Exported() { return fc.pkgVar(o.Pkg()) + "." + o.Name() + "$ptr" } name, ok := fc.pkgCtx.varPtrNames[o] if !ok { - name = fc.newVariableWithLevel(o.Name()+"$ptr", isPkgLevel(o)) + name = fc.newVariable(o.Name()+"$ptr", getVarLevel(o)) fc.pkgCtx.varPtrNames[o] = name } return name } +// typeName returns a JS identifier name for the given Go type. +// +// For the built-in types it returns identifiers declared in the prelude. For +// all user-defined or composite types it creates a unique JS identifier and +// will return it on all subsequent calls for the type. func (fc *funcContext) typeName(ty types.Type) string { switch t := ty.(type) { case *types.Basic: @@ -364,25 +562,70 @@ func (fc *funcContext) typeName(ty types.Type) string { if t.Obj().Name() == "error" { return "$error" } - return fc.objectName(t.Obj()) + if t.TypeArgs() == nil { + return fc.objectName(t.Obj()) + } + // Generic types require generic factory call. + args := []string{} + for i := 0; i < t.TypeArgs().Len(); i++ { + args = append(args, fc.typeName(t.TypeArgs().At(i))) + } + return fmt.Sprintf("(%s(%s))", fc.objectName(t.Obj()), strings.Join(args, ",")) + case *types.TypeParam: + o := t.Obj() + if fc.funcObject == nil { + // If the current context is not associated with a function or method, + // we processing type declaration, so we canonicalize method's type + // parameter names to their receiver counterparts. + o = fc.pkgCtx.canonicalTypeParams.Lookup(t).Obj() + } + return fc.objectName(o) case *types.Interface: if t.Empty() { return "$emptyInterface" } } - anonType, ok := fc.pkgCtx.anonTypeMap.At(ty).(*types.TypeName) - if !ok { - fc.initArgs(ty) // cause all embedded types to be registered - varName := fc.newVariableWithLevel(strings.ToLower(typeKind(ty)[5:])+"Type", true) + // For anonymous composite types, generate a synthetic package-level type + // declaration, which will be reused for all instances of this type. This + // improves performance, since runtime won't have to synthesize the same type + // repeatedly. + anonTypes := &fc.pkgCtx.anonTypes + level := varPackage + if typesutil.IsGeneric(ty) { + anonTypes = &fc.genericCtx.anonTypes + level = varGenericFactory + } + anonType := anonTypes.Get(ty) + if anonType == nil { + fc.initArgs(ty) // cause all dependency types to be registered + varName := fc.newVariable(strings.ToLower(typeKind(ty)[5:])+"Type", level) anonType = types.NewTypeName(token.NoPos, fc.pkgCtx.Pkg, varName, ty) // fake types.TypeName - fc.pkgCtx.anonTypes = append(fc.pkgCtx.anonTypes, anonType) - fc.pkgCtx.anonTypeMap.Set(ty, anonType) + anonTypes.Register(anonType, ty) } - fc.pkgCtx.dependencies[anonType] = true + fc.DeclareDCEDep(anonType) return anonType.Name() } +// importedPkgVar returns a package-level variable name for accessing an imported +// package. +// +// Allocates a new variable if this is the first call, or returns the existing +// one. The variable is based on the package name (implicitly derived from the +// `package` declaration in the imported package, or explicitly assigned by the +// import decl in the importing source file). +// +// Returns the allocated variable name. +func (fc *funcContext) importedPkgVar(pkg *types.Package) string { + if pkgVar, ok := fc.pkgCtx.pkgVars[pkg.Path()]; ok { + return pkgVar // Already registered. + } + + pkgVar := fc.newVariable(pkg.Name(), varPackage) + fc.pkgCtx.pkgVars[pkg.Path()] = pkgVar + return pkgVar +} + func (fc *funcContext) externalize(s string, t types.Type) string { if typesutil.IsJsObject(t) { return s @@ -461,6 +704,8 @@ func toJavaScriptType(t *types.Basic) string { return "Int32" case types.UnsafePointer: return "UnsafePointer" + case types.UntypedString: + return "String" default: name := t.String() return strings.ToUpper(name[:1]) + name[1:] @@ -695,7 +940,15 @@ func rangeCheck(pattern string, constantIndex, array bool) string { } func encodeIdent(name string) string { - return strings.Replace(url.QueryEscape(name), "%", "$", -1) + // Quick-and-dirty way to make any string safe for use as an identifier in JS. + name = url.QueryEscape(name) + // We use unicode middle dot as a visual separator in synthetic identifiers. + // It is safe for use in a JS identifier, so we un-encode it for readability. + name = strings.ReplaceAll(name, "%C2%B7", midDot) + // QueryEscape uses '%' before hex-codes of escaped characters, which is not + // allowed in a JS identifier, use '$' instead. + name = strings.ReplaceAll(name, "%", "$") + return name } // formatJSStructTagVal returns JavaScript code for accessing an object's property @@ -723,56 +976,6 @@ func formatJSStructTagVal(jsTag string) string { return "." + jsTag } -// signatureTypes is a helper that provides convenient access to function -// signature type information. -type signatureTypes struct { - Sig *types.Signature -} - -// RequiredParams returns the number of required parameters in the function signature. -func (st signatureTypes) RequiredParams() int { - l := st.Sig.Params().Len() - if st.Sig.Variadic() { - return l - 1 // Last parameter is a slice of variadic params. - } - return l -} - -// VariadicType returns the slice-type corresponding to the signature's variadic -// parameter, or nil of the signature is not variadic. With the exception of -// the special-case `append([]byte{}, "string"...)`, the returned type is -// `*types.Slice` and `.Elem()` method can be used to get the type of individual -// arguments. -func (st signatureTypes) VariadicType() types.Type { - if !st.Sig.Variadic() { - return nil - } - return st.Sig.Params().At(st.Sig.Params().Len() - 1).Type() -} - -// Returns the expected argument type for the i'th argument position. -// -// This function is able to return correct expected types for variadic calls -// both when ellipsis syntax (e.g. myFunc(requiredArg, optionalArgSlice...)) -// is used and when optional args are passed individually. -// -// The returned types may differ from the actual argument expression types if -// there is an implicit type conversion involved (e.g. passing a struct into a -// function that expects an interface). -func (st signatureTypes) Param(i int, ellipsis bool) types.Type { - if i < st.RequiredParams() { - return st.Sig.Params().At(i).Type() - } - if !st.Sig.Variadic() { - // This should never happen if the code was type-checked successfully. - panic(fmt.Errorf("Tried to access parameter %d of a non-variadic signature %s", i, st.Sig)) - } - if ellipsis { - return st.VariadicType() - } - return st.VariadicType().(*types.Slice).Elem() -} - // ErrorAt annotates an error with a position in the source code. func ErrorAt(err error, fset *token.FileSet, pos token.Pos) error { return fmt.Errorf("%s: %w", fset.Position(pos), err) @@ -831,3 +1034,13 @@ func bailingOut(err interface{}) (*FatalError, bool) { fe, ok := err.(*FatalError) return fe, ok } + +func removeMatching[T comparable](haystack []T, needle T) []T { + var result []T + for _, el := range haystack { + if el != needle { + result = append(result, el) + } + } + return result +} diff --git a/go.mod b/go.mod index d1e88d3d5..3985b8bc2 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 @@ -14,13 +14,14 @@ require ( github.com/spf13/pflag v1.0.5 github.com/visualfc/goembed v0.3.3 golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 - golang.org/x/sync v0.3.0 - golang.org/x/sys v0.10.0 - golang.org/x/tools v0.11.0 + golang.org/x/exp/typeparams v0.0.0-20230127193734-31bee513bff7 + golang.org/x/sync v0.4.0 + golang.org/x/sys v0.13.0 + golang.org/x/tools v0.14.0 ) require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect - golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect ) diff --git a/go.sum b/go.sum index 80bdd5b30..47039c802 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= @@ -272,6 +272,10 @@ 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 v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/exp/typeparams v0.0.0-20230127193734-31bee513bff7 h1:b1tzrw2iCf2wUlbWCLmOD3SdX6hiDxxfanz/zrnIEOs= +golang.org/x/exp/typeparams v0.0.0-20230127193734-31bee513bff7/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= @@ -297,7 +301,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.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= 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= @@ -357,8 +361,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.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 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= @@ -403,8 +407,8 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -469,14 +473,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.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= 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/tests/gorepo/run.go b/tests/gorepo/run.go index 0ed56a7fb..168c0a63b 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -110,9 +110,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"}, @@ -122,7 +119,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"}, @@ -135,7 +131,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"}, @@ -150,12 +145,28 @@ 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/issue49665.go": {category: other, desc: "attempts to pass -gcflags=-G=3 to enable generics, GopherJS doesn't expect the flag; re-enable in Go 1.19 where the flag is removed"}, "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"}, + "typeparam/issue51733.go": {category: usesUnsupportedPackage, desc: "unsafe: uintptr to struct pointer conversion is unsupported"}, + "typeparam/chans.go": {category: neverTerminates, desc: "uses runtime.SetFinalizer() and runtime.GC()."}, + + // 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/absdiff2.go": {category: generics, desc: "missing support for unary minus operator"}, + "typeparam/absdiff3.go": {category: generics, desc: "missing support for unary minus operator"}, + "typeparam/boundmethod.go": {category: generics, desc: "missing support for method expressions with a type param"}, + "typeparam/double.go": {category: generics, desc: "missing support for range over type parameter"}, + "typeparam/index2.go": {category: generics, desc: "missing index operator support for generic types"}, + "typeparam/issue47716.go": {category: generics, desc: "unsafe.Sizeof() doesn't work with generic types"}, + "typeparam/issue48453.go": {category: generics, desc: "missing support for range over type parameter"}, + "typeparam/issue51303.go": {category: generics, desc: "missing support for range over type parameter"}, + "typeparam/nested.go": {category: generics, desc: "missing comparison operator support for generic types"}, + "typeparam/typeswitch2.go": {category: generics, desc: "complex types have different print() format"}, + "typeparam/typeswitch3.go": {category: generics, desc: "missing support for type switching on generic types"}, + "typeparam/typeswitch5.go": {category: generics, desc: "different print() format for floating point types"}, } type failCategory uint8 @@ -169,6 +180,7 @@ const ( unsureIfGopherJSSupportsThisFeature lowLevelRuntimeDifference // JavaScript runtime behaves differently from Go in ways that are difficult to work around. notApplicable // Test that doesn't need to run under GopherJS; it doesn't apply to the Go language in a general way. + generics // Test requires generics support. ) type failReason struct { @@ -196,7 +208,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 @@ -634,16 +646,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: @@ -693,6 +709,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:]...) @@ -1257,3 +1286,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 +} diff --git a/tests/js_test.go b/tests/js_test.go index 7680749dc..6563098df 100644 --- a/tests/js_test.go +++ b/tests/js_test.go @@ -326,6 +326,7 @@ func TestInternalizeStruct(t *testing.T) { t.Errorf("Mismatch (-want +got):\n%s", diff) } } + func TestInternalizeStructUnexportedFields(t *testing.T) { type Person struct { Name string @@ -731,19 +732,19 @@ func TestReflection(t *testing.T) { s := S{o} v := reflect.ValueOf(&s).Elem() - if v.Field(0).Interface().(*js.Object).Get("answer").Int() != 42 { - t.Fail() + if got := v.Field(0).Interface().(*js.Object).Get("answer").Int(); got != 42 { + t.Errorf("Got: Accessing JS object property via reflect.Value.Interface() returned %v. Want: 42.", got) } - if v.Field(0).MethodByName("Get").Call([]reflect.Value{reflect.ValueOf("answer")})[0].Interface().(*js.Object).Int() != 42 { - t.Fail() + if got := v.Field(0).MethodByName("Get").Call([]reflect.Value{reflect.ValueOf("answer")})[0].Interface().(*js.Object).Int(); got != 42 { + t.Errorf("Got: accessing JS object property via reflect.Value.Call('Get') returned %v. Want: 42.", got) } v.Field(0).Set(reflect.ValueOf(js.Global.Call("eval", "({ answer: 100 })"))) - if s.Field.Get("answer").Int() != 100 { - t.Fail() + if got := s.Field.Get("answer").Int(); got != 100 { + t.Errorf("Got: setting a field to JS object via reflection failed, got %s. Want: 100.", got) } - if fmt.Sprintf("%+v", s) != "{Field:[object Object]}" { - t.Fail() + if got, want := fmt.Sprintf("%+v", s), "{Field:[object Object]}"; got != want { + t.Errorf("Got: Formatting JS object returned %q. Want: %q.", got, want) } } diff --git a/tests/typeparams/arithmetics_test.go b/tests/typeparams/arithmetics_test.go new file mode 100644 index 000000000..8ac534203 --- /dev/null +++ b/tests/typeparams/arithmetics_test.go @@ -0,0 +1,453 @@ +package typeparams_test + +import ( + "fmt" + "go/token" + "math/bits" + "testing" + + "golang.org/x/exp/constraints" +) + +type arithmetic interface { + constraints.Integer | constraints.Float | constraints.Complex +} + +type addable interface { + arithmetic | string +} + +type testCaseI interface { + Run(t *testing.T) + String() string +} + +type testCase[T comparable] struct { + op func(x, y T) T + opName token.Token + x T + y T + want T +} + +func (tc *testCase[T]) Run(t *testing.T) { + got := tc.op(tc.x, tc.y) + if got != tc.want { + t.Errorf("Got: %v %v %v = %v. Want: %v.", tc.x, tc.opName, tc.y, got, tc.want) + } +} + +func (tc *testCase[T]) String() string { + return fmt.Sprintf("%T/%v%v%v", tc.x, tc.x, tc.opName, tc.y) +} + +func add[T addable](x, y T) T { + return x + y +} + +func addTC[T addable](x, y, want T) *testCase[T] { + return &testCase[T]{ + op: add[T], + opName: token.ADD, + x: x, + y: y, + want: want, + } +} + +func TestAdd(t *testing.T) { + tests := []testCaseI{ + addTC[int](1, 2, 3), + addTC[uint](1, 2, 3), + addTC[uintptr](1, 2, 3), + addTC[int8](1, 2, 3), + addTC[int16](1, 2, 3), + addTC[int32](1, 2, 3), + addTC[uint8](1, 2, 3), + addTC[uint16](1, 2, 3), + addTC[uint32](1, 2, 3), + addTC[int8](127, 2, -127), // Overflow. + addTC[uint8](255, 2, 1), // Overflow. + addTC[float32](1.5, 1.1, 2.6), + addTC[float64](1.5, 1.1, 2.6), + addTC[int64](0x00000030FFFFFFFF, 0x0000000100000002, 0x0000003200000001), + addTC[uint64](0x00000030FFFFFFFF, 0x0000000100000002, 0x0000003200000001), + addTC[string]("abc", "def", "abcdef"), + addTC[complex64](1+2i, 3+4i, 4+6i), + addTC[complex128](1+2i, 3+4i, 4+6i), + } + + for _, test := range tests { + t.Run(test.String(), test.Run) + } +} + +func subtract[T arithmetic](x, y T) T { + return x - y +} + +func subTC[T arithmetic](x, y, want T) *testCase[T] { + return &testCase[T]{ + op: subtract[T], + opName: token.SUB, + x: x, + y: y, + want: want, + } +} + +func TestSubtract(t *testing.T) { + tests := []testCaseI{ + subTC[int](3, 1, 2), + subTC[uint](3, 1, 2), + subTC[uintptr](3, 1, 2), + subTC[int8](3, 1, 2), + subTC[int16](3, 1, 2), + subTC[int32](3, 1, 2), + subTC[uint8](3, 1, 2), + subTC[uint16](3, 1, 2), + subTC[uint32](3, 1, 2), + subTC[int8](-127, 2, 127), // Overflow. + subTC[uint8](1, 2, 255), // Overflow. + subTC[float32](2.5, 1.4, 1.1), + subTC[float64](2.5, 1.4, 1.1), + subTC[int64](0x0000003200000001, 0x0000000100000002, 0x00000030FFFFFFFF), + subTC[uint64](0x0000003200000001, 0x0000000100000002, 0x00000030FFFFFFFF), + subTC[complex64](10+11i, 2+1i, 8+10i), + subTC[complex128](10+11i, 2+1i, 8+10i), + } + + for _, test := range tests { + t.Run(test.String(), test.Run) + } +} + +func mul[T arithmetic](x, y T) T { + return x * y +} + +func mulTC[T arithmetic](x, y, want T) *testCase[T] { + return &testCase[T]{ + op: mul[T], + opName: token.MUL, + x: x, + y: y, + want: want, + } +} + +func TestMul(t *testing.T) { + tests := []testCaseI{ + mulTC[int](2, 3, 6), + mulTC[uint](2, 3, 6), + mulTC[uintptr](2, 3, 6), + mulTC[int8](2, 3, 6), + mulTC[int16](2, 3, 6), + mulTC[int32](2, 3, 6), + mulTC[uint8](2, 3, 6), + mulTC[uint16](2, 3, 6), + mulTC[uint32](2, 3, 6), + mulTC[int8](127, 3, 125), // Overflow. + mulTC[uint8](250, 3, 238), // Overflow. + mulTC[float32](2.5, 1.4, 3.5), + mulTC[float64](2.5, 1.4, 3.5), + mulTC[int64](0x0000003200000001, 0x0000000100000002, 0x0000006500000002), + mulTC[uint64](0x0000003200000001, 0x0000000100000002, 0x0000006500000002), + mulTC[complex64](1+2i, 3+4i, -5+10i), + mulTC[complex128](1+2i, 3+4i, -5+10i), + } + + for _, test := range tests { + t.Run(test.String(), test.Run) + } +} + +func div[T arithmetic](x, y T) T { + return x / y +} + +func divTC[T arithmetic](x, y, want T) *testCase[T] { + return &testCase[T]{ + op: div[T], + opName: token.QUO, + x: x, + y: y, + want: want, + } +} + +func TestDiv(t *testing.T) { + tests := []testCaseI{ + divTC[int](7, 2, 3), + divTC[uint](7, 2, 3), + divTC[uintptr](7, 2, 3), + divTC[int8](7, 2, 3), + divTC[int16](7, 2, 3), + divTC[int32](7, 2, 3), + divTC[uint8](7, 2, 3), + divTC[uint16](7, 2, 3), + divTC[uint32](7, 2, 3), + divTC[float32](3.5, 2.5, 1.4), + divTC[float64](3.5, 2.5, 1.4), + divTC[int64](0x0000006500000003, 0x0000003200000001, 2), + divTC[uint64](0x0000006500000003, 0x0000003200000001, 2), + divTC[complex64](-5+10i, 1+2i, 3+4i), + divTC[complex128](-5+10i, 1+2i, 3+4i), + } + + for _, test := range tests { + t.Run(test.String(), test.Run) + } +} + +func rem[T constraints.Integer](x, y T) T { + return x % y +} + +func remTC[T constraints.Integer](x, y, want T) *testCase[T] { + return &testCase[T]{ + op: rem[T], + opName: token.REM, + x: x, + y: y, + want: want, + } +} + +func TestRemainder(t *testing.T) { + tests := []testCaseI{ + remTC[int](7, 2, 1), + remTC[uint](7, 2, 1), + remTC[uintptr](7, 2, 1), + remTC[int8](7, 2, 1), + remTC[int16](7, 2, 1), + remTC[int32](7, 2, 1), + remTC[uint8](7, 2, 1), + remTC[uint16](7, 2, 1), + remTC[uint32](7, 2, 1), + remTC[int64](0x0000006500000003, 0x0000003200000001, 0x100000001), + remTC[uint64](0x0000006500000003, 0x0000003200000001, 0x100000001), + } + + for _, test := range tests { + t.Run(test.String(), test.Run) + } +} + +func and[T constraints.Integer](x, y T) T { + return x & y +} + +func andTC[T constraints.Integer](x, y, want T) *testCase[T] { + return &testCase[T]{ + op: and[T], + opName: token.AND, + x: x, + y: y, + want: want, + } +} + +func TestBitwiseAnd(t *testing.T) { + tests := []testCaseI{ + andTC[int](0x0011, 0x0101, 0x0001), + andTC[uint](0x0011, 0x0101, 0x0001), + andTC[uintptr](0x0011, 0x0101, 0x0001), + andTC[int8](0x11, 0x01, 0x01), + andTC[int16](0x0011, 0x0101, 0x0001), + andTC[int32](0x0011, 0x0101, 0x0001), + andTC[uint8](0x11, 0x01, 0x01), + andTC[uint16](0x0011, 0x0101, 0x0001), + andTC[uint32](0x0011, 0x0101, 0x0001), + andTC[int64](0x0000001100000011, 0x0000010100000101, 0x0000000100000001), + andTC[uint64](0x0000001100000011, 0x0000010100000101, 0x0000000100000001), + } + + for _, test := range tests { + t.Run(test.String(), test.Run) + } +} + +func or[T constraints.Integer](x, y T) T { + return x | y +} + +func orTC[T constraints.Integer](x, y, want T) *testCase[T] { + return &testCase[T]{ + op: or[T], + opName: token.OR, + x: x, + y: y, + want: want, + } +} + +func TestBitwiseOr(t *testing.T) { + tests := []testCaseI{ + orTC[int](0x0011, 0x0101, 0x0111), + orTC[uint](0x0011, 0x0101, 0x0111), + orTC[uintptr](0x0011, 0x0101, 0x0111), + orTC[int8](0x11, 0x01, 0x11), + orTC[int16](0x0011, 0x0101, 0x0111), + orTC[int32](0x0011, 0x0101, 0x0111), + orTC[uint8](0x11, 0x01, 0x11), + orTC[uint16](0x0011, 0x0101, 0x0111), + orTC[uint32](0x0011, 0x0101, 0x0111), + orTC[int64](0x0000001100000011, 0x0000010100000101, 0x0000011100000111), + orTC[uint64](0x0000001100000011, 0x0000010100000101, 0x0000011100000111), + } + + for _, test := range tests { + t.Run(test.String(), test.Run) + } +} + +func xor[T constraints.Integer](x, y T) T { + return x ^ y +} + +func xorTC[T constraints.Integer](x, y, want T) *testCase[T] { + return &testCase[T]{ + op: xor[T], + opName: token.XOR, + x: x, + y: y, + want: want, + } +} + +func TestBitwiseXor(t *testing.T) { + tests := []testCaseI{ + xorTC[int](0x0011, 0x0101, 0x0110), + xorTC[uint](0x0011, 0x0101, 0x0110), + xorTC[uintptr](0x0011, 0x0101, 0x0110), + xorTC[int8](0x11, 0x01, 0x10), + xorTC[int16](0x0011, 0x0101, 0x0110), + xorTC[int32](0x0011, 0x0101, 0x0110), + xorTC[uint8](0x11, 0x01, 0x10), + xorTC[uint16](0x0011, 0x0101, 0x0110), + xorTC[uint32](0x0011, 0x0101, 0x0110), + xorTC[int64](0x0000001100000011, 0x0000010100000101, 0x0000011000000110), + xorTC[uint64](0x0000001100000011, 0x0000010100000101, 0x0000011000000110), + } + + for _, test := range tests { + t.Run(test.String(), test.Run) + } +} + +func andNot[T constraints.Integer](x, y T) T { + return x &^ y +} + +func andNotTC[T constraints.Integer](x, y, want T) *testCase[T] { + return &testCase[T]{ + op: andNot[T], + opName: token.AND_NOT, + x: x, + y: y, + want: want, + } +} + +func TestBitwiseAndNot(t *testing.T) { + tests := []testCaseI{ + andNotTC[int](0x0011, 0x0101, 0x0010), + andNotTC[uint](0x0011, 0x0101, 0x0010), + andNotTC[uintptr](0x0011, 0x0101, 0x0010), + andNotTC[int8](0x11, 0x01, 0x10), + andNotTC[int16](0x0011, 0x0101, 0x0010), + andNotTC[int32](0x0011, 0x0101, 0x0010), + andNotTC[uint8](0x11, 0x01, 0x10), + andNotTC[uint16](0x0011, 0x0101, 0x0010), + andNotTC[uint32](0x0011, 0x0101, 0x0010), + andNotTC[int64](0x0000001100000011, 0x0000010100000101, 0x0000001000000010), + andNotTC[uint64](0x0000001100000011, 0x0000010100000101, 0x0000001000000010), + } + + for _, test := range tests { + t.Run(test.String(), test.Run) + } +} + +func leftShift[T constraints.Integer](x, y T) T { + return x << y +} + +func leftShiftTC[T constraints.Integer](x, y, want T) *testCase[T] { + return &testCase[T]{ + op: leftShift[T], + opName: token.SHL, + x: x, + y: y, + want: want, + } +} + +func TestBitwiseShitLeft(t *testing.T) { + tests := []testCaseI{ + leftShiftTC[int8](0x48, 1, -0x70), + leftShiftTC[int16](0x4008, 1, -0x7ff0), + leftShiftTC[int32](0x40000008, 1, -0x7ffffff0), + leftShiftTC[uint8](0x88, 1, 0x10), + leftShiftTC[uint16](0x8008, 1, 0x0010), + leftShiftTC[uint32](0x80000008, 1, 0x00000010), + leftShiftTC[int64](0x4000000000000008, 1, -0x7ffffffffffffff0), + leftShiftTC[uint64](0x8000000000000008, 1, 0x0000000000000010), + leftShiftTC[uint32](0xFFFFFFFF, 32, 0), + leftShiftTC[int32](-0x80000000, 32, 0), + } + + if bits.UintSize == 32 { + tests = append(tests, + leftShiftTC[int](0x40000008, 1, -0x7ffffff0), + leftShiftTC[uint](0x80000008, 1, 0x00000010), + leftShiftTC[uintptr](0x80000008, 1, 0x00000010), + ) + } + + for _, test := range tests { + t.Run(test.String(), test.Run) + } +} + +func rightShift[T constraints.Integer](x, y T) T { + return x >> y +} + +func rightShiftTC[T constraints.Integer](x, y, want T) *testCase[T] { + return &testCase[T]{ + op: rightShift[T], + opName: token.SHR, + x: x, + y: y, + want: want, + } +} + +func TestBitwiseShitRight(t *testing.T) { + tests := []testCaseI{ + rightShiftTC[int8](-0x70, 1, -0x38), + rightShiftTC[int16](-0x7ff0, 1, -0x3ff8), + rightShiftTC[int32](-0x7ffffff0, 1, -0x3ffffff8), + rightShiftTC[uint8](0x80, 1, 0x40), + rightShiftTC[uint16](0x8010, 1, 0x4008), + rightShiftTC[uint32](0x80000010, 1, 0x40000008), + rightShiftTC[int64](-0x7ffffffffffffff0, 1, -0x3ffffffffffffff8), + rightShiftTC[uint64](0x8000000000000010, 1, 0x4000000000000008), + rightShiftTC[uint32](0xFFFFFFFF, 32, 0), + rightShiftTC[int32](-0x80000000, 32, -1), + } + + if bits.UintSize == 32 { + tests = append(tests, + rightShiftTC[int](-0x7ffffff0, 1, -0x3ffffff8), + rightShiftTC[uint](0x80000010, 1, 0x40000008), + rightShiftTC[uintptr](0x80000010, 1, 0x40000008), + ) + } + + for _, test := range tests { + t.Run(test.String(), test.Run) + } +} diff --git a/tests/typeparams/blocking_test.go b/tests/typeparams/blocking_test.go new file mode 100644 index 000000000..3abdbbd8c --- /dev/null +++ b/tests/typeparams/blocking_test.go @@ -0,0 +1,21 @@ +package typeparams_test + +import ( + "runtime" + "testing" +) + +func _GenericBlocking[T any]() string { + runtime.Gosched() + return "Hello, world." +} + +// TestBlocking verifies that a generic function correctly resumes after a +// blocking operation. +func TestBlocking(t *testing.T) { + got := _GenericBlocking[any]() + want := "Hello, world." + if got != want { + t.Fatalf("Got: _GenericBlocking[any]() = %q. Want: %q.", got, want) + } +} diff --git a/tests/typeparams/builtins_test.go b/tests/typeparams/builtins_test.go new file mode 100644 index 000000000..66b379935 --- /dev/null +++ b/tests/typeparams/builtins_test.go @@ -0,0 +1,258 @@ +package typeparams_test + +import ( + "fmt" + "reflect" + "testing" +) + +func TestMake(t *testing.T) { + t.Run("slice", func(t *testing.T) { + tests := []struct { + slice []int + wantStr string + wantLen int + wantCap int + }{{ + slice: make([]int, 1), + wantStr: "[]int{0}", + wantLen: 1, + wantCap: 1, + }, { + slice: make([]int, 1, 2), + wantStr: "[]int{0}", + wantLen: 1, + wantCap: 2, + }} + + for i, test := range tests { + t.Run(fmt.Sprint(i), func(t *testing.T) { + if got := fmt.Sprintf("%#v", test.slice); got != test.wantStr { + t.Errorf("Got: fmt.Sprint(%v) = %q. Want: %q.", test.slice, got, test.wantStr) + } + if got := len(test.slice); got != test.wantLen { + t.Errorf("Got: len(%v) = %d. Want: %d.", test.slice, got, test.wantLen) + } + if got := cap(test.slice); got != test.wantCap { + t.Errorf("Got: cap(%v) = %d. Want: %d.", test.slice, got, test.wantCap) + } + }) + } + }) + + t.Run("map", func(t *testing.T) { + tests := []map[int]int{ + make(map[int]int), + make(map[int]int, 1), + } + + for i, test := range tests { + t.Run(fmt.Sprint(i), func(t *testing.T) { + want := "map[int]int{}" + got := fmt.Sprintf("%#v", test) + if want != got { + t.Errorf("Got: fmt.Sprint(%v) = %q. Want: %q.", test, got, want) + } + }) + } + }) + + t.Run("chan", func(t *testing.T) { + tests := []struct { + ch chan int + wantCap int + }{{ + ch: make(chan int), + wantCap: 0, + }, { + ch: make(chan int, 1), + wantCap: 1, + }} + + for i, test := range tests { + t.Run(fmt.Sprint(i), func(t *testing.T) { + wantStr := "chan int" + if got := fmt.Sprintf("%T", test.ch); got != wantStr { + t.Errorf("Got: fmt.Sprint(%v) = %q. Want: %q.", test.ch, got, wantStr) + } + if got := cap(test.ch); got != test.wantCap { + t.Errorf("Got: cap(%v) = %d. Want: %d.", test.ch, got, test.wantCap) + } + }) + } + }) +} + +func _len[T []int | *[3]int | map[int]int | chan int | string](x T) int { + return len(x) +} + +func TestLen(t *testing.T) { + ch := make(chan int, 2) + ch <- 1 + + tests := []struct { + desc string + got int + want int + }{{ + desc: "string", + got: _len("abcd"), + want: 4, + }, { + desc: "[]int", + got: _len([]int{1, 2, 3}), + want: 3, + }, { + desc: "[3]int", + got: _len(&[3]int{1}), + want: 3, + }, { + desc: "map[int]int", + got: _len(map[int]int{1: 1, 2: 2}), + want: 2, + }, { + desc: "chan int", + got: _len(ch), + want: 1, + }} + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + if test.got != test.want { + t.Errorf("Got: len() = %d. Want: %d.", test.got, test.want) + } + }) + } +} + +func _cap[T []int | *[3]int | [3]int | chan int](x T) int { + return cap(x) +} + +func TestCap(t *testing.T) { + ch := make(chan int, 2) + ch <- 1 + + tests := []struct { + desc string + got int + want int + }{{ + desc: "[]int", + got: _cap([]int{1, 2, 3}), + want: 3, + }, { + desc: "*[3]int", + got: _cap(&[3]int{1}), + want: 3, + }, { + desc: "[3]int", + got: _cap([3]int{1}), + want: 3, + }, { + desc: "chan int", + got: _cap(ch), + want: 2, + }} + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + if test.got != test.want { + t.Errorf("Got: len() = %d. Want: %d.", test.got, test.want) + } + }) + } +} + +func _new[T any]() *T { + return new(T) +} + +func TestNew(t *testing.T) { + type S struct{ i int } + + tests := []struct { + desc string + got any + want any + }{{ + desc: "struct S", + got: *_new[S](), + want: S{}, + }, { + desc: "[3]int", + got: *_new[[3]int](), + want: [3]int{}, + }, { + desc: "int", + got: *_new[int](), + want: int(0), + }} + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + if !reflect.DeepEqual(test.got, test.want) { + t.Errorf("Got: new(%T) = %#v. Want: %#v.", test.want, test.got, test.want) + } + }) + } +} + +func _append[E any, S []E](s S, e E) S { + return append(s, e) +} + +func TestAppend(t *testing.T) { + got := _append([]int{1, 2}, 3) + want := []int{1, 2, 3} + + if !reflect.DeepEqual(got, want) { + t.Errorf("Got: append(...) = %#v. Want: %#v", got, want) + } +} + +func _delete[K comparable, M map[K]string | map[K]int](m M, k K) { + delete(m, k) +} + +func TestDelete(t *testing.T) { + got := map[int]string{1: "a", 2: "b"} + delete(got, 1) + want := map[int]string{2: "b"} + + if !reflect.DeepEqual(got, want) { + t.Errorf("Got: delete(...) = %#v. Want: %#v", got, want) + } +} + +func _copy[D []byte, S []byte | string](dst D, src S) int { + return copy(dst, src) +} + +func TestCopy(t *testing.T) { + t.Run("string", func(t *testing.T) { + src := "abc" + got := make([]byte, 3) + n := _copy(got, src) + want := []byte{'a', 'b', 'c'} + if !reflect.DeepEqual(want, got) { + t.Errorf("Got: copy result: %v. Want: %v", got, want) + } + if n != 3 { + t.Errorf("Got: copied %d elements. Want: 3", n) + } + }) + t.Run("slice", func(t *testing.T) { + src := []byte{'a', 'b', 'c', 'd'} + got := make([]byte, 3) + n := _copy(got, src) + want := []byte{'a', 'b', 'c'} + if !reflect.DeepEqual(want, got) { + t.Errorf("Got: copy result: %v. Want: %v", got, want) + } + if n != 3 { + t.Errorf("Got: copied %d elements. Want: 3", n) + } + }) +} diff --git a/tests/typeparams/conversion_test.go b/tests/typeparams/conversion_test.go new file mode 100644 index 000000000..2d9bf02bf --- /dev/null +++ b/tests/typeparams/conversion_test.go @@ -0,0 +1,415 @@ +package typeparams_test + +import ( + "fmt" + "math" + "reflect" + "runtime" + "strings" + "testing" + + "github.com/gopherjs/gopherjs/js" +) + +// checkConversion is a general type conversion result checker. +// +// The expectation is that got and want have the same underlying Go type, and +// they contain equal values. Src is the original value before type conversion, +// provided for error message purposes. +// +// Note that this function is suitable for checking most conversion results +// except conversion to interfaces. This is because use of reflect APIs requires +// conversion to `any` interface, which must be assumed correct for this test to +// be meaningful. +func checkConversion(t *testing.T, src, got, want any) { + t.Helper() + if reflect.TypeOf(got) != reflect.TypeOf(want) { + t.Errorf("Got: %v. Want: converted type is: %v.", reflect.TypeOf(got), reflect.TypeOf(want)) + } + + if !reflect.DeepEqual(want, got) { + t.Errorf("Got: %[1]T(%#[1]v). Want: %[2]T(%#[2]v) convert to %[3]T(%#[3]v).", got, src, want) + } +} + +// conversionTest is a common interface for type conversion test cases. +type conversionTest interface { + Run(t *testing.T) +} + +type ( // Named types for use in conversion test cases. + i64 int64 + i32 int32 + f64 float64 + f32 float32 + c64 complex64 + c128 complex128 + str string + strPtr *string + b bool + st struct { + s string + i int + } + st2 st + stPtr *st + sl []byte + arr [3]byte + arrPtr *[3]byte + m map[string]string + ch chan string + fun func() int +) + +type numeric interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | + ~float32 | ~float64 | + ~uintptr +} + +type numericConversion[srcType numeric, dstType numeric] struct { + src srcType + want dstType + quirk bool +} + +func (tc numericConversion[srcType, dstType]) Run(t *testing.T) { + if tc.quirk && runtime.Compiler != "gopherjs" { + t.Skip("GopherJS-only test") + } + + checkConversion(t, tc.src, dstType(tc.src), tc.want) +} + +type complex interface { + ~complex64 | ~complex128 +} + +type complexConversion[srcType complex, dstType complex] struct { + src srcType + want dstType +} + +func (tc complexConversion[srcType, dstType]) Run(t *testing.T) { + checkConversion(t, tc.src, dstType(tc.src), tc.want) +} + +type stringLike interface { + // Ideally, we would test conversions from all integer types. unfortunately, + // that trips up the stringintconv check in `go vet` that is ran by `go test` + // by default. Unfortunately, there is no way to selectively suppress that + // check. + // ~int | ~int8 | ~int16 | ~int32 | ~int64 | + // ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | + // ~uintptr | + byte | rune | + ~[]byte | ~[]rune | ~string +} + +type stringConversion[srcType stringLike, dstType ~string] struct { + src srcType + want dstType +} + +func (tc stringConversion[srcType, dstType]) Run(t *testing.T) { + checkConversion(t, tc.src, dstType(tc.src), tc.want) +} + +type boolConversion[srcType ~bool, dstType ~bool] struct { + src srcType + want dstType +} + +func (tc boolConversion[srcType, dstType]) Run(t *testing.T) { + checkConversion(t, tc.src, dstType(tc.src), tc.want) +} + +// asString returns a string that reflects internal representation of the object. +// +// There is not specific guarantees about the string format, expect that if two +// strings match, the two objects _almost certainly_ are deeply equal. +func asString(o *js.Object) string { + f := js.Global.Get("Function").New("o", ` + const seen = []; + // JSON.stringify can't deal with circular references, which GopherJS objects + // can have. So when the same object is seen more than once we replace it with + // a string stub. + const suppressCycles = (key, value) => { + if (typeof value !== 'object') { + return value; + } + const idx = seen.indexOf(value); + if (idx !== -1) { + return "[Cycle " + idx + "]" + } + seen.push(value); + return value; + } + return JSON.stringify(o, suppressCycles); + `) + return f.Invoke(o).String() +} + +type interfaceConversion[srcType any] struct { + src srcType +} + +func (tc interfaceConversion[srcType]) Run(t *testing.T) { + // All of the following expressions are semantically equivalent, but may be + // compiled by GopherJS differently, so we test all of them. + var got1 any + got1 = tc.src + var got2 any = tc.src + var got3 any = any(tc.src) + + for i, got := range []any{got1, got2, got3} { + t.Run(fmt.Sprint(i), func(t *testing.T) { + checkConversion(t, tc.src, got, tc.src) + + concrete := got.(srcType) // Must not panic. + if runtime.Compiler == "gopherjs" { + // Can't use reflect.DeepEqual() here because it itself relies on + // conversion to interface, so instead we do some JS-level introspection. + srcRepr := asString(js.InternalObject(tc.src)) + concreteRepr := asString(js.InternalObject(concrete)) + if srcRepr == "" { + t.Fatalf("Got: internal representation of the original value is empty. Want: not empty.") + } + if concreteRepr != srcRepr { + t.Errorf("Got: result of type assertion %q is not equal to the original value %q. Want: values are equal.", concreteRepr, srcRepr) + } + } + }) + } +} + +type sliceConversion[elType any, srcType ~[]elType, dstType ~[]elType] struct { + src srcType + want dstType +} + +func (tc sliceConversion[elType, srcType, dstType]) Run(t *testing.T) { + checkConversion(t, tc.src, dstType(tc.src), tc.want) +} + +type stringToSliceConversion[dstType []byte | []rune] struct { + src string + want dstType +} + +func (tc stringToSliceConversion[dstType]) Run(t *testing.T) { + checkConversion(t, tc.src, dstType(tc.src), tc.want) +} + +type sliceToArrayPtrConversion[elType any, srcType ~[]elType, dstType ~*[3]elType | ~*[0]elType] struct { + src srcType + want dstType + wantPanic string +} + +func (tc sliceToArrayPtrConversion[elType, srcType, dstType]) Run(t *testing.T) { + if tc.wantPanic == "" { + checkConversion(t, tc.src, dstType(tc.src), tc.want) + return + } + + var got dstType + defer func() { + err := recover() + if err == nil { + t.Fatalf("Got: %T(%v) = %v. Want: panic.", got, tc.src, got) + } + if msg := fmt.Sprint(err); !strings.Contains(msg, tc.wantPanic) { + t.Fatalf("Got panic: %v. Want: panic containing %q.", err, tc.wantPanic) + } + }() + got = dstType(tc.src) +} + +type ptrConversion[T any, srcType ~*T, dstType ~*T] struct { + src srcType + want dstType +} + +func (tc ptrConversion[T, srcType, dstType]) Run(t *testing.T) { + checkConversion(t, tc.src, dstType(tc.src), tc.want) +} + +type structConversion[srcType ~struct { + s string + i int +}, dstType ~struct { + s string + i int +}] struct { + src srcType + want dstType +} + +func (tc structConversion[srcType, dstType]) Run(t *testing.T) { + checkConversion(t, tc.src, dstType(tc.src), tc.want) +} + +type arrayConversion[srcType ~[3]byte, dstType ~[3]byte] struct { + src srcType + want dstType +} + +func (tc arrayConversion[srcType, dstType]) Run(t *testing.T) { + checkConversion(t, tc.src, dstType(tc.src), tc.want) +} + +type mapConversion[srcType ~map[string]string, dstType ~map[string]string] struct { + src srcType + want dstType +} + +func (tc mapConversion[srcType, dstType]) Run(t *testing.T) { + checkConversion(t, tc.src, dstType(tc.src), tc.want) +} + +type chanConversion[srcType ~chan string, dstType ~chan string] struct { + src srcType + want dstType +} + +func (tc chanConversion[srcType, dstType]) Run(t *testing.T) { + checkConversion(t, tc.src, dstType(tc.src), tc.want) +} + +type funcConversion[srcType ~func() int, dstType ~func() int] struct { + src srcType + want dstType +} + +func (tc funcConversion[srcType, dstType]) Run(t *testing.T) { + got := dstType(tc.src) + if reflect.TypeOf(got) != reflect.TypeOf(tc.want) { + t.Errorf("Got: %v. Want: converted type is: %v.", reflect.TypeOf(got), reflect.TypeOf(tc.want)) + } + + if js.InternalObject(got) != js.InternalObject(tc.want) { + t.Errorf("Got: %v != %v. Want: after type conversion function object should remain the same.", got, tc.want) + } +} + +func TestConversion(t *testing.T) { + strVar := "abc" + stVar := st{s: "abc", i: 42} + arrVal := [3]byte{1, 2, 3} + chanVal := make(chan string) + funcVal := func() int { return 42 } + + tests := []conversionTest{ + // $convertToInt64 + numericConversion[int, int64]{src: 0x7FFFFFFF, want: 0x7FFFFFFF}, + numericConversion[int64, uint64]{src: -0x8000000000000000, want: 0x8000000000000000}, + numericConversion[uint, int64]{src: 0xFFFFFFFF, want: 0xFFFFFFFF}, + numericConversion[uint64, int64]{src: 0xFFFFFFFFFFFFFFFF, want: -1}, + numericConversion[uint64, uint64]{src: 0xFFFFFFFFFFFFFFFF, want: 0xFFFFFFFFFFFFFFFF}, + numericConversion[uintptr, uint64]{src: 0xFFFFFFFF, want: 0xFFFFFFFF}, + numericConversion[uintptr, uint64]{src: reflect.ValueOf(&struct{}{}).Pointer(), want: 0x1, quirk: true}, + numericConversion[float32, int64]{src: 2e10, want: 20000000000}, + numericConversion[float64, int64]{src: 2e10, want: 20000000000}, + numericConversion[int64, i64]{src: 1, want: 1}, + numericConversion[i64, int64]{src: 1, want: 1}, + // $convertToNativeInt + numericConversion[int64, int32]{src: math.MaxInt64, want: -1}, + numericConversion[int64, int32]{src: -100, want: -100}, + numericConversion[int64, int32]{src: 0x00C0FFEE4B1D4B1D, want: 0x4B1D4B1D}, + numericConversion[int32, int16]{src: 0x0BAD4B1D, want: 0x4B1D}, + numericConversion[int16, int8]{src: 0x4B1D, want: 0x1D}, + numericConversion[uint64, uint32]{src: 0xDEADC0DE00C0FFEE, want: 0x00C0FFEE}, + numericConversion[uint32, uint16]{src: 0xDEADC0DE, want: 0xC0DE}, + numericConversion[uint16, uint8]{src: 0xC0DE, want: 0xDE}, + numericConversion[float32, int32]{src: 12345678.12345678, want: 12345678}, + numericConversion[float32, int16]{src: 12345678.12345678, want: 24910}, + numericConversion[float64, int32]{src: 12345678.12345678, want: 12345678}, + numericConversion[float64, int16]{src: 12345678.12345678, want: 24910}, + numericConversion[int32, int]{src: 0x00C0FFEE, want: 0x00C0FFEE}, + numericConversion[uint32, uint]{src: 0x00C0FFEE, want: 0x00C0FFEE}, + numericConversion[uint32, uintptr]{src: 0x00C0FFEE, want: 0x00C0FFEE}, + numericConversion[int32, i32]{src: 0x00C0FFEE, want: 0x00C0FFEE}, + numericConversion[i32, int32]{src: 0x00C0FFEE, want: 0x00C0FFEE}, + numericConversion[uint32, int32]{src: 0xFFFFFFFF, want: -1}, + numericConversion[uint16, int16]{src: 0xFFFF, want: -1}, + numericConversion[uint8, int8]{src: 0xFF, want: -1}, + // $convertToFloat + numericConversion[float64, float32]{src: 12345678.1234567890, want: 12345678.0}, + numericConversion[int64, float32]{src: 123456789, want: 123456792.0}, + numericConversion[int32, float32]{src: 12345678, want: 12345678.0}, + numericConversion[f32, float32]{src: 12345678.0, want: 12345678.0}, + numericConversion[float32, f32]{src: 12345678.0, want: 12345678.0}, + numericConversion[float32, float64]{src: 1234567.125000, want: 1234567.125000}, + numericConversion[int64, float64]{src: 12345678, want: 12345678.0}, + numericConversion[int32, float64]{src: 12345678, want: 12345678.0}, + numericConversion[f64, float64]{src: 12345678.0, want: 12345678.0}, + numericConversion[float64, f64]{src: 12345678.0, want: 12345678.0}, + // $convertToComplex + complexConversion[complex64, complex128]{src: 1 + 1i, want: 1 + 1i}, + complexConversion[complex128, complex64]{src: 1 + 1i, want: 1 + 1i}, + complexConversion[complex128, c128]{src: 1 + 1i, want: 1 + 1i}, + complexConversion[complex64, c64]{src: 1 + 1i, want: 1 + 1i}, + // $convertToString + stringConversion[str, string]{src: "abc", want: "abc"}, + stringConversion[string, str]{src: "abc", want: "abc"}, + stringConversion[rune, string]{src: 'a', want: "a"}, + stringConversion[byte, string]{src: 'a', want: "a"}, + stringConversion[[]byte, string]{src: []byte{'a', 'b', 'c'}, want: "abc"}, + stringConversion[[]rune, string]{src: []rune{'a', 'b', 'c'}, want: "abc"}, + // $convertToBool + boolConversion[b, bool]{src: true, want: true}, + boolConversion[b, bool]{src: false, want: false}, + boolConversion[bool, b]{src: true, want: true}, + boolConversion[bool, b]{src: false, want: false}, + // $convertToInterface + interfaceConversion[int]{src: 1}, + interfaceConversion[string]{src: "abc"}, + interfaceConversion[string]{src: "abc"}, + interfaceConversion[st]{src: st{s: "abc", i: 1}}, + interfaceConversion[error]{src: fmt.Errorf("test error")}, + interfaceConversion[*js.Object]{src: js.Global}, + interfaceConversion[*int]{src: func(i int) *int { return &i }(1)}, + // $convertToSlice + sliceConversion[byte, []byte, sl]{src: []byte{1, 2, 3}, want: sl{1, 2, 3}}, + sliceConversion[byte, sl, []byte]{src: sl{1, 2, 3}, want: []byte{1, 2, 3}}, + sliceConversion[byte, []byte, sl]{src: []byte(nil), want: sl(nil)}, + sliceConversion[byte, sl, []byte]{src: sl(nil), want: []byte(nil)}, + stringToSliceConversion[[]byte]{src: "🐞", want: []byte{240, 159, 144, 158}}, + stringToSliceConversion[[]rune]{src: "🐞x", want: []rune{'🐞', 'x'}}, + // $convertToPointer + sliceToArrayPtrConversion[byte, []byte, *[3]byte]{src: []byte{1, 2, 3}, want: &[3]byte{1, 2, 3}}, + sliceToArrayPtrConversion[byte, sl, arrPtr]{src: []byte{1, 2, 3}, want: arrPtr(&[3]byte{1, 2, 3})}, + sliceToArrayPtrConversion[byte, []byte, *[0]byte]{src: nil, want: nil}, + sliceToArrayPtrConversion[byte, []byte, *[3]byte]{src: []byte{1, 2}, wantPanic: "length"}, + sliceToArrayPtrConversion[byte, []byte, *[3]byte]{src: nil, wantPanic: "length"}, + ptrConversion[string, *string, strPtr]{src: &strVar, want: strPtr(&strVar)}, + ptrConversion[string, *string, strPtr]{src: nil, want: nil}, + ptrConversion[[3]byte, *[3]byte, arrPtr]{src: &arrVal, want: arrPtr(&arrVal)}, + ptrConversion[[3]byte, *[3]byte, arrPtr]{src: nil, want: nil}, + ptrConversion[st, *st, stPtr]{src: &stVar, want: stPtr(&stVar)}, + ptrConversion[st, *st, stPtr]{src: nil, want: nil}, + // $convertToStruct + structConversion[st, st2]{src: st{i: 42, s: "abc"}, want: st2{s: "abc", i: 42}}, + structConversion[st, struct { + s string + i int + }]{src: st{i: 42, s: "abc"}, want: st2{s: "abc", i: 42}}, + // $convertToArray + arrayConversion[[3]byte, arr]{src: [3]byte{1, 2, 3}, want: arr{1, 2, 3}}, + // $convertToMap + mapConversion[map[string]string, m]{src: map[string]string{"abc": "def"}, want: m{"abc": "def"}}, + mapConversion[map[string]string, m]{src: nil, want: nil}, + // $convertToChan + chanConversion[chan string, ch]{src: chanVal, want: ch(chanVal)}, + chanConversion[chan string, ch]{src: nil, want: nil}, + // $convertToFunc + funcConversion[func() int, fun]{src: funcVal, want: fun(funcVal)}, + funcConversion[func() int, fun]{src: nil, want: nil}, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%T", test), test.Run) + } +} diff --git a/tests/typeparams/literals_test.go b/tests/typeparams/literals_test.go new file mode 100644 index 000000000..cd3f2937b --- /dev/null +++ b/tests/typeparams/literals_test.go @@ -0,0 +1,102 @@ +package typeparams_test + +import ( + "fmt" + "testing" + + "golang.org/x/exp/constraints" +) + +func intLit[T constraints.Integer]() T { + var i T = 1 + return i +} + +func runeLit[T rune]() T { + var r T = 'a' + return r +} + +func floatLit[T constraints.Float]() T { + var f T = 1.1 + return f +} + +func complexLit[T constraints.Complex]() T { + var c T = 1 + 2i + return c +} + +func complexLit2[T constraints.Complex]() T { + var c T = 1 + return c +} + +func strLit[T string]() T { + var s T = "abc" + return s +} + +func boolLit[T bool]() T { + var b T = true + return b +} + +func nilLit[T *int]() T { + var p T = nil + return p +} + +func TestLiterals(t *testing.T) { + tests := []struct { + got any + want any + }{{ + got: intLit[int32](), + want: int32(1), + }, { + got: intLit[uint32](), + want: uint32(1), + }, { + got: intLit[int64](), + want: int64(1), + }, { + got: intLit[uint64](), + want: uint64(1), + }, { + got: runeLit[rune](), + want: 'a', + }, { + got: floatLit[float32](), + want: float32(1.1), + }, { + got: floatLit[float64](), + want: float64(1.1), + }, { + got: complexLit[complex64](), + want: complex64(1 + 2i), + }, { + got: complexLit[complex128](), + want: complex128(1 + 2i), + }, { + got: complexLit2[complex128](), + want: complex128(1), + }, { + got: strLit[string](), + want: "abc", + }, { + got: boolLit[bool](), + want: true, + }, { + got: nilLit[*int](), + want: (*int)(nil), + }} + + for _, test := range tests { + t.Run(fmt.Sprintf("%T/%v", test.want, test.want), func(t *testing.T) { + if test.got != test.want { + t.Errorf("Got: %v. Want: %v.", test.got, test.want) + } + }) + } +} diff --git a/tests/typeparams/low_level_test.go b/tests/typeparams/low_level_test.go new file mode 100644 index 000000000..c5a6a7259 --- /dev/null +++ b/tests/typeparams/low_level_test.go @@ -0,0 +1,49 @@ +package typeparams + +import "testing" + +// This file contains test cases for low-level details of typeparam +// implementation like variable name assignment. + +type TypeParamNameMismatch[T any] struct{} + +func (TypeParamNameMismatch[T1]) M(_ T1) {} + +func TestTypeParamNameMismatch(t *testing.T) { + // This test case exercises the case when the same typeparam is named + // differently between the struct definition and one of its methods. GopherJS + // must allocate the same JS variable name to both instances of the type param + // in order to make it possible for the reflection method data to be evaluated + // within the type's generic factory function. + + a := TypeParamNameMismatch[int]{} + a.M(0) // Make sure the method is not eliminated as dead code. +} + +type ( + TypeParamVariableCollision1[T any] struct{} + TypeParamVariableCollision2[T any] struct{} + TypeParamVariableCollision3[T any] struct{} +) + +func (TypeParamVariableCollision1[T]) M() {} +func (TypeParamVariableCollision2[T]) M() {} +func (TypeParamVariableCollision3[T]) M() {} + +func TestTypeParamVariableCollision(t *testing.T) { + // This test case exercises a situation when in minified mode the variable + // name that gets assigned to the type parameter in the method's generic + // factory function collides with a different variable in the type's generic + // factory function. The bug occurred because the JS variable name allocated + // to the *types.TypeName object behind a type param within the method's + // factory function was not marked as used within type's factory function. + + // Note: to trigger the bug, a package should contain multiple generic types, + // so that sequentially allocated minified variable names get far enough to + // cause the collision. + + // Make sure types and methods are not eliminated as dead code. + TypeParamVariableCollision1[int]{}.M() + TypeParamVariableCollision2[int]{}.M() + TypeParamVariableCollision3[int]{}.M() +}