Skip to content

Commit d5771cc

Browse files
committed
Assign identity to all function literals and use them as funcRefs.
The main change is that we assign explicit names to all function objects that correspond to Go functions (named and literals). Function name is declared as `var f = function nameHere() { ... }` and is visible inside the function scope only. Doing so serves two purposes: - It is an identifier which we can use when saving state of a blocked function to know which function to call upon resumption. - It shows up in the stack trace, which helps distinguish similarly-named functions. For methods, we include the receiver type in the identifier to make A.String and B.String easily distinguishable. The main trick is that we synthesize names for the function literals, which are anonymous as far as go/types is concerned. The upstream Go compiler does something very similar. (based on commit 4d24395)
1 parent 06e8925 commit d5771cc

File tree

6 files changed

+127
-43
lines changed

6 files changed

+127
-43
lines changed

compiler/decls.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance)
345345
}
346346

347347
d.DceDeps = fc.CollectDCEDeps(func() {
348-
d.DeclCode = fc.translateTopLevelFunction(fun, inst)
348+
d.DeclCode = fc.namedFuncContext(inst).translateTopLevelFunction(fun)
349349
})
350350
return d
351351
}

compiler/expressions.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression {
201201
}
202202

203203
case *ast.FuncLit:
204-
fun := fc.nestedFunctionContext(fc.pkgCtx.FuncLitInfos[e], exprType.(*types.Signature), typeparams.Instance{}).translateFunctionBody(e.Type, nil, e.Body, "")
204+
fun := fc.literalFuncContext(e).translateFunctionBody(e.Type, nil, e.Body)
205205
if len(fc.pkgCtx.escapingVars) != 0 {
206206
names := make([]string, 0, len(fc.pkgCtx.escapingVars))
207207
for obj := range fc.pkgCtx.escapingVars {

compiler/functions.go

Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,21 @@ import (
1818
"github.com/gopherjs/gopherjs/compiler/typesutil"
1919
)
2020

21-
// newFunctionContext creates a new nested context for a function corresponding
21+
// nestedFunctionContext creates a new nested context for a function corresponding
2222
// to the provided info and instance.
23-
func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, sig *types.Signature, inst typeparams.Instance) *funcContext {
23+
func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, inst typeparams.Instance) *funcContext {
2424
if info == nil {
2525
panic(errors.New("missing *analysis.FuncInfo"))
2626
}
27-
if sig == nil {
28-
panic(errors.New("missing *types.Signature"))
27+
if inst.Object == nil {
28+
panic(errors.New("missing inst.Object"))
2929
}
30+
o := inst.Object.(*types.Func)
31+
sig := o.Type().(*types.Signature)
3032

3133
c := &funcContext{
3234
FuncInfo: info,
35+
instance: inst,
3336
pkgCtx: fc.pkgCtx,
3437
parent: fc,
3538
allVars: make(map[string]int, len(fc.allVars)),
@@ -54,43 +57,73 @@ func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, sig *types
5457
c.objectNames = map[types.Object]string{}
5558
}
5659

60+
// Synthesize an identifier by which the function may reference itself. Since
61+
// it appears in the stack trace, it's useful to include the receiver type in
62+
// it.
63+
funcRef := o.Name()
64+
if recvType := typesutil.RecvType(sig); recvType != nil {
65+
funcRef = recvType.Obj().Name() + midDot + funcRef
66+
}
67+
c.funcRef = c.newVariable(funcRef, true /*pkgLevel*/)
68+
69+
return c
70+
}
71+
72+
// namedFuncContext creates a new funcContext for a named Go function
73+
// (standalone or method).
74+
func (fc *funcContext) namedFuncContext(inst typeparams.Instance) *funcContext {
75+
info := fc.pkgCtx.FuncDeclInfos[inst.Object.(*types.Func)]
76+
c := fc.nestedFunctionContext(info, inst)
77+
78+
return c
79+
}
80+
81+
// literalFuncContext creates a new funcContext for a function literal. Since
82+
// go/types doesn't generate *types.Func objects for function literals, we
83+
// generate a synthetic one for it.
84+
func (fc *funcContext) literalFuncContext(fun *ast.FuncLit) *funcContext {
85+
info := fc.pkgCtx.FuncLitInfos[fun]
86+
sig := fc.pkgCtx.TypeOf(fun).(*types.Signature)
87+
o := types.NewFunc(fun.Pos(), fc.pkgCtx.Pkg, fc.newLitFuncName(), sig)
88+
inst := typeparams.Instance{Object: o}
89+
90+
c := fc.nestedFunctionContext(info, inst)
5791
return c
5892
}
5993

6094
// translateTopLevelFunction translates a top-level function declaration
61-
// (standalone function or method) into a corresponding JS function.
95+
// (standalone function or method) into a corresponding JS function. Must be
96+
// called on the function context created for the function corresponding instance.
6297
//
6398
// Returns a string with JavaScript statements that define the function or
6499
// method. For methods it returns declarations for both value- and
65100
// pointer-receiver (if appropriate).
66-
func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl, inst typeparams.Instance) []byte {
101+
func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl) []byte {
67102
if fun.Recv == nil {
68-
return fc.translateStandaloneFunction(fun, inst)
103+
return fc.translateStandaloneFunction(fun)
69104
}
70105

71-
return fc.translateMethod(fun, inst)
106+
return fc.translateMethod(fun)
72107
}
73108

74109
// translateStandaloneFunction translates a package-level function.
75110
//
76111
// It returns JS statements which define the corresponding function in a
77112
// package context. Exported functions are also assigned to the `$pkg` object.
78-
func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl, inst typeparams.Instance) []byte {
79-
o := inst.Object.(*types.Func)
80-
info := fc.pkgCtx.FuncDeclInfos[o]
81-
sig := o.Type().(*types.Signature)
113+
func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl) []byte {
114+
o := fc.instance.Object.(*types.Func)
82115

83116
if fun.Recv != nil {
84117
panic(fmt.Errorf("expected standalone function, got method: %s", o))
85118
}
86119

87-
lvalue := fc.instName(inst)
120+
lvalue := fc.instName(fc.instance)
88121

89122
if fun.Body == nil {
90123
return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fc.unimplementedFunction(o)))
91124
}
92125

