Merge pull request #1335 from nevkontakte/gng7
Final round of refactoring ported from the original generics branch.
diff --git a/compiler/decls.go b/compiler/decls.go
index 36f97d3..c134bc4 100644
--- a/compiler/decls.go
+++ b/compiler/decls.go
@@ -345,7 +345,7 @@
}
d.DceDeps = fc.CollectDCEDeps(func() {
- d.DeclCode = fc.translateTopLevelFunction(fun, inst)
+ d.DeclCode = fc.namedFuncContext(inst).translateTopLevelFunction(fun)
})
return d
}
diff --git a/compiler/expressions.go b/compiler/expressions.go
index fea80b6..5652439 100644
--- a/compiler/expressions.go
+++ b/compiler/expressions.go
@@ -201,7 +201,7 @@
}
case *ast.FuncLit:
- fun := fc.nestedFunctionContext(fc.pkgCtx.FuncLitInfos[e], exprType.(*types.Signature), typeparams.Instance{}).translateFunctionBody(e.Type, nil, e.Body, "")
+ 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 {
@@ -730,10 +730,7 @@
}
}
- 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:
diff --git a/compiler/functions.go b/compiler/functions.go
index ed3062c..31a9974 100644
--- a/compiler/functions.go
+++ b/compiler/functions.go
@@ -5,6 +5,7 @@
import (
"bytes"
+ "errors"
"fmt"
"go/ast"
"go/types"
@@ -17,18 +18,21 @@
"github.com/gopherjs/gopherjs/compiler/typesutil"
)
-// newFunctionContext creates a new nested context for a function corresponding
+// nestedFunctionContext creates a new nested context for a function corresponding
// to the provided info and instance.
-func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, sig *types.Signature, inst typeparams.Instance) *funcContext {
+func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, inst typeparams.Instance) *funcContext {
if info == nil {
- panic(fmt.Errorf("missing *analysis.FuncInfo"))
+ panic(errors.New("missing *analysis.FuncInfo"))
}
- if sig == nil {
- panic(fmt.Errorf("missing *types.Signature"))
+ if inst.Object == nil {
+ panic(errors.New("missing inst.Object"))
}
+ o := inst.Object.(*types.Func)
+ sig := o.Type().(*types.Signature)
c := &funcContext{
FuncInfo: info,
+ instance: inst,
pkgCtx: fc.pkgCtx,
parent: fc,
allVars: make(map[string]int, len(fc.allVars)),
@@ -53,119 +57,73 @@
c.objectNames = map[types.Object]string{}
}
+ // 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 recvType := typesutil.RecvType(sig); recvType != nil {
+ funcRef = recvType.Obj().Name() + midDot + funcRef
+ }
+ c.funcRef = c.newVariable(funcRef, true /*pkgLevel*/)
+
+ return c
+}
+
+// namedFuncContext creates a new funcContext for a named Go function
+// (standalone or method).
+func (fc *funcContext) namedFuncContext(inst typeparams.Instance) *funcContext {
+ info := fc.pkgCtx.FuncDeclInfos[inst.Object.(*types.Func)]
+ c := fc.nestedFunctionContext(info, inst)
+
+ 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)
+ inst := typeparams.Instance{Object: o}
+
+ c := fc.nestedFunctionContext(info, inst)
return c
}
// translateTopLevelFunction translates a top-level function declaration
-// (standalone function or method) into a corresponding JS function.
+// (standalone function or method) into a corresponding JS function. Must be
+// called on the function context created for the function corresponding instance.
//
-// Returns a string with a JavaScript statements that define the function or
+// Returns a string with JavaScript statements that define the function or
// method. For methods it returns declarations for both value- and
// pointer-receiver (if appropriate).
-func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl, inst typeparams.Instance) []byte {
+func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl) []byte {
if fun.Recv == nil {
- return fc.translateStandaloneFunction(fun, inst)
+ return fc.translateStandaloneFunction(fun)
}
- o := inst.Object.(*types.Func)
- info := fc.pkgCtx.FuncDeclInfos[o]
-
- sig := o.Type().(*types.Signature)
- // 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 = function() {\n\t\t$throwRuntimeError(\"native function not implemented: %s\");\n\t};\n", lvalue, o.FullName()))
- }
-
- var recv *ast.Ident
- if fun.Recv != nil && fun.Recv.List[0].Names != nil {
- recv = fun.Recv.List[0].Names[0]
- }
- fun := fc.nestedFunctionContext(info, sig, inst).translateFunctionBody(fun.Type, recv, fun.Body, lvalue)
- return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fun))
- }
-
- funName := fun.Name.Name
- if reservedKeywords[funName] {
- funName += "$"
- }
-
- // proxyFunction generates a JS function that forwards the call to the actual
- // method implementation for the alternate receiver (e.g. pointer vs
- // non-pointer).
- proxyFunction := func(lvalue, receiver string) []byte {
- fun := fmt.Sprintf("function(...$args) { return %s.%s(...$args); }", receiver, funName)
- return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fun))
- }
-
- recvInst := inst.Recv()
- recvInstName := fc.instName(recvInst)
- recvType := recvInst.Object.Type().(*types.Named)
-
- // Objects the method should be assigned to for the plain and pointer type
- // of the receiver.
- prototypeVar := fmt.Sprintf("%s.prototype.%s", recvInstName, funName)
- ptrPrototypeVar := fmt.Sprintf("$ptrType(%s).prototype.%s", recvInstName, funName)
-
- code := bytes.NewBuffer(nil)
-
- if _, isStruct := recvType.Underlying().(*types.Struct); isStruct {
- // Structs are a special case: they are represented by JS objects and their
- // methods are the underlying object's methods. Due to reference semantics
- // of the JS variables, the actual backing object is considered to represent
- // the pointer-to-struct type, and methods are attacher to it first and
- // foremost.
- code.Write(primaryFunction(ptrPrototypeVar))
- code.Write(proxyFunction(prototypeVar, "this.$val"))
- return code.Bytes()
- }
-
- if ptr, isPointer := sig.Recv().Type().(*types.Pointer); isPointer {
- if _, isArray := ptr.Elem().Underlying().(*types.Array); isArray {
- // Pointer-to-array is another special case.
- // TODO(nevkontakte) Find out and document why.
- code.Write(primaryFunction(prototypeVar))
- code.Write(proxyFunction(ptrPrototypeVar, fmt.Sprintf("(new %s(this.$get()))", recvInstName)))
- return code.Bytes()
- }
-
- // Methods with pointer-receiver are only attached to the pointer-receiver
- // type.
- return primaryFunction(ptrPrototypeVar)
- }
-
- // Methods defined for non-pointer receiver are attached to both pointer- and
- // non-pointer-receiver types.
- recvExpr := "this.$get()"
- if isWrapped(recvType) {
- recvExpr = fmt.Sprintf("new %s(%s)", recvInstName, recvExpr)
- }
- code.Write(primaryFunction(prototypeVar))
- code.Write(proxyFunction(ptrPrototypeVar, recvExpr))
- return code.Bytes()
+ return fc.translateMethod(fun)
}
// translateStandaloneFunction translates a package-level function.
//
-// It returns a JS statements which define the corresponding function in a
+// 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, inst typeparams.Instance) []byte {
- o := inst.Object.(*types.Func)
- info := fc.pkgCtx.FuncDeclInfos[o]
- sig := o.Type().(*types.Signature)
+func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl) []byte {
+ o := fc.instance.Object.(*types.Func)
if fun.Recv != nil {
panic(fmt.Errorf("expected standalone function, got method: %s", o))
}
- lvalue := fc.instName(inst)
+ lvalue := fc.instName(fc.instance)
if fun.Body == nil {
- return []byte(fmt.Sprintf("\t%s = function() {\n\t\t$throwRuntimeError(\"native function not implemented: %s\");\n\t};\n", lvalue, o.FullName()))
+ return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fc.unimplementedFunction(o)))
}
- body := fc.nestedFunctionContext(info, sig, inst).translateFunctionBody(fun.Type, nil, fun.Body, lvalue)
+ body := fc.translateFunctionBody(fun.Type, nil, fun.Body)
code := bytes.NewBuffer(nil)
fmt.Fprintf(code, "\t%s = %s;\n", lvalue, body)
if fun.Name.IsExported() {
@@ -174,12 +132,91 @@
return code.Bytes()
}
+// translateMethod translates a named type method.
+//
+// It returns one or more JS statements which define the method. Methods with
+// non-pointer receiver are automatically defined for the pointer-receiver type.
+func (fc *funcContext) translateMethod(fun *ast.FuncDecl) []byte {
+ o := fc.instance.Object.(*types.Func)
+ funName := fc.methodName(o)
+
+ // 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)))
+ }
+
+ var recv *ast.Ident
+ if fun.Recv != nil && fun.Recv.List[0].Names != nil {
+ recv = fun.Recv.List[0].Names[0]
+ }
+ fun := fc.translateFunctionBody(fun.Type, recv, fun.Body)
+ return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fun))
+ }
+
+ recvInst := fc.instance.Recv()
+ recvInstName := fc.instName(recvInst)
+ recvType := recvInst.Object.Type().(*types.Named)
+
+ // Objects the method should be assigned to for the plain and pointer type
+ // of the receiver.
+ prototypeVar := fmt.Sprintf("%s.prototype.%s", recvInstName, funName)
+ ptrPrototypeVar := fmt.Sprintf("$ptrType(%s).prototype.%s", recvInstName, funName)
+
+ // Methods with pointer-receiver are only attached to the pointer-receiver type.
+ if _, isPointer := fc.sig.Sig.Recv().Type().(*types.Pointer); 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 {
+ fun := fmt.Sprintf("function(...$args) { return %s.%s(...$args); }", receiver, funName)
+ return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fun))
+ }
+
+ // Structs are a special case: they are represented by JS objects and their
+ // methods are the underlying object's methods. Due to reference semantics of
+ // the JS variables, the actual backing object is considered to represent the
+ // pointer-to-struct type, and methods are attacher to it first and foremost.
+ if _, isStruct := recvType.Underlying().(*types.Struct); isStruct {
+ code := bytes.Buffer{}
+ code.Write(primaryFunction(ptrPrototypeVar))
+ code.Write(proxyFunction(prototypeVar, "this.$val"))
+ return code.Bytes()
+ }
+
+ // Methods defined for non-pointer receiver are attached to both pointer- and
+ // non-pointer-receiver types.
+ proxyRecvExpr := "this.$get()"
+ if isWrapped(recvType) {
+ proxyRecvExpr = fmt.Sprintf("new %s(%s)", recvInstName, 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())
+}
+
// translateFunctionBody translates body of a top-level or literal function.
//
// It returns a JS function expression that represents the given Go function.
// Function receiver must have been created with nestedFunctionContext() to have
// required metadata set up.
-func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, funcRef string) string {
+func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt) string {
prevEV := fc.pkgCtx.escapingVars
// Generate a list of function argument variables. Since Go allows nameless
@@ -233,7 +270,7 @@
sort.Strings(fc.localVars)
- var prefix, suffix, functionName string
+ var prefix, suffix string
if len(fc.Flattened) != 0 {
// $s contains an index of the switch case a blocking function reached
@@ -254,21 +291,19 @@
localVarDefs := "" // Function-local var declaration at the top.
if len(fc.Blocking) != 0 {
- if funcRef == "" {
- funcRef = "$b"
- functionName = " $b"
- }
-
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(args, ", "))
// If the function gets blocked, save local variables for future.
- saveContext := fmt.Sprintf("var $f = {$blk: "+funcRef+", $c: true, $r, %s};", strings.Join(fc.localVars, ", "))
+ saveContext := fmt.Sprintf("var $f = {$blk: "+fc.funcRef+", $c: true, $r, %s};", strings.Join(fc.localVars, ", "))
suffix = " " + saveContext + "return $f;" + suffix
} else if len(fc.localVars) > 0 {
@@ -316,5 +351,5 @@
fc.pkgCtx.escapingVars = prevEV
- return fmt.Sprintf("function%s(%s) {\n%s%s}", functionName, strings.Join(args, ", "), bodyOutput, fc.Indentation(0))
+ return fmt.Sprintf("function %s(%s) {\n%s%s}", fc.funcRef, strings.Join(args, ", "), bodyOutput, fc.Indentation(0))
}
diff --git a/compiler/natives/src/reflect/reflect.go b/compiler/natives/src/reflect/reflect.go
index 47b9366..81f4c7b 100644
--- a/compiler/natives/src/reflect/reflect.go
+++ b/compiler/natives/src/reflect/reflect.go
@@ -1778,26 +1778,28 @@
var pc [5]uintptr
n := runtime.Callers(1, pc[:])
frames := runtime.CallersFrames(pc[:n])
+ valueTyp := TypeOf(Value{})
var frame runtime.Frame
for more := true; more; {
frame, more = frames.Next()
name := frame.Function
-
// Function name extracted from the call stack can be different from
// vanilla Go, so is not prefixed by "reflect.Value." as needed by the original.
// See https://cs.opensource.google/go/go/+/refs/tags/go1.19.13:src/reflect/value.go;l=173-191
- // Here we try to fix stuff like "Object.$packages.reflect.Q.ptr.SetIterKey"
- // into "reflect.Value.SetIterKey".
// This workaround may become obsolete after
// https://github.com/gopherjs/gopherjs/issues/1085 is resolved.
- const prefix = `Object.$packages.reflect.`
- if stringsHasPrefix(name, prefix) {
- if idx := stringsLastIndex(name, '.'); idx >= 0 {
- methodName := name[idx+1:]
- if len(methodName) > 0 && 'A' <= methodName[0] && methodName[0] <= 'Z' {
- return `reflect.Value.` + methodName
- }
+ methodName := name
+ if idx := stringsLastIndex(name, '.'); idx >= 0 {
+ methodName = name[idx+1:]
+ }
+
+ // Since function name in the call stack doesn't contain receiver name,
+ // we are looking for the first exported function name that matches a
+ // known Value method.
+ if _, ok := valueTyp.MethodByName(methodName); ok {
+ if len(methodName) > 0 && 'A' <= methodName[0] && methodName[0] <= 'Z' {
+ return `reflect.Value.` + methodName
}
}
}
diff --git a/compiler/natives/src/reflect/reflect_test.go b/compiler/natives/src/reflect/reflect_test.go
index 79bbe53..4c0bcd0 100644
--- a/compiler/natives/src/reflect/reflect_test.go
+++ b/compiler/natives/src/reflect/reflect_test.go
@@ -298,3 +298,23 @@
func TestStructOfTooLarge(t *testing.T) {
t.Skip("This test is dependent on field alignment to determine if a struct size would exceed virtual address space.")
}
+
+func TestSetLenCap(t *testing.T) {
+ t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085")
+}
+
+func TestSetPanic(t *testing.T) {
+ t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085")
+}
+
+func TestCallPanic(t *testing.T) {
+ t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085")
+}
+
+func TestValuePanic(t *testing.T) {
+ t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085")
+}
+
+func TestSetIter(t *testing.T) {
+ t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085")
+}
diff --git a/compiler/package.go b/compiler/package.go
index 34387b5..bcdfca5 100644
--- a/compiler/package.go
+++ b/compiler/package.go
@@ -53,6 +53,15 @@
// JavaScript code (as defined for `var` declarations).
type funcContext struct {
*analysis.FuncInfo
+ // Function instance this context corresponds to, or zero 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.
+ instance typeparams.Instance
+ // 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
// Function context, surrounding this function definition. For package-level
@@ -104,6 +113,8 @@
typeResolver *typeparams.Resolver
// Mapping from function-level objects to JS variable names they have been assigned.
objectNames map[types.Object]string
+ // Number of function literals encountered within the current function context.
+ funcLitCounter int
}
func newRootCtx(tContext *types.Context, srcs sources, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(*types.Func) bool, minify bool) *funcContext {
diff --git a/compiler/utils.go b/compiler/utils.go
index 7fec5b2..62c09a0 100644
--- a/compiler/utils.go
+++ b/compiler/utils.go
@@ -23,6 +23,11 @@
"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 = "·"
+
// root returns the topmost function context corresponding to the package scope.
func (fc *funcContext) root() *funcContext {
if fc.isRoot() {
@@ -376,6 +381,25 @@
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.instance.Object != nil {
+ if recvType := typesutil.RecvType(fc.sig.Sig); recvType != nil {
+ name.WriteString(recvType.Obj().Name())
+ name.WriteString(midDot)
+ }
+ name.WriteString(fc.instance.Object.Name())
+ name.WriteString(midDot)
+ }
+ fmt.Fprintf(name, "func%d", fc.funcLitCounter)
+ return name.String()
+}
+
func (fc *funcContext) setType(e ast.Expr, t types.Type) ast.Expr {
fc.pkgCtx.Types[e] = types.TypeAndValue{Type: t}
return e
@@ -474,6 +498,21 @@
return fmt.Sprintf("%s[%d /* %v */]", objName, fc.pkgCtx.instanceSet.ID(inst), inst.TArgs)
}
+// 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() {
return fc.pkgVar(o.Pkg()) + "." + o.Name() + "$ptr"
@@ -894,7 +933,15 @@
}
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
@@ -980,3 +1027,13 @@
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
+}