From 900dda7b220aae50bb5ea5739d25d19985574cc7 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 2 Oct 2022 16:30:07 +0100 Subject: [PATCH 1/2] compiler: Rename and document functions responsible for identifier generation. These functions provide mappings from Go entities (variables, types, functions, etc.) to JS identifiers assigned to them. This commit adds documentation comments and renames a couple of methods to better match their role. --- compiler/expressions.go | 36 +++++++++++++++++------------------ compiler/package.go | 6 +++--- compiler/statements.go | 36 +++++++++++++++++------------------ compiler/utils.go | 42 +++++++++++++++++++++++++++++++++-------- 4 files changed, 73 insertions(+), 47 deletions(-) diff --git a/compiler/expressions.go b/compiler/expressions.go index 5d9e37cc9..d30b53312 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -362,14 +362,14 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { 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) + 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.newVariable("_r"), e.X, e.Y) + 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) { @@ -385,7 +385,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { 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") + 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) { @@ -408,7 +408,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if fc.Blocking[e.Y] { skipCase := fc.caseCounter fc.caseCounter++ - resultVar := fc.newVariable("_v") + 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) @@ -418,7 +418,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if fc.Blocking[e.Y] { skipCase := fc.caseCounter fc.caseCounter++ - resultVar := fc.newVariable("_v") + 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) @@ -477,7 +477,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()), @@ -485,7 +485,7 @@ 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()), @@ -642,13 +642,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:])) @@ -795,7 +795,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 { @@ -845,7 +845,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 @@ -1124,8 +1124,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)) @@ -1173,8 +1173,8 @@ 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") + array := fc.newLocalVariable("_array") + target := fc.newLocalVariable("_struct") return fc.formatExpr("(%s = %e, %s = %e, %s, %s)", array, expr, target, fc.zeroValue(t.Elem()), fc.loadStruct(array, target, ptrElType), target) } // Convert between structs of different types but identical layouts, @@ -1196,7 +1196,7 @@ 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") + ptrVar := fc.newLocalVariable("_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) return fc.formatExpr("(%1s = %2e, new %3s(function() { return %4s; }, function($v) { %1s.$set(%5s); }, %1s.$target))", ptrVar, expr, fc.typeName(desiredType), getterConv, setterConv) @@ -1268,7 +1268,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) @@ -1398,7 +1398,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 } diff --git a/compiler/package.go b/compiler/package.go index db68c7208..4c96feb69 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -294,7 +294,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor // but now we do it here to maintain previous behavior. continue } - funcCtx.pkgCtx.pkgVars[importedPkg.Path()] = funcCtx.newVariableWithLevel(importedPkg.Name(), true) + funcCtx.pkgCtx.pkgVars[importedPkg.Path()] = funcCtx.newVariable(importedPkg.Name(), true) importedPaths = append(importedPaths, importedPkg.Path()) } sort.Strings(importedPaths) @@ -773,12 +773,12 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, var params []string for _, param := range typ.Params.List { if len(param.Names) == 0 { - params = append(params, c.newVariable("param")) + params = append(params, c.newLocalVariable("param")) continue } for _, ident := range param.Names { if isBlank(ident) { - params = append(params, c.newVariable("param")) + params = append(params, c.newLocalVariable("param")) continue } params = append(params, c.objectName(c.pkgCtx.Defs[ident])) diff --git a/compiler/statements.go b/compiler/statements.go index 8e4b913b8..1a83b6a52 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -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(), true) 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 @@ -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, @@ -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/utils.go b/compiler/utils.go index 11793b5be..0dc2ea7dd 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -110,7 +110,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 { @@ -138,7 +138,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 } @@ -233,11 +233,26 @@ 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, false) } -func (fc *funcContext) newVariableWithLevel(name string, pkgLevel bool) string { +// 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, pkgLevel bool) string { if name == "" { panic("newVariable: empty name") } @@ -320,6 +335,8 @@ func isPkgLevel(o types.Object) bool { return o.Parent() != nil && o.Parent().Parent() == types.Universe } +// 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 @@ -331,7 +348,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(), isPkgLevel(o)) fc.pkgCtx.objectNames[o] = name } @@ -348,12 +365,17 @@ func (fc *funcContext) varPtrName(o *types.Var) string { name, ok := fc.pkgCtx.varPtrNames[o] if !ok { - name = fc.newVariableWithLevel(o.Name()+"$ptr", isPkgLevel(o)) + name = fc.newVariable(o.Name()+"$ptr", isPkgLevel(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: @@ -369,10 +391,14 @@ func (fc *funcContext) typeName(ty types.Type) string { } } + // For anonymous composite types, generate a synthetic package-level type + // declaration, which will be reused for all instances of this time. This + // improves performance, since runtime won't have to synthesize the same type + // repeatedly. anonType, ok := fc.pkgCtx.anonTypeMap.At(ty).(*types.TypeName) if !ok { fc.initArgs(ty) // cause all embedded types to be registered - varName := fc.newVariableWithLevel(strings.ToLower(typeKind(ty)[5:])+"Type", true) + varName := fc.newVariable(strings.ToLower(typeKind(ty)[5:])+"Type", true) 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) From 3b7ea278c6b7abf1679f2644b91dbae98a692666 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Wed, 5 Oct 2022 19:51:03 +0100 Subject: [PATCH 2/2] Rudimentary support for passing type parameters into generic functions. Instead of generating an independent function instance for every combination of type parameters at compile time we construct generic function instances at runtime using "generic factory functions". Such a factory takes type params as arguments and returns a concrete instance of the function for the given type params (type param values are captured by the returned function as a closure and can be used as necessary). Here is an abbreviated example of how a generic function is compiled and called: ``` // Go: func F[T any](t T) {} f(1) // JS: F = function(T){ return function(t) {}; }; F($Int)(1); ``` This approach minimizes the size of the generated JS source, which is critical for the client-side use case, at the cost of runtime performance. See https://github.com/gopherjs/gopherjs/issues/1013#issuecomment-1217237123 for the detailed description. Note that the implementation in this commit is far from complete: - Generic function instances are not cached. - Generic types are not supported. - Declaring types dependent on type parameters doesn't work correctly. - Operators (such as `+`) do not work correctly with generic arguments. --- compiler/expressions.go | 48 ++++++++++++++++++++++++++-- compiler/package.go | 24 ++++++++++++-- compiler/statements.go | 2 +- compiler/utils.go | 71 ++++++++++++++++++++++++++++++++--------- 4 files changed, 124 insertions(+), 21 deletions(-) diff --git a/compiler/expressions.go b/compiler/expressions.go index d30b53312..1d86174ce 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -492,10 +492,18 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { ) 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 { @@ -749,6 +757,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())) @@ -788,6 +800,38 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } } +// 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] { diff --git a/compiler/package.go b/compiler/package.go index 4c96feb69..f360d9af1 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -180,6 +180,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor Implicits: make(map[ast.Node]types.Object), Selections: make(map[*ast.SelectorExpr]*types.Selection), Scopes: make(map[ast.Node]*types.Scope), + Instances: make(map[*ast.Ident]types.Instance), } var errList ErrorList @@ -294,7 +295,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor // but now we do it here to maintain previous behavior. continue } - funcCtx.pkgCtx.pkgVars[importedPkg.Path()] = funcCtx.newVariable(importedPkg.Name(), true) + funcCtx.pkgCtx.pkgVars[importedPkg.Path()] = funcCtx.newVariable(importedPkg.Name(), varPackage) importedPaths = append(importedPaths, importedPkg.Path()) } sort.Strings(importedPaths) @@ -521,7 +522,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor d.DeclCode = funcCtx.CatchOutput(0, func() { typeName := funcCtx.objectName(o) lhs := typeName - if isPkgLevel(o) { + if typeVarLevel(o) == varPackage { lhs += " = $pkg." + encodeIdent(o.Name()) } size := int64(0) @@ -898,5 +899,22 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, c.pkgCtx.escapingVars = prevEV - return params, fmt.Sprintf("function%s(%s) {\n%s%s}", functionName, strings.Join(params, ", "), bodyOutput, c.Indentation(0)) + if !c.sigTypes.IsGeneric() { + return params, fmt.Sprintf("function%s(%s) {\n%s%s}", functionName, strings.Join(params, ", "), bodyOutput, c.Indentation(0)) + } + + // 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. + // TODO(nevkontakte): Generate type parameter arguments and derive all dependent + // types inside the function. + typeParams := []string{} + for i := 0; i < c.sigTypes.Sig.TypeParams().Len(); i++ { + typeParam := c.sigTypes.Sig.TypeParams().At(i) + typeParams = append(typeParams, c.typeName(typeParam)) + } + + return params, fmt.Sprintf("function%s(%s){ return function(%s) {\n%s%s}; }", + functionName, strings.Join(typeParams, ", "), strings.Join(params, ", "), bodyOutput, c.Indentation(0)) } diff --git a/compiler/statements.go b/compiler/statements.go index 1a83b6a52..dc457e0d5 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -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.newVariable(o.Name(), true) + fc.pkgCtx.objectNames[o] = fc.newVariable(o.Name(), varPackage) fc.pkgCtx.dependencies[o] = true } case token.CONST: diff --git a/compiler/utils.go b/compiler/utils.go index 0dc2ea7dd..cf59cd573 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -237,9 +237,23 @@ func (fc *funcContext) newConst(t types.Type, value constant.Value) ast.Expr { // 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, false) + return fc.newVariable(name, varFuncLocal) } +// 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 = 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. // @@ -252,7 +266,7 @@ func (fc *funcContext) newLocalVariable(name string) string { // 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, pkgLevel bool) string { +func (fc *funcContext) newVariable(name string, level varLevel) string { if name == "" { panic("newVariable: empty name") } @@ -261,7 +275,7 @@ func (fc *funcContext) newVariable(name string, pkgLevel bool) string { i := 0 for { offset := int('a') - if pkgLevel { + if level == varPackage { offset = int('A') } j := i @@ -286,9 +300,22 @@ func (fc *funcContext) newVariable(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.sigTypes.IsGeneric() { + break + } } return varName } @@ -331,14 +358,20 @@ func isVarOrConst(o types.Object) bool { return false } -func isPkgLevel(o types.Object) bool { - return o.Parent() != nil && o.Parent().Parent() == types.Universe +func typeVarLevel(o types.Object) varLevel { + if _, ok := o.Type().(*types.TypeParam); ok { + 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) { + if typeVarLevel(o) == varPackage { fc.pkgCtx.dependencies[o] = true if o.Pkg() != fc.pkgCtx.Pkg || (isVarOrConst(o) && o.Exported()) { @@ -348,7 +381,7 @@ func (fc *funcContext) objectName(o types.Object) string { name, ok := fc.pkgCtx.objectNames[o] if !ok { - name = fc.newVariable(o.Name(), isPkgLevel(o)) + name = fc.newVariable(o.Name(), typeVarLevel(o)) fc.pkgCtx.objectNames[o] = name } @@ -359,13 +392,13 @@ func (fc *funcContext) objectName(o types.Object) string { } func (fc *funcContext) varPtrName(o *types.Var) string { - if isPkgLevel(o) && o.Exported() { + if typeVarLevel(o) == varPackage && o.Exported() { return fc.pkgVar(o.Pkg()) + "." + o.Name() + "$ptr" } name, ok := fc.pkgCtx.varPtrNames[o] if !ok { - name = fc.newVariable(o.Name()+"$ptr", isPkgLevel(o)) + name = fc.newVariable(o.Name()+"$ptr", typeVarLevel(o)) fc.pkgCtx.varPtrNames[o] = name } return name @@ -385,6 +418,8 @@ func (fc *funcContext) typeName(ty types.Type) string { return "$error" } return fc.objectName(t.Obj()) + case *types.TypeParam: + return fc.objectName(t.Obj()) case *types.Interface: if t.Empty() { return "$emptyInterface" @@ -392,13 +427,13 @@ func (fc *funcContext) typeName(ty types.Type) string { } // For anonymous composite types, generate a synthetic package-level type - // declaration, which will be reused for all instances of this time. This + // declaration, which will be reused for all instances of this type. This // improves performance, since runtime won't have to synthesize the same type // repeatedly. anonType, ok := fc.pkgCtx.anonTypeMap.At(ty).(*types.TypeName) if !ok { - fc.initArgs(ty) // cause all embedded types to be registered - varName := fc.newVariable(strings.ToLower(typeKind(ty)[5:])+"Type", true) + fc.initArgs(ty) // cause all dependency types to be registered + varName := fc.newVariable(strings.ToLower(typeKind(ty)[5:])+"Type", varPackage) 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) @@ -815,6 +850,12 @@ func (st signatureTypes) 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 signatureTypes) IsGeneric() bool { + return st.Sig.TypeParams().Len() > 0 || st.Sig.RecvTypeParams().Len() > 0 +} + // 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)