93-
body := fc.nestedFunctionContext(info, sig, inst).translateFunctionBody(fun.Type, nil, fun.Body, lvalue)
126+
body := fc.translateFunctionBody(fun.Type, nil, fun.Body)
94127
code := bytes.NewBuffer(nil)
95128
fmt.Fprintf(code, "\t%s = %s;\n", lvalue, body)
96129
if fun.Name.IsExported() {
@@ -103,12 +136,10 @@ func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl, inst typep
103136
//
104137
// It returns one or more JS statements which define the method. Methods with
105138
// non-pointer receiver are automatically defined for the pointer-receiver type.
106-
func (fc *funcContext) translateMethod(fun *ast.FuncDecl, inst typeparams.Instance) []byte {
107-
o := inst.Object.(*types.Func)
108-
info := fc.pkgCtx.FuncDeclInfos[o]
139+
func (fc *funcContext) translateMethod(fun *ast.FuncDecl) []byte {
140+
o := fc.instance.Object.(*types.Func)
109141
funName := fc.methodName(o)
110142

111-
sig := o.Type().(*types.Signature)
112143
// primaryFunction generates a JS function equivalent of the current Go function
113144
// and assigns it to the JS expression defined by lvalue.
114145
primaryFunction := func(lvalue string) []byte {
@@ -120,11 +151,11 @@ func (fc *funcContext) translateMethod(fun *ast.FuncDecl, inst typeparams.Instan
120151
if fun.Recv != nil && fun.Recv.List[0].Names != nil {
121152
recv = fun.Recv.List[0].Names[0]
122153
}
123-
fun := fc.nestedFunctionContext(info, sig, inst).translateFunctionBody(fun.Type, recv, fun.Body, lvalue)
154+
fun := fc.translateFunctionBody(fun.Type, recv, fun.Body)
124155
return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fun))
125156
}
126157

127-
recvInst := inst.Recv()
158+
recvInst := fc.instance.Recv()
128159
recvInstName := fc.instName(recvInst)
129160
recvType := recvInst.Object.Type().(*types.Named)
130161

@@ -134,7 +165,7 @@ func (fc *funcContext) translateMethod(fun *ast.FuncDecl, inst typeparams.Instan
134165
ptrPrototypeVar := fmt.Sprintf("$ptrType(%s).prototype.%s", recvInstName, funName)
135166

136167
// Methods with pointer-receiver are only attached to the pointer-receiver type.
137-
if _, isPointer := sig.Recv().Type().(*types.Pointer); isPointer {
168+
if _, isPointer := fc.sig.Sig.Recv().Type().(*types.Pointer); isPointer {
138169
return primaryFunction(ptrPrototypeVar)
139170
}
140171

@@ -185,7 +216,7 @@ func (fc *funcContext) unimplementedFunction(o *types.Func) string {
185216
// It returns a JS function expression that represents the given Go function.
186217
// Function receiver must have been created with nestedFunctionContext() to have
187218
// required metadata set up.
188-
func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, funcRef string) string {
219+
func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt) string {
189220
prevEV := fc.pkgCtx.escapingVars
190221

191222
// Generate a list of function argument variables. Since Go allows nameless
@@ -239,7 +270,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident,
239270

240271
sort.Strings(fc.localVars)
241272

242-
var prefix, suffix, functionName string
273+
var prefix, suffix string
243274

244275
if len(fc.Flattened) != 0 {
245276
// $s contains an index of the switch case a blocking function reached
@@ -260,21 +291,19 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident,
260291
localVarDefs := "" // Function-local var declaration at the top.
261292

262293
if len(fc.Blocking) != 0 {
263-
if funcRef == "" {
264-
funcRef = "$b"
265-
functionName = " $b"
266-
}
267-
268294
localVars := append([]string{}, fc.localVars...)
269295
// There are several special variables involved in handling blocking functions:
270296
// $r is sometimes used as a temporary variable to store blocking call result.
271297
// $c indicates that a function is being resumed after a blocking call when set to true.
272298
// $f is an object used to save and restore function context for blocking calls.
273299
localVars = append(localVars, "$r")
300+
// funcRef identifies the function object itself, so it doesn't need to be saved
301+
// or restored.
302+
localVars = removeMatching(localVars, fc.funcRef)
274303
// If a blocking function is being resumed, initialize local variables from the saved context.
275304
localVarDefs = fmt.Sprintf("var {%s, $c} = $restore(this, {%s});\n", strings.Join(localVars, ", "), strings.Join(args, ", "))
276305
// If the function gets blocked, save local variables for future.
277-
saveContext := fmt.Sprintf("var $f = {$blk: "+funcRef+", $c: true, $r, %s};", strings.Join(fc.localVars, ", "))
306+
saveContext := fmt.Sprintf("var $f = {$blk: "+fc.funcRef+", $c: true, $r, %s};", strings.Join(fc.localVars, ", "))
278307

279308
suffix = " " + saveContext + "return $f;" + suffix
280309
} else if len(fc.localVars) > 0 {
@@ -322,5 +351,5 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident,
322351

323352
fc.pkgCtx.escapingVars = prevEV
324353

325-
return fmt.Sprintf("function%s(%s) {\n%s%s}", functionName, strings.Join(args, ", "), bodyOutput, fc.Indentation(0))
354+
return fmt.Sprintf("function %s(%s) {\n%s%s}", fc.funcRef, strings.Join(args, ", "), bodyOutput, fc.Indentation(0))
326355
}

compiler/natives/src/reflect/reflect.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1778,26 +1778,28 @@ func valueMethodName() string {
17781778
var pc [5]uintptr
17791779
n := runtime.Callers(1, pc[:])
17801780
frames := runtime.CallersFrames(pc[:n])
1781+
valueTyp := TypeOf(Value{})
17811782
var frame runtime.Frame
17821783
for more := true; more; {
17831784
frame, more = frames.Next()
17841785
name := frame.Function
1785-
17861786
// Function name extracted from the call stack can be different from
17871787
// vanilla Go, so is not prefixed by "reflect.Value." as needed by the original.
17881788
// See https://cs.opensource.google/go/go/+/refs/tags/go1.19.13:src/reflect/value.go;l=173-191
1789-
// Here we try to fix stuff like "Object.$packages.reflect.Q.ptr.SetIterKey"
1790-
// into "reflect.Value.SetIterKey".
17911789
// This workaround may become obsolete after
17921790
// https://github.com/gopherjs/gopherjs/issues/1085 is resolved.
17931791

1794-
const prefix = `Object.$packages.reflect.`
1795-
if stringsHasPrefix(name, prefix) {
1796-
if idx := stringsLastIndex(name, '.'); idx >= 0 {
1797-
methodName := name[idx+1:]
1798-
if len(methodName) > 0 && 'A' <= methodName[0] && methodName[0] <= 'Z' {
1799-
return `reflect.Value.` + methodName
1800-
}
1792+
methodName := name
1793+
if idx := stringsLastIndex(name, '.'); idx >= 0 {
1794+
methodName = name[idx+1:]
1795+
}
1796+
1797+
// Since function name in the call stack doesn't contain receiver name,
1798+
// we are looking for the first exported function name that matches a
1799+
// known Value method.
1800+
if _, ok := valueTyp.MethodByName(methodName); ok {
1801+
if len(methodName) > 0 && 'A' <= methodName[0] && methodName[0] <= 'Z' {
1802+
return `reflect.Value.` + methodName
18011803
}
18021804
}
18031805
}

compiler/package.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ func (pc *pkgContext) isMain() bool {
5353
// JavaScript code (as defined for `var` declarations).
5454
type funcContext struct {
5555
*analysis.FuncInfo
56+
// Function instance this context corresponds to, or zero if the context is
57+
// top-level or doesn't correspond to a function. For function literals, this
58+
// is a synthetic object that assigns a unique identity to the function.
59+
instance typeparams.Instance
60+
// JavaScript identifier assigned to the function object (the word after the
61+
// "function" keyword in the generated code). This identifier can be used
62+
// within the function scope to reference the function object. It will also
63+
// appear in the stack trace.
64+
funcRef string
5665
// Surrounding package context.
5766
pkgCtx *pkgContext
5867
// Function context, surrounding this function definition. For package-level
@@ -104,6 +113,8 @@ type funcContext struct {
104113
typeResolver *typeparams.Resolver
105114
// Mapping from function-level objects to JS variable names they have been assigned.
106115
objectNames map[types.Object]string
116+
// Number of function literals encountered within the current function context.
117+
funcLitCounter int
107118
}
108119

109120
func newRootCtx(tContext *types.Context, srcs sources, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(*types.Func) bool, minify bool) *funcContext {

compiler/utils.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ import (
2323
"github.com/gopherjs/gopherjs/compiler/typesutil"
2424
)
2525

26+
// We use this character as a separator in synthetic identifiers instead of a
27+
// regular dot. This character is safe for use in JS identifiers and helps to
28+
// visually separate components of the name when it appears in a stack trace.
29+
const midDot = "·"
30+
2631
// root returns the topmost function context corresponding to the package scope.
2732
func (fc *funcContext) root() *funcContext {
2833
if fc.isRoot() {
@@ -376,6 +381,25 @@ func (fc *funcContext) newTypeIdent(name string, obj types.Object) *ast.Ident {
376381
return ident
377382
}
378383

384+
// newLitFuncName generates a new synthetic name for a function literal.
385+
func (fc *funcContext) newLitFuncName() string {
386+
fc.funcLitCounter++
387+
name := &strings.Builder{}
388+
389+
// If function literal is defined inside another function, qualify its
390+
// synthetic name with the outer function to make it easier to identify.
391+
if fc.instance.Object != nil {
392+
if recvType := typesutil.RecvType(fc.sig.Sig); recvType != nil {
393+
name.WriteString(recvType.Obj().Name())
394+
name.WriteString(midDot)
395+
}
396+
name.WriteString(fc.instance.Object.Name())
397+
name.WriteString(midDot)
398+
}
399+
fmt.Fprintf(name, "func%d", fc.funcLitCounter)
400+
return name.String()
401+
}
402+
379403
func (fc *funcContext) setType(e ast.Expr, t types.Type) ast.Expr {
380404
fc.pkgCtx.Types[e] = types.TypeAndValue{Type: t}
381405
return e
@@ -909,7 +933,15 @@ func rangeCheck(pattern string, constantIndex, array bool) string {
909933
}
910934

911935
func encodeIdent(name string) string {
912-
return strings.Replace(url.QueryEscape(name), "%", "$", -1)
936+
// Quick-and-dirty way to make any string safe for use as an identifier in JS.
937+
name = url.QueryEscape(name)
938+
// We use unicode middle dot as a visual separator in synthetic identifiers.
939+
// It is safe for use in a JS identifier, so we un-encode it for readability.
940+
name = strings.ReplaceAll(name, "%C2%B7", midDot)
941+
// QueryEscape uses '%' before hex-codes of escaped characters, which is not
942+
// allowed in a JS identifier, use '$' instead.
943+
name = strings.ReplaceAll(name, "%", "$")
944+
return name
913945
}
914946

915947
// formatJSStructTagVal returns JavaScript code for accessing an object's property
@@ -995,3 +1027,13 @@ func bailingOut(err interface{}) (*FatalError, bool) {
9951027
fe, ok := err.(*FatalError)
9961028
return fe, ok
9971029
}
1030+
1031+
func removeMatching[T comparable](haystack []T, needle T) []T {
1032+
var result []T
1033+
for _, el := range haystack {
1034+
if el != needle {
1035+
result = append(result, el)
1036+
}
1037+
}
1038+
return result
1039+
}

0 commit comments

Comments
 (0)