From 707213ed638396bead060fa5b83d25c3b4868c5f Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 19 Feb 2023 17:49:36 +0000 Subject: [PATCH 01/58] Factor out unimplemented function translation. (based on commit 7866b1ed6c9d0e7428f6caf7c5ab03eba9a6d345) --- compiler/functions.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/compiler/functions.go b/compiler/functions.go index ed3062c60..ba4dea86b 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -75,7 +75,7 @@ func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl, inst typepar // 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())) + return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fc.unimplementedFunction(o))) } var recv *ast.Ident @@ -162,7 +162,7 @@ func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl, inst typep lvalue := fc.instName(inst) 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) @@ -174,6 +174,15 @@ func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl, inst typep 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. From 0ee97fe943de7116192cabd9d4ce053ffe66e3c2 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 11 Mar 2023 19:56:01 +0000 Subject: [PATCH 02/58] Remove a special case for methods with array-pointer receivers. (based on commit d15130fefba8693c948c7d3daffb005880609833) This special case doesn't seem to serve any purpose that I can discern. My best guess is that it was necessary at some point, but the compiler has changed to not need it anymore. The compiler seems to wrap the returned value in a pointer-type at a call site as appropriate anyway and defining this method on the value type doesn't seem correct. --- compiler/functions.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/compiler/functions.go b/compiler/functions.go index ba4dea86b..54d857719 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -121,15 +121,7 @@ func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl, inst typepar 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() - } - + if _, isPointer := sig.Recv().Type().(*types.Pointer); isPointer { // Methods with pointer-receiver are only attached to the pointer-receiver // type. return primaryFunction(ptrPrototypeVar) From 06e892587161be23c3e10221192e3ed464118752 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 20 Jul 2024 22:11:21 +0100 Subject: [PATCH 03/58] Refactor method translation code. - Move it into a separate function, similar to translateStandaloneFunction(). - Add some comments explaining quirks of GopherJS's method implementation. (based on commit ccda9188a8667df0860cbc8fa5f428794b247284) --- compiler/expressions.go | 5 +- compiler/functions.go | 123 +++++++++++++++++++++------------------- compiler/utils.go | 15 +++++ 3 files changed, 80 insertions(+), 63 deletions(-) diff --git a/compiler/expressions.go b/compiler/expressions.go index fea80b65a..c72d91a83 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -730,10 +730,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: diff --git a/compiler/functions.go b/compiler/functions.go index 54d857719..c9fdaab30 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -5,6 +5,7 @@ package compiler import ( "bytes" + "errors" "fmt" "go/ast" "go/types" @@ -21,10 +22,10 @@ import ( // to the provided info and instance. func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, sig *types.Signature, 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")) + panic(errors.New("missing *types.Signature")) } c := &funcContext{ @@ -59,7 +60,7 @@ func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, sig *types // translateTopLevelFunction translates a top-level function declaration // (standalone function or method) into a corresponding JS function. // -// 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 { @@ -67,8 +68,45 @@ func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl, inst typepar return fc.translateStandaloneFunction(fun, inst) } + return fc.translateMethod(fun, inst) +} + +// 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, inst typeparams.Instance) []byte { o := inst.Object.(*types.Func) info := fc.pkgCtx.FuncDeclInfos[o] + sig := o.Type().(*types.Signature) + + if fun.Recv != nil { + panic(fmt.Errorf("expected standalone function, got method: %s", o)) + } + + lvalue := fc.instName(inst) + + if fun.Body == nil { + 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) + code := bytes.NewBuffer(nil) + 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 method. Methods with +// non-pointer receiver are automatically defined for the pointer-receiver type. +func (fc *funcContext) translateMethod(fun *ast.FuncDecl, inst typeparams.Instance) []byte { + o := inst.Object.(*types.Func) + info := fc.pkgCtx.FuncDeclInfos[o] + funName := fc.methodName(o) sig := o.Type().(*types.Signature) // primaryFunction generates a JS function equivalent of the current Go function @@ -86,19 +124,6 @@ func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl, inst typepar 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) @@ -108,61 +133,41 @@ func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl, inst typepar prototypeVar := fmt.Sprintf("%s.prototype.%s", recvInstName, funName) ptrPrototypeVar := fmt.Sprintf("$ptrType(%s).prototype.%s", recvInstName, funName) - code := bytes.NewBuffer(nil) + // Methods with pointer-receiver are only attached to the pointer-receiver type. + if _, isPointer := 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 { - // 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 := bytes.Buffer{} code.Write(primaryFunction(ptrPrototypeVar)) code.Write(proxyFunction(prototypeVar, "this.$val")) return code.Bytes() } - if _, isPointer := sig.Recv().Type().(*types.Pointer); isPointer { - // 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()" + proxyRecvExpr := "this.$get()" if isWrapped(recvType) { - recvExpr = fmt.Sprintf("new %s(%s)", recvInstName, recvExpr) + proxyRecvExpr = fmt.Sprintf("new %s(%s)", recvInstName, proxyRecvExpr) } + code := bytes.Buffer{} code.Write(primaryFunction(prototypeVar)) - code.Write(proxyFunction(ptrPrototypeVar, recvExpr)) - return code.Bytes() -} - -// translateStandaloneFunction translates a package-level function. -// -// It returns a 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) - - if fun.Recv != nil { - panic(fmt.Errorf("expected standalone function, got method: %s", o)) - } - - lvalue := fc.instName(inst) - - if fun.Body == nil { - 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) - code := bytes.NewBuffer(nil) - 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) - } + code.Write(proxyFunction(ptrPrototypeVar, proxyRecvExpr)) return code.Bytes() } diff --git a/compiler/utils.go b/compiler/utils.go index 7fec5b223..1911f5b2a 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -474,6 +474,21 @@ func (fc *funcContext) instName(inst typeparams.Instance) string { 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" From 677087929839a02cb5c7f446541f07bcb903627d Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Wed, 31 Jul 2024 20:25:45 +0100 Subject: [PATCH 04/58] Remove uglify-es from package.json. We haven't been using it since we switched to esbuild for prelude minification. --- package-lock.json | 27 +-------------------------- package.json | 3 --- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8fa9be563..b06b8d7ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,6 @@ "dependencies": { "source-map-support": "^0.5.19" }, - "devDependencies": { - "uglify-es": "^3.3.9" - }, "optionalDependencies": { "syscall": "file:./node-syscall" } @@ -204,12 +201,6 @@ "color-support": "bin.js" } }, - "node_modules/commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", - "dev": true - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -983,23 +974,6 @@ "node": ">=8" } }, - "node_modules/uglify-es": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", - "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", - "deprecated": "support for ECMAScript is superseded by `uglify-js` as of v3.13.0", - "dev": true, - "dependencies": { - "commander": "~2.13.0", - "source-map": "~0.6.1" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -1061,6 +1035,7 @@ "optional": true }, "node-syscall": { + "name": "syscall", "hasInstallScript": true, "license": "BSD-2-Clause", "optional": true, diff --git a/package.json b/package.json index f276a4eb1..b4ff8e786 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,6 @@ { "name": "gopherjs", "license": "BSD-2-Clause", - "devDependencies": { - "uglify-es": "^3.3.9" - }, "dependencies": { "source-map-support": "^0.5.19" }, From 22c65b81261d6ee21767ef6929d51e43b9e99047 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Wed, 31 Jul 2024 20:43:04 +0100 Subject: [PATCH 05/58] Use Node's native source map support instead of a third-party module. Since Node 12 it supports the --enable-source-maps flag, which has been considered stable since Node 14. Given we are now on 18, we can drop the unneeded dependency and use the built-in flag. --- README.md | 6 +----- package-lock.json | 25 ------------------------- package.json | 3 --- tool.go | 8 +------- 4 files changed, 2 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index b653bb177..016d41bf3 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,7 @@ _Note: GopherJS will try to write compiled object files of the core packages to #### gopherjs run, gopherjs test -If you want to use `gopherjs run` or `gopherjs test` to run the generated code locally, install Node.js 10.0.0 (or newer), and the `source-map-support` module: - -``` -npm install --global source-map-support -``` +If you want to use `gopherjs run` or `gopherjs test` to run the generated code locally, install Node.js 18 (or newer). On supported `GOOS` platforms, it's possible to make system calls (file system access, etc.) available. See [doc/syscalls.md](https://github.com/gopherjs/gopherjs/blob/master/doc/syscalls.md) for instructions on how to do so. diff --git a/package-lock.json b/package-lock.json index b06b8d7ad..b8ba5e000 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,9 +6,6 @@ "": { "name": "gopherjs", "license": "BSD-2-Clause", - "dependencies": { - "source-map-support": "^0.5.19" - }, "optionalDependencies": { "syscall": "file:./node-syscall" } @@ -140,11 +137,6 @@ "concat-map": "0.0.1" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, "node_modules/cacache": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", @@ -874,23 +866,6 @@ "node": ">= 10" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", diff --git a/package.json b/package.json index b4ff8e786..ec8add087 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,6 @@ { "name": "gopherjs", "license": "BSD-2-Clause", - "dependencies": { - "source-map-support": "^0.5.19" - }, "optionalDependencies": { "syscall": "file:./node-syscall" } diff --git a/tool.go b/tool.go index 06483e96b..d72e187c6 100644 --- a/tool.go +++ b/tool.go @@ -838,13 +838,7 @@ func sprintError(err error) string { func runNode(script string, args []string, dir string, quiet bool, out io.Writer) error { var allArgs []string if b, _ := strconv.ParseBool(os.Getenv("SOURCE_MAP_SUPPORT")); os.Getenv("SOURCE_MAP_SUPPORT") == "" || b { - allArgs = []string{"--require", "source-map-support/register"} - if err := exec.Command("node", "--require", "source-map-support/register", "--eval", "").Run(); err != nil { - if !quiet { - fmt.Fprintln(os.Stderr, "gopherjs: Source maps disabled. Install source-map-support module for nice stack traces. See https://github.com/gopherjs/gopherjs#gopherjs-run-gopherjs-test.") - } - allArgs = []string{} - } + allArgs = []string{"--enable-source-maps"} } if runtime.GOOS != "windows" { From d5771cc9f6b734ddfe33789060b8dc87f93b6905 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 21 Jul 2024 00:17:31 +0100 Subject: [PATCH 06/58] 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 4d2439585cd7b83a77e8fa81c3f4cee692be3162) --- compiler/decls.go | 2 +- compiler/expressions.go | 2 +- compiler/functions.go | 89 ++++++++++++++++--------- compiler/natives/src/reflect/reflect.go | 22 +++--- compiler/package.go | 11 +++ compiler/utils.go | 44 +++++++++++- 6 files changed, 127 insertions(+), 43 deletions(-) diff --git a/compiler/decls.go b/compiler/decls.go index 36f97d3ff..c134bc44b 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -345,7 +345,7 @@ func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance) } 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 c72d91a83..5652439fe 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -201,7 +201,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } 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 { diff --git a/compiler/functions.go b/compiler/functions.go index c9fdaab30..31a9974eb 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -18,18 +18,21 @@ import ( "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(errors.New("missing *analysis.FuncInfo")) } - if sig == nil { - panic(errors.New("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)), @@ -54,43 +57,73 @@ func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, sig *types 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 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) } - return fc.translateMethod(fun, inst) + 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, 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 = %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() { @@ -103,12 +136,10 @@ func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl, inst typep // // 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, inst typeparams.Instance) []byte { - o := inst.Object.(*types.Func) - info := fc.pkgCtx.FuncDeclInfos[o] +func (fc *funcContext) translateMethod(fun *ast.FuncDecl) []byte { + o := fc.instance.Object.(*types.Func) funName := fc.methodName(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 { @@ -120,11 +151,11 @@ func (fc *funcContext) translateMethod(fun *ast.FuncDecl, inst typeparams.Instan 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) + fun := fc.translateFunctionBody(fun.Type, recv, fun.Body) return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fun)) } - recvInst := inst.Recv() + recvInst := fc.instance.Recv() recvInstName := fc.instName(recvInst) recvType := recvInst.Object.Type().(*types.Named) @@ -134,7 +165,7 @@ func (fc *funcContext) translateMethod(fun *ast.FuncDecl, inst typeparams.Instan ptrPrototypeVar := fmt.Sprintf("$ptrType(%s).prototype.%s", recvInstName, funName) // Methods with pointer-receiver are only attached to the pointer-receiver type. - if _, isPointer := sig.Recv().Type().(*types.Pointer); isPointer { + if _, isPointer := fc.sig.Sig.Recv().Type().(*types.Pointer); isPointer { return primaryFunction(ptrPrototypeVar) } @@ -185,7 +216,7 @@ func (fc *funcContext) unimplementedFunction(o *types.Func) string { // 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 @@ -239,7 +270,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, 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 @@ -260,21 +291,19 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, 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 { @@ -322,5 +351,5 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, 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 47b93662e..81f4c7b08 100644 --- a/compiler/natives/src/reflect/reflect.go +++ b/compiler/natives/src/reflect/reflect.go @@ -1778,26 +1778,28 @@ func valueMethodName() string { 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/package.go b/compiler/package.go index 34387b5ab..bcdfca514 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -53,6 +53,15 @@ func (pc *pkgContext) isMain() bool { // 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 @@ type funcContext struct { 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 1911f5b2a..62c09a09d 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -23,6 +23,11 @@ import ( "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 @@ func (fc *funcContext) newTypeIdent(name string, obj types.Object) *ast.Ident { 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 @@ -909,7 +933,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 @@ -995,3 +1027,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 +} From 4ebc56caf2e980f9e3dd2416797406d1d42802a3 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 3 Aug 2024 16:36:33 +0100 Subject: [PATCH 07/58] Temporarily disable reflect tests that depend on caller function names. JS function names are subtly different from what vanilla Go may expect, unless https://github.com/gopherjs/gopherjs/issues/1085 is implemented. It turns out that a combination of d5771cc9f6b734ddfe33789060b8dc87f93b6905 and 22c65b81261d6ee21767ef6929d51e43b9e99047 subtly changes how node outputs stack trace in a way that breaks my workarounds in the reflect package. Instead of further fumbling, I am going to disable the offending tests temporarily, and I have a proper fix for #1085 in the works, which will allow us to re-enable them along with a few other tests. --- compiler/natives/src/reflect/reflect_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/compiler/natives/src/reflect/reflect_test.go b/compiler/natives/src/reflect/reflect_test.go index 79bbe5385..4c0bcd0be 100644 --- a/compiler/natives/src/reflect/reflect_test.go +++ b/compiler/natives/src/reflect/reflect_test.go @@ -298,3 +298,23 @@ func TestIssue50208(t *testing.T) { 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") +} From 3b94762ec446cffd6b28800708de3d8bc1263584 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 22 Aug 2024 09:13:16 -0600 Subject: [PATCH 08/58] Encapsulating DCE --- compiler/compiler.go | 57 +-- compiler/decls.go | 63 ++- compiler/expressions.go | 4 +- compiler/internal/dce/collector.go | 46 +++ compiler/internal/dce/dce_test.go | 631 +++++++++++++++++++++++++++++ compiler/internal/dce/info.go | 108 +++++ compiler/internal/dce/selector.go | 93 +++++ compiler/package.go | 4 +- compiler/statements.go | 2 +- compiler/utils.go | 41 +- 10 files changed, 919 insertions(+), 130 deletions(-) create mode 100644 compiler/internal/dce/collector.go create mode 100644 compiler/internal/dce/dce_test.go create mode 100644 compiler/internal/dce/info.go create mode 100644 compiler/internal/dce/selector.go diff --git a/compiler/compiler.go b/compiler/compiler.go index b8f6a49bc..cffd4c86d 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -17,6 +17,7 @@ import ( "strings" "time" + "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/prelude" "golang.org/x/tools/go/gcexportdata" ) @@ -125,12 +126,6 @@ func ImportDependencies(archive *Archive, importPkg func(string) (*Archive, erro return deps, nil } -type dceInfo struct { - decl *Decl - objectFilter string - methodFilter string -} - func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) error { mainPkg := pkgs[len(pkgs)-1] minify := mainPkg.Minified @@ -141,61 +136,21 @@ func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) err gls.Add(pkg.GoLinknames) } - byFilter := make(map[string][]*dceInfo) - var pendingDecls []*Decl // A queue of live decls to find other live decls. + sel := &dce.Selector[*Decl]{} for _, pkg := range pkgs { for _, d := range pkg.Declarations { - if d.DceObjectFilter == "" && d.DceMethodFilter == "" { - // This is an entry point (like main() or init() functions) or a variable - // initializer which has a side effect, consider it live. - pendingDecls = append(pendingDecls, d) - continue - } + implementsLink := false if gls.IsImplementation(d.LinkingName) { // If a decl is referenced by a go:linkname directive, we just assume // it's not dead. // TODO(nevkontakte): This is a safe, but imprecise assumption. We should // try and trace whether the referencing functions are actually live. - pendingDecls = append(pendingDecls, d) - } - info := &dceInfo{decl: d} - if d.DceObjectFilter != "" { - info.objectFilter = pkg.ImportPath + "." + d.DceObjectFilter - byFilter[info.objectFilter] = append(byFilter[info.objectFilter], info) - } - if d.DceMethodFilter != "" { - info.methodFilter = pkg.ImportPath + "." + d.DceMethodFilter - byFilter[info.methodFilter] = append(byFilter[info.methodFilter], info) - } - } - } - - dceSelection := make(map[*Decl]struct{}) // Known live decls. - for len(pendingDecls) != 0 { - d := pendingDecls[len(pendingDecls)-1] - pendingDecls = pendingDecls[:len(pendingDecls)-1] - - dceSelection[d] = struct{}{} // Mark the decl as live. - - // Consider all decls the current one is known to depend on and possible add - // them to the live queue. - for _, dep := range d.DceDeps { - if infos, ok := byFilter[dep]; ok { - delete(byFilter, dep) - for _, info := range infos { - if info.objectFilter == dep { - info.objectFilter = "" - } - if info.methodFilter == dep { - info.methodFilter = "" - } - if info.objectFilter == "" && info.methodFilter == "" { - pendingDecls = append(pendingDecls, info.decl) - } - } + implementsLink = true } + sel.Include(d, implementsLink) } } + dceSelection := sel.AliveDecls() if _, err := w.Write([]byte("\"use strict\";\n(function() {\n\n")); err != nil { return err diff --git a/compiler/decls.go b/compiler/decls.go index c134bc44b..b6427a697 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" @@ -51,16 +52,8 @@ type Decl struct { // 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 + // dce stores the information for dead-code elimination. + dce dce.Info // 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 @@ -78,6 +71,11 @@ func (d Decl) minify() Decl { return d } +// Dce gets the information for dead-code elimination. +func (d *Decl) Dce() *dce.Info { + return &d.dce +} + // 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 typesutil.TypeNames) { @@ -161,11 +159,13 @@ func (fc *funcContext) importDecls() (importedPaths []string, importDecls []*Dec // 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{ + d := &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) }), } + d.Dce().SetAsAlive() + return d } // importInitializer calls the imported package $init() function to ensure it is @@ -241,7 +241,7 @@ func (fc *funcContext) newVarDecl(init *types.Initializer) *Decl { } } - d.DceDeps = fc.CollectDCEDeps(func() { + fc.pkgCtx.CollectDCEDeps(&d, func() { fc.localVars = nil d.InitCode = fc.CatchOutput(1, func() { fc.translateStmt(&ast.AssignStmt{ @@ -257,10 +257,9 @@ func (fc *funcContext) newVarDecl(init *types.Initializer) *Decl { 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() - } + d.Dce().SetName(init.Lhs[0]) + if len(init.Lhs) != 1 || analysis.HasSideEffect(init.Rhs, fc.pkgCtx.Info.Info) { + d.Dce().SetAsAlive() } return &d } @@ -280,9 +279,8 @@ func (fc *funcContext) funcDecls(functions []*ast.FuncDecl) ([]*Decl, error) { if fun.Recv == nil { // Auxiliary decl shared by all instances of the function that defines // package-level variable by which they all are referenced. - // TODO(nevkontakte): Set DCE attributes such that it is eliminated if all - // instances are dead. varDecl := Decl{} + varDecl.Dce().SetName(o) varDecl.Vars = []string{fc.objectName(o)} if o.Type().(*types.Signature).TypeParams().Len() != 0 { varDecl.DeclCode = fc.CatchOutput(0, func() { @@ -322,29 +320,25 @@ func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance) Blocking: fc.pkgCtx.IsBlocking(o), LinkingName: symbol.New(o), } + d.Dce().SetName(o) if typesutil.IsMethod(o) { recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj() d.NamedRecvType = fc.objectName(recv) - d.DceObjectFilter = recv.Name() - if !fun.Name.IsExported() { - d.DceMethodFilter = o.Name() + "~" - } } else { d.RefExpr = fc.instName(inst) - d.DceObjectFilter = o.Name() switch o.Name() { case "main": if fc.pkgCtx.isMain() { // Found main() function of the program. - d.DceObjectFilter = "" // Always reachable. + d.Dce().SetAsAlive() // Always reachable. } case "init": d.InitCode = fc.CatchOutput(1, func() { fc.translateStmt(fc.callInitFunc(o), nil) }) - d.DceObjectFilter = "" // init() function is always reachable. + d.Dce().SetAsAlive() // init() function is always reachable. } } - d.DceDeps = fc.CollectDCEDeps(func() { + fc.pkgCtx.CollectDCEDeps(d, func() { d.DeclCode = fc.namedFuncContext(inst).translateTopLevelFunction(fun) }) return d @@ -455,10 +449,9 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er } underlying := instanceType.Underlying() - d := &Decl{ - DceObjectFilter: inst.Object.Name(), - } - d.DceDeps = fc.CollectDCEDeps(func() { + d := &Decl{} + d.Dce().SetName(inst.Object) + fc.pkgCtx.CollectDCEDeps(d, func() { // Code that declares a JS type (i.e. prototype) for each Go type. d.DeclCode = fc.CatchOutput(0, func() { size := int64(0) @@ -577,14 +570,14 @@ func (fc *funcContext) anonTypeDecls(anonTypes []*types.TypeName) []*Decl { } decls := []*Decl{} for _, t := range anonTypes { - d := Decl{ - Vars: []string{t.Name()}, - DceObjectFilter: t.Name(), + d := &Decl{ + Vars: []string{t.Name()}, } - d.DceDeps = fc.CollectDCEDeps(func() { + d.Dce().SetName(t) + fc.pkgCtx.CollectDCEDeps(d, 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) + decls = append(decls, d) } return decls } diff --git a/compiler/expressions.go b/compiler/expressions.go index 5652439fe..dcf1b7844 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -592,7 +592,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.DeclareDCEDep(sel.Obj()) + fc.pkgCtx.DeclareDCEDep(sel.Obj()) } if _, ok := sel.Recv().Underlying().(*types.Interface); ok { return fc.formatExpr(`$ifaceMethodExpr("%s")`, sel.Obj().(*types.Func).Name()) @@ -908,7 +908,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression { sel, _ := fc.selectionOf(e) if !sel.Obj().Exported() { - fc.DeclareDCEDep(sel.Obj()) + fc.pkgCtx.DeclareDCEDep(sel.Obj()) } x := e.X diff --git a/compiler/internal/dce/collector.go b/compiler/internal/dce/collector.go new file mode 100644 index 000000000..7d251029b --- /dev/null +++ b/compiler/internal/dce/collector.go @@ -0,0 +1,46 @@ +package dce + +import ( + "errors" + "go/types" +) + +// Decl is any code declaration that has dead-code elimination (DCE) +// information attached to it. +type Decl interface { + Dce() *Info +} + +// Collector is a tool to collect dependencies for a declaration +// that'll be used in dead-code elimination (DCE). +type Collector struct { + dependencies map[types.Object]struct{} +} + +// CollectDCEDeps captures a list of Go objects (types, functions, etc.) +// the code translated inside f() depends on. Then sets those objects +// as dependencies of the given dead-code elimination info. +// +// Only one CollectDCEDeps call can be active at a time. +// This will overwrite any previous dependencies collected for the given DCE. +func (c *Collector) CollectDCEDeps(decl Decl, f func()) { + if c.dependencies != nil { + panic(errors.New(`called CollectDCEDeps inside another CollectDCEDeps call`)) + } + + c.dependencies = make(map[types.Object]struct{}) + defer func() { c.dependencies = nil }() + + f() + + decl.Dce().setDeps(c.dependencies) +} + +// DeclareDCEDep records that the code that is currently being transpiled +// depends on a given Go object. +func (c *Collector) DeclareDCEDep(o types.Object) { + if c.dependencies == nil { + return // Dependencies are not being collected. + } + c.dependencies[o] = struct{}{} +} diff --git a/compiler/internal/dce/dce_test.go b/compiler/internal/dce/dce_test.go new file mode 100644 index 000000000..c46a7f03c --- /dev/null +++ b/compiler/internal/dce/dce_test.go @@ -0,0 +1,631 @@ +package dce + +import ( + "fmt" + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "regexp" + "sort" + "testing" +) + +func Test_Collector_CalledOnce(t *testing.T) { + var c Collector + decl1 := &testDecl{} + decl2 := &testDecl{} + + err := capturePanic(t, func() { + c.CollectDCEDeps(decl1, func() { + c.CollectDCEDeps(decl2, func() { + t.Fatal(`the nested collect function was called`) + }) + }) + }) + errorMatches(t, err, `^called CollectDCEDeps inside another`) +} + +func Test_Collector_Collecting(t *testing.T) { + pkg := testPackage(`tristan`) + obj1 := quickVar(pkg, `Primus`) + obj2 := quickVar(pkg, `Secundus`) + obj3 := quickVar(pkg, `Tertius`) + obj4 := quickVar(pkg, `Quartus`) + obj5 := quickVar(pkg, `Quintus`) + obj6 := quickVar(pkg, `Sextus`) + obj7 := quickVar(pkg, `Una`) + + decl1 := quickTestDecl(obj1) + decl2 := quickTestDecl(obj2) + var c Collector + + c.DeclareDCEDep(obj1) // no effect since a collection isn't running. + depCount(t, decl1, 0) + depCount(t, decl2, 0) + + c.CollectDCEDeps(decl1, func() { + c.DeclareDCEDep(obj2) + c.DeclareDCEDep(obj3) + c.DeclareDCEDep(obj3) // already added so has no effect. + }) + depCount(t, decl1, 2) + depCount(t, decl2, 0) + + c.DeclareDCEDep(obj4) // no effect since a collection isn't running. + depCount(t, decl1, 2) + depCount(t, decl2, 0) + + c.CollectDCEDeps(decl2, func() { + c.DeclareDCEDep(obj5) + c.DeclareDCEDep(obj6) + c.DeclareDCEDep(obj7) + }) + depCount(t, decl1, 2) + depCount(t, decl2, 3) + + // The second collection overwrites the first collection. + c.CollectDCEDeps(decl2, func() { + c.DeclareDCEDep(obj5) + }) + depCount(t, decl1, 2) + depCount(t, decl2, 1) +} + +func Test_Info_SetNameAndDep(t *testing.T) { + tests := []struct { + name string + obj types.Object + want Info // expected Info after SetName + wantDep string // expected dep after addDep + }{ + { + name: `package`, + obj: parseObject(t, `Sarah`, + `package jim + import Sarah "fmt"`), + want: Info{ + importPath: `jim`, + objectFilter: `Sarah`, + }, + wantDep: `jim.Sarah`, + }, + { + name: `exposed var`, + obj: parseObject(t, `Toby`, + `package jim + var Toby float64`), + want: Info{ + importPath: `jim`, + objectFilter: `Toby`, + }, + wantDep: `jim.Toby`, + }, + { + name: `exposed const`, + obj: parseObject(t, `Ludo`, + `package jim + const Ludo int = 42`), + want: Info{ + importPath: `jim`, + objectFilter: `Ludo`, + }, + wantDep: `jim.Ludo`, + }, + { + name: `label`, + obj: parseObject(t, `Gobo`, + `package jim + func main() { + i := 0 + Gobo: + i++ + if i < 10 { + goto Gobo + } + }`), + want: Info{ + importPath: `jim`, + objectFilter: `Gobo`, + }, + wantDep: `jim.Gobo`, + }, + { + name: `exposed specific type`, + obj: parseObject(t, `Jen`, + `package jim + type Jen struct{}`), + want: Info{ + importPath: `jim`, + objectFilter: `Jen`, + }, + wantDep: `jim.Jen`, + }, + { + name: `exposed generic type`, + obj: parseObject(t, `Henson`, + `package jim + type Henson[T comparable] struct{}`), + want: Info{ + importPath: `jim`, + objectFilter: `Henson`, + }, + wantDep: `jim.Henson`, + }, + { + name: `exposed specific function`, + obj: parseObject(t, `Jareth`, + `package jim + func Jareth() {}`), + want: Info{ + importPath: `jim`, + objectFilter: `Jareth`, + }, + wantDep: `jim.Jareth`, + }, + { + name: `exposed generic function`, + obj: parseObject(t, `Didymus`, + `package jim + func Didymus[T comparable]() {}`), + want: Info{ + importPath: `jim`, + objectFilter: `Didymus`, + }, + wantDep: `jim.Didymus`, + }, + { + name: `exposed specific method`, + obj: parseObject(t, `Kira`, + `package jim + type Fizzgig string + func (f Fizzgig) Kira() {}`), + want: Info{ + importPath: `jim`, + objectFilter: `Fizzgig`, + }, + wantDep: `jim.Kira~`, + }, + { + name: `unexposed specific method`, + obj: parseObject(t, `frank`, + `package jim + type Aughra int + func (a Aughra) frank() {}`), + want: Info{ + importPath: `jim`, + objectFilter: `Aughra`, + methodFilter: `frank~`, + }, + wantDep: `jim.frank~`, + }, + { + name: `specific method on unexposed type`, + obj: parseObject(t, `Red`, + `package jim + type wembley struct{} + func (w wembley) Red() {}`), + want: Info{ + importPath: `jim`, + objectFilter: `wembley`, + }, + wantDep: `jim.Red~`, + }, + } + + t.Run(`SetName`, func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &testDecl{} + equal(t, d.Dce().unnamed(), true) + equal(t, d.Dce().String(), `[unnamed] . -> []`) + t.Log(`object:`, types.ObjectString(tt.obj, nil)) + + d.Dce().SetName(tt.obj) + equal(t, d.Dce().unnamed(), tt.want.unnamed()) + equal(t, d.Dce().importPath, tt.want.importPath) + equal(t, d.Dce().objectFilter, tt.want.objectFilter) + equal(t, d.Dce().methodFilter, tt.want.methodFilter) + equal(t, d.Dce().String(), tt.want.String()) + }) + } + }) + + t.Run(`addDep`, func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &testDecl{} + t.Log(`object:`, types.ObjectString(tt.obj, nil)) + + d.Dce().setDeps(map[types.Object]struct{}{ + tt.obj: {}, + }) + equal(t, len(d.Dce().deps), 1) + equal(t, d.Dce().deps[0], tt.wantDep) + }) + } + }) +} + +func Test_Info_SetNameOnlyOnce(t *testing.T) { + pkg := testPackage(`mogwai`) + obj1 := quickVar(pkg, `Gizmo`) + obj2 := quickVar(pkg, `Stripe`) + + decl := &testDecl{} + decl.Dce().SetName(obj1) + + err := capturePanic(t, func() { + decl.Dce().SetName(obj2) + }) + errorMatches(t, err, `^may only set the name once for path/to/mogwai\.Gizmo .*$`) +} + +func Test_Info_SetAsAlive(t *testing.T) { + pkg := testPackage(`fantasia`) + + t.Run(`set alive prior to naming`, func(t *testing.T) { + obj := quickVar(pkg, `Falkor`) + decl := &testDecl{} + equal(t, decl.Dce().isAlive(), true) // unnamed is automatically alive + equal(t, decl.Dce().String(), `[unnamed] . -> []`) + + decl.Dce().SetAsAlive() + equal(t, decl.Dce().isAlive(), true) // still alive but now explicitly alive + equal(t, decl.Dce().String(), `[alive] [unnamed] . -> []`) + + decl.Dce().SetName(obj) + equal(t, decl.Dce().isAlive(), true) // alive because SetAsAlive was called + equal(t, decl.Dce().String(), `[alive] path/to/fantasia.Falkor -> []`) + }) + + t.Run(`set alive after naming`, func(t *testing.T) { + obj := quickVar(pkg, `Artax`) + decl := &testDecl{} + equal(t, decl.Dce().isAlive(), true) // unnamed is automatically alive + equal(t, decl.Dce().String(), `[unnamed] . -> []`) + + decl.Dce().SetName(obj) + equal(t, decl.Dce().isAlive(), false) // named so no longer automatically alive + equal(t, decl.Dce().String(), `path/to/fantasia.Artax -> []`) + + decl.Dce().SetAsAlive() + equal(t, decl.Dce().isAlive(), true) // alive because SetAsAlive was called + equal(t, decl.Dce().String(), `[alive] path/to/fantasia.Artax -> []`) + }) +} + +func Test_Selector_JustVars(t *testing.T) { + pkg := testPackage(`tolkien`) + frodo := quickTestDecl(quickVar(pkg, `Frodo`)) + samwise := quickTestDecl(quickVar(pkg, `Samwise`)) + meri := quickTestDecl(quickVar(pkg, `Meri`)) + pippin := quickTestDecl(quickVar(pkg, `Pippin`)) + aragorn := quickTestDecl(quickVar(pkg, `Aragorn`)) + boromir := quickTestDecl(quickVar(pkg, `Boromir`)) + gimli := quickTestDecl(quickVar(pkg, `Gimli`)) + legolas := quickTestDecl(quickVar(pkg, `Legolas`)) + gandalf := quickTestDecl(quickVar(pkg, `Gandalf`)) + fellowship := []*testDecl{ + frodo, samwise, meri, pippin, aragorn, + boromir, gimli, legolas, gandalf, + } + + c := Collector{} + c.CollectDCEDeps(frodo, func() { + c.DeclareDCEDep(samwise.obj) + c.DeclareDCEDep(meri.obj) + c.DeclareDCEDep(pippin.obj) + }) + c.CollectDCEDeps(pippin, func() { + c.DeclareDCEDep(meri.obj) + }) + c.CollectDCEDeps(aragorn, func() { + c.DeclareDCEDep(boromir.obj) + }) + c.CollectDCEDeps(gimli, func() { + c.DeclareDCEDep(legolas.obj) + }) + c.CollectDCEDeps(legolas, func() { + c.DeclareDCEDep(gimli.obj) + }) + c.CollectDCEDeps(gandalf, func() { + c.DeclareDCEDep(frodo.obj) + c.DeclareDCEDep(aragorn.obj) + c.DeclareDCEDep(gimli.obj) + c.DeclareDCEDep(legolas.obj) + }) + + for _, decl := range fellowship { + equal(t, decl.Dce().isAlive(), false) + } + + tests := []struct { + name string + init []*testDecl // which decls to set explicitly alive + want []*testDecl // which decls should be determined as alive + }{ + { + name: `all alive`, + init: fellowship, + want: fellowship, + }, + { + name: `all dead`, + init: []*testDecl{}, + want: []*testDecl{}, + }, + { + name: `Frodo`, + init: []*testDecl{frodo}, + want: []*testDecl{frodo, samwise, meri, pippin}, + }, + { + name: `Sam and Pippin`, + init: []*testDecl{samwise, pippin}, + want: []*testDecl{samwise, meri, pippin}, + }, + { + name: `Gandalf`, + init: []*testDecl{gandalf}, + want: fellowship, + }, + { + name: `Legolas`, + init: []*testDecl{legolas}, + want: []*testDecl{legolas, gimli}, + }, + { + name: `Gimli`, + init: []*testDecl{gimli}, + want: []*testDecl{legolas, gimli}, + }, + { + name: `Boromir`, + init: []*testDecl{boromir}, + want: []*testDecl{boromir}, + }, + { + name: `Aragorn`, + init: []*testDecl{aragorn}, + want: []*testDecl{aragorn, boromir}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, decl := range fellowship { + decl.Dce().alive = false + } + for _, decl := range tt.init { + decl.Dce().SetAsAlive() + } + + s := &Selector[*testDecl]{} + for _, decl := range fellowship { + s.Include(decl, false) + } + + selected := s.AliveDecls() + for _, decl := range tt.want { + if _, ok := selected[decl]; !ok { + t.Errorf(`expected %q to be alive`, decl.obj.String()) + } + delete(selected, decl) + } + for decl := range selected { + t.Errorf(`expected %q to be dead`, decl.obj.String()) + } + }) + } +} + +func Test_Selector_SpecificMethods(t *testing.T) { + objects := parseObjects(t, + `package pratchett + + type rincewind struct{} + func (r rincewind) Run() {} + func (r rincewind) hide() {} + + type Vimes struct{} + func (v Vimes) Run() {} + func (v Vimes) Read() {} + + func Vetinari() {}`) + + var ( + // Objects are in read order so pick the objects we want for this test + // while skipping over `r rincewind` and `v Vimes`. + rincewind = quickTestDecl(objects[0]) + rincewindRun = quickTestDecl(objects[2]) + rincewindHide = quickTestDecl(objects[4]) + vimes = quickTestDecl(objects[5]) + vimesRun = quickTestDecl(objects[7]) + vimesRead = quickTestDecl(objects[9]) + vetinari = quickTestDecl(objects[10]) + ) + allDecls := []*testDecl{rincewind, rincewindRun, rincewindHide, vimes, vimesRun, vimesRead, vetinari} + + c := Collector{} + c.CollectDCEDeps(rincewindRun, func() { + c.DeclareDCEDep(rincewind.obj) + }) + c.CollectDCEDeps(rincewindHide, func() { + c.DeclareDCEDep(rincewind.obj) + }) + c.CollectDCEDeps(vimesRun, func() { + c.DeclareDCEDep(vimes.obj) + }) + c.CollectDCEDeps(vimesRead, func() { + c.DeclareDCEDep(vimes.obj) + }) + vetinari.Dce().SetAsAlive() + + tests := []struct { + name string + deps []*testDecl // which decls are vetinari dependent on + want []*testDecl // which decls should be determined as alive + }{ + { + name: `no deps`, + deps: []*testDecl{}, + want: []*testDecl{vetinari}, + }, + { + name: `structs`, + deps: []*testDecl{rincewind, vimes}, + // rincewindHide is not included because it is not exported and not used. + want: []*testDecl{rincewind, rincewindRun, vimes, vimesRun, vimesRead, vetinari}, + }, + { + name: `exposed method`, + deps: []*testDecl{rincewind, rincewindRun}, + want: []*testDecl{rincewind, rincewindRun, vetinari}, + }, + { + name: `unexposed method`, + deps: []*testDecl{rincewind, rincewindHide}, + want: []*testDecl{rincewind, rincewindRun, rincewindHide, vetinari}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c.CollectDCEDeps(vetinari, func() { + for _, decl := range tt.deps { + c.DeclareDCEDep(decl.obj) + } + }) + + s := Selector[*testDecl]{} + for _, decl := range allDecls { + s.Include(decl, false) + } + selected := s.AliveDecls() + for _, decl := range tt.want { + if _, ok := selected[decl]; !ok { + t.Errorf(`expected %q to be alive`, decl.obj.String()) + } + delete(selected, decl) + } + for decl := range selected { + t.Errorf(`expected %q to be dead`, decl.obj.String()) + } + }) + } +} + +type testDecl struct { + obj types.Object // should match the object used in Dce.SetName when set + dce Info +} + +func (d *testDecl) Dce() *Info { + return &d.dce +} + +func testPackage(name string) *types.Package { + return types.NewPackage(`path/to/`+name, name) +} + +func quickTestDecl(o types.Object) *testDecl { + d := &testDecl{obj: o} + d.Dce().SetName(o) + return d +} + +func quickVar(pkg *types.Package, name string) *types.Var { + return types.NewVar(token.NoPos, pkg, name, types.Typ[types.Int]) +} + +func parseObject(t *testing.T, name, source string) types.Object { + t.Helper() + objects := parseObjects(t, source) + for _, obj := range objects { + if obj.Name() == name { + return obj + } + } + t.Fatalf(`object %q not found`, name) + return nil +} + +func parseObjects(t *testing.T, source string) []types.Object { + t.Helper() + info := &types.Info{ + Defs: map[*ast.Ident]types.Object{}, + } + parseInfo(t, source, info) + objects := make([]types.Object, 0, len(info.Defs)) + for _, obj := range info.Defs { + if obj != nil { + objects = append(objects, obj) + } + } + sort.Slice(objects, func(i, j int) bool { + return objects[i].Pos() < objects[j].Pos() + }) + return objects +} + +func parseInfo(t *testing.T, source string, info *types.Info) *types.Package { + t.Helper() + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, `test.go`, source, 0) + if err != nil { + t.Fatal(`parsing source:`, err) + } + + conf := types.Config{ + Importer: importer.Default(), + DisableUnusedImportCheck: true, + } + pkg, err := conf.Check(f.Name.Name, fset, []*ast.File{f}, info) + if err != nil { + t.Fatal(`type checking:`, err) + } + return pkg +} + +func capturePanic(t *testing.T, f func()) (err error) { + t.Helper() + defer func() { + t.Helper() + if r := recover(); r != nil { + if err2, ok := r.(error); ok { + err = err2 + return + } + t.Errorf(`expected an error to be panicked but got (%[1]T) %[1]#v`, r) + return + } + t.Error(`expected a panic but got none`) + }() + + f() + return nil +} + +func errorMatches(t *testing.T, err error, wantPattern string) { + t.Helper() + re := regexp.MustCompile(wantPattern) + if got := fmt.Sprint(err); !re.MatchString(got) { + t.Errorf(`expected error %q to match %q`, got, re.String()) + } +} + +func depCount(t *testing.T, decl *testDecl, want int) { + t.Helper() + if got := len(decl.Dce().deps); got != want { + t.Errorf(`expected %d deps but got %d`, want, got) + } +} + +func equal[T comparable](t *testing.T, got, want T) { + t.Helper() + if got != want { + t.Errorf(`expected %#v but got %#v`, want, got) + } +} diff --git a/compiler/internal/dce/info.go b/compiler/internal/dce/info.go new file mode 100644 index 000000000..d5993a659 --- /dev/null +++ b/compiler/internal/dce/info.go @@ -0,0 +1,108 @@ +package dce + +import ( + "fmt" + "go/types" + "sort" + "strings" + + "github.com/gopherjs/gopherjs/compiler/typesutil" +) + +// Info contains information used by the dead-code elimination (DCE) logic to +// determine whether a declaration is alive or dead. +type Info struct { + + // alive indicates if the declaration is marked as alive + // and will not be eliminated. + alive bool + + // importPath is the package path of the package the declaration is in. + importPath string + + // 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. + objectFilter string + + // The second part of the identified used by dead-code elimination for methods. + // Empty for other types of symbols. + methodFilter string + + // List of fully qualified (including package path) DCE symbol identifiers the + // symbol depends on for dead code elimination purposes. + deps []string +} + +// String gets a human-readable representation of the DCE info. +func (d *Info) String() string { + tags := `` + if d.alive { + tags += `[alive] ` + } + if d.unnamed() { + tags += `[unnamed] ` + } + fullName := d.importPath + `.` + d.objectFilter + if len(d.methodFilter) > 0 { + fullName += `.` + d.methodFilter + } + return tags + fullName + ` -> [` + strings.Join(d.deps, `, `) + `]` +} + +// unnamed returns true if SetName has not been called for this declaration. +// This indicates that the DCE is not initialized. +func (d *Info) unnamed() bool { + return d.objectFilter == `` && d.methodFilter == `` +} + +// isAlive returns true if the declaration is marked as alive. +// +// Returns true if SetAsAlive was called on this declaration or +// if SetName was not called meaning the DCE is not initialized. +func (d *Info) isAlive() bool { + return d.alive || d.unnamed() +} + +// SetAsAlive marks the declaration as alive, meaning it will not be eliminated. +// +// This should be called by an entry point (like main() or init() functions) +// or a variable initializer which has a side effect, consider it live. +func (d *Info) SetAsAlive() { + d.alive = true +} + +// SetName sets the name used by DCE to represent the declaration +// this DCE info is attached to. +func (d *Info) SetName(o types.Object) { + if !d.unnamed() { + panic(fmt.Errorf(`may only set the name once for %s`, d.String())) + } + + d.importPath = o.Pkg().Path() + if typesutil.IsMethod(o) { + recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj() + d.objectFilter = recv.Name() + if !o.Exported() { + d.methodFilter = o.Name() + `~` + } + } else { + d.objectFilter = o.Name() + } +} + +// setDeps sets the declaration dependencies used by DCE +// for the declaration this DCE info is attached to. +// This overwrites any prior set dependencies. +func (d *Info) setDeps(objectSet map[types.Object]struct{}) { + deps := make([]string, 0, len(objectSet)) + for o := range objectSet { + qualifiedName := o.Pkg().Path() + "." + o.Name() + if typesutil.IsMethod(o) { + qualifiedName += "~" + } + deps = append(deps, qualifiedName) + } + sort.Strings(deps) + d.deps = deps +} diff --git a/compiler/internal/dce/selector.go b/compiler/internal/dce/selector.go new file mode 100644 index 000000000..4eea572e0 --- /dev/null +++ b/compiler/internal/dce/selector.go @@ -0,0 +1,93 @@ +package dce + +// DeclConstraint is type constraint for any code declaration that has +// dead-code elimination (DCE) information attached to it and will be +// used in a set. +type DeclConstraint interface { + Decl + comparable +} + +// Selector gathers all declarations that are still alive after dead-code elimination. +type Selector[D DeclConstraint] struct { + byFilter map[string][]*declInfo[D] + + // A queue of live decls to find other live decls. + pendingDecls []D +} + +type declInfo[D DeclConstraint] struct { + decl D + objectFilter string + methodFilter string +} + +// Include will add a new declaration to be checked as alive or not. +func (s *Selector[D]) Include(decl D, implementsLink bool) { + if s.byFilter == nil { + s.byFilter = make(map[string][]*declInfo[D]) + } + + dce := decl.Dce() + + if dce.isAlive() { + s.pendingDecls = append(s.pendingDecls, decl) + return + } + + if implementsLink { + s.pendingDecls = append(s.pendingDecls, decl) + } + + info := &declInfo[D]{decl: decl} + + if dce.objectFilter != `` { + info.objectFilter = dce.importPath + `.` + dce.objectFilter + s.byFilter[info.objectFilter] = append(s.byFilter[info.objectFilter], info) + } + + if dce.methodFilter != `` { + info.methodFilter = dce.importPath + `.` + dce.methodFilter + s.byFilter[info.methodFilter] = append(s.byFilter[info.methodFilter], info) + } +} + +func (s *Selector[D]) popPending() D { + max := len(s.pendingDecls) - 1 + d := s.pendingDecls[max] + s.pendingDecls = s.pendingDecls[:max] + return d +} + +// AliveDecls returns a set of declarations that are still alive +// after dead-code elimination. +// This should only be called once all declarations have been included. +func (s *Selector[D]) AliveDecls() map[D]struct{} { + dceSelection := make(map[D]struct{}) // Known live decls. + for len(s.pendingDecls) != 0 { + d := s.popPending() + dce := d.Dce() + + dceSelection[d] = struct{}{} // Mark the decl as live. + + // Consider all decls the current one is known to depend on and possible add + // them to the live queue. + for _, dep := range dce.deps { + if infos, ok := s.byFilter[dep]; ok { + delete(s.byFilter, dep) + for _, info := range infos { + if info.objectFilter == dep { + info.objectFilter = `` + } + if info.methodFilter == dep { + info.methodFilter = `` + } + if info.objectFilter == `` && info.methodFilter == `` { + s.pendingDecls = append(s.pendingDecls, info.decl) + } + } + } + } + } + return dceSelection +} diff --git a/compiler/package.go b/compiler/package.go index bcdfca514..430542b65 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -11,6 +11,7 @@ import ( "time" "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" "github.com/gopherjs/gopherjs/internal/experiments" @@ -21,6 +22,7 @@ import ( // pkgContext maintains compiler context for a specific package. type pkgContext struct { *analysis.Info + dce.Collector additionalSelections map[*ast.SelectorExpr]typesutil.Selection typesCtx *types.Context @@ -35,7 +37,6 @@ type pkgContext struct { anonTypeMap typeutil.Map escapingVars map[*types.Var]bool indentation int - dependencies map[types.Object]bool minify bool fileSet *token.FileSet errList ErrorList @@ -136,7 +137,6 @@ func newRootCtx(tContext *types.Context, srcs sources, typesInfo *types.Info, ty varPtrNames: make(map[*types.Var]string), escapingVars: make(map[*types.Var]bool), indentation: 1, - dependencies: nil, minify: minify, fileSet: srcs.FileSet, instanceSet: tc.Instances, diff --git a/compiler/statements.go b/compiler/statements.go index 3d7210e47..d4ca76471 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -445,7 +445,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.Add(o) - fc.DeclareDCEDep(o) + fc.pkgCtx.DeclareDCEDep(o) } case token.CONST: // skip, constants are inlined diff --git a/compiler/utils.go b/compiler/utils.go index 62c09a09d..a69d0fe77 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -107,43 +107,6 @@ 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. @@ -452,7 +415,7 @@ func (fc *funcContext) assignedObjectName(o types.Object) (name string, found bo // allocated as needed. func (fc *funcContext) objectName(o types.Object) string { if isPkgLevel(o) { - fc.DeclareDCEDep(o) + fc.pkgCtx.DeclareDCEDep(o) if o.Pkg() != fc.pkgCtx.Pkg || (isVarOrConst(o) && o.Exported()) { return fc.pkgVar(o.Pkg()) + "." + o.Name() @@ -562,7 +525,7 @@ func (fc *funcContext) typeName(ty types.Type) string { fc.pkgCtx.anonTypes = append(fc.pkgCtx.anonTypes, anonType) fc.pkgCtx.anonTypeMap.Set(ty, anonType) } - fc.DeclareDCEDep(anonType) + fc.pkgCtx.DeclareDCEDep(anonType) return anonType.Name() } From 525b19b3e7baec05a233cdc79c9bdab5de9cac98 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Fri, 23 Aug 2024 14:30:59 -0600 Subject: [PATCH 09/58] Adding instance tracking to the DCE --- compiler/decls.go | 4 +- compiler/expressions.go | 4 +- compiler/internal/dce/README.md | 600 +++++++++++++++++ compiler/internal/dce/collector.go | 24 +- compiler/internal/dce/dce_test.go | 799 +++++++++++++++++++++-- compiler/internal/dce/filters.go | 340 ++++++++++ compiler/internal/dce/info.go | 91 ++- compiler/internal/dce/selector.go | 6 +- compiler/internal/typeparams/collect.go | 3 +- compiler/internal/typeparams/instance.go | 12 + compiler/statements.go | 3 +- compiler/utils.go | 5 +- go.mod | 1 - go.sum | 2 - 14 files changed, 1766 insertions(+), 128 deletions(-) create mode 100644 compiler/internal/dce/README.md create mode 100644 compiler/internal/dce/filters.go diff --git a/compiler/decls.go b/compiler/decls.go index b6427a697..b55eecdd5 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -320,7 +320,7 @@ func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance) Blocking: fc.pkgCtx.IsBlocking(o), LinkingName: symbol.New(o), } - d.Dce().SetName(o) + d.Dce().SetName(o, inst.TArgs...) if typesutil.IsMethod(o) { recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj() @@ -450,7 +450,7 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er underlying := instanceType.Underlying() d := &Decl{} - d.Dce().SetName(inst.Object) + d.Dce().SetName(inst.Object, inst.TArgs...) fc.pkgCtx.CollectDCEDeps(d, func() { // Code that declares a JS type (i.e. prototype) for each Go type. d.DeclCode = fc.CatchOutput(0, func() { diff --git a/compiler/expressions.go b/compiler/expressions.go index dcf1b7844..72c44b5db 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -591,9 +591,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case types.MethodVal: return fc.formatExpr(`$methodVal(%s, "%s")`, fc.makeReceiver(e), sel.Obj().(*types.Func).Name()) case types.MethodExpr: - if !sel.Obj().Exported() { - fc.pkgCtx.DeclareDCEDep(sel.Obj()) - } + fc.pkgCtx.DeclareDCEDep(sel.Obj(), inst.TArgs...) if _, ok := sel.Recv().Underlying().(*types.Interface); ok { return fc.formatExpr(`$ifaceMethodExpr("%s")`, sel.Obj().(*types.Func).Name()) } diff --git a/compiler/internal/dce/README.md b/compiler/internal/dce/README.md new file mode 100644 index 000000000..4fe2882c0 --- /dev/null +++ b/compiler/internal/dce/README.md @@ -0,0 +1,600 @@ +# Dead-Code Elimination + +Dead-Code Eliminations (DCE) is used to remove code that isn't +reachable from a code entry point. Entry points are code like the main method, +init functions, and variable initializations with side effects. +These entry points are always considered alive. Any dependency of +something alive, is also considered alive. + +Once all dependencies are taken into consideration we have the set of alive +declarations. Anything not considered alive is considered dead and +may be safely eliminated, i.e. not outputted to the JS file(s). + +- [Idea](#idea) + - [Package](#package) + - [Named Types](#named-types) + - [Named Structs](#named-structs) + - [Interfaces](#interfaces) + - [Functions](#functions) + - [Variables](#variables) + - [Generics and Instances](#generics-and-instances) + - [Links](#links) +- [Design](#design) + - [Initially Alive](#initially-alive) + - [Naming](#naming) + - [Name Specifics](#name-specifics) + - [Dependencies](#dependencies) +- [Examples](#examples) + - [Dead Package](#dead-package) + - [Grandmas and Zombies](#grandmas-and-zombies) + - [Side Effects](#side-effects) + - [Instance Duck-typing](#instance-duck-typing) +- [Additional Notes](#additional-notes) + +## Idea + +The following is the logic behind the DCE mechanism. Not all of the following +is used since some conditions are difficult to determine even with a lot of +additional information. To ensure that the JS output is fully functional, +we bias the DCE towards things being alive. We'd rather keep something we +don't need than remove something that is needed. + +### Package + +Package declarations (e.g. `package foo`) might be able to be removed +when only used by dead-code. However, packages may be imported and not used +for various reasons including to invoke some initialization or to implement +a link. So it is difficult to determine. +See [Dead Package](#dead-package) example. + +Currently, we won't remove any packages, but someday the complexity +could be added to check for inits, side effects, links, etc then determine +if any of those are are alive or affect alive things. + +### Named Types + +Named type definitions (e.g. `type Foo int`) depend on +the underlying type for each definition. + +When a named type is alive, all of its exported methods +(e.g. `func (f Foo) Bar() { }`) are also alive, even any unused exported method. +Unused exported methods are still important when duck-typing. +See [Interfaces](#interfaces) for more information. +See [Grandmas and Zombies](#grandmas-and-zombies) for an example of what +can happen when removing an unused exported method. + +Also unused exported methods could be accessed by name via reflect +(e.g. `reflect.ValueOf(&Foo{}).MethodByName("Bar")`). Since the +string name may be provided from outside the code, such as the command line, +it is impossible to determine which exported methods could be accessed this way. +It would be very difficult to determine which types are ever accessed via +reflect so by default we simply assume any can be. + +Methods that are unexported may be considered dead when unused even when +the receiver type is alive. The exception is when an interface in the same +package has the same unexported method in it. +See [Interfaces](#interfaces) for more information. + +#### Named Structs + +A named struct is a named type that has a struct as its underlying type, +e.g. `type Foo struct { }`. A struct type depends on all of the types in +its fields and embedded fields. + +If the struct type is alive then all the types of the fields will also be alive. +Even unexported fields maybe accessed via reflections, so they all must be +alive. Also, the fields are needed for comparisons and serializations +(such as `encoding/binary`). + +### Interfaces + +All the types in the function signatures and embedded interfaces are the +dependents of the interface. + +Interfaces may contain exported and unexported function signatures. +If an interface is alive then all of the functions, even the unexported +functions, are alive. +Since there are many ways to wrap a type with an interface, any alive type that +duck-types to an interface must have all of the matching methods alive. + +Since the exported methods in an alive type will be alive, see +[Named Types](#named-types), the only ones here that need to be considered +are the unexported methods. An interface with unexported methods may only +duck-type to types within the package the interface is defined in. +Therefore, if an interface is alive with unexported methods, then all +alive types within the same package that duck-type to that interface, +will have the matching unexported methods be alive. + +Since doing a full `types.Implements` check between every named types and +interfaces in a package is difficult, we simplify this requirement to be +any unexported method in an alive named type that matches an unexported +method in an alive interface is alive even if the named type doesn't duck-type +to the interface. This means that in some rare cases, some unexported +methods on named structs that could have been eliminated will not be. +For example, given `type Foo struct{}; func(f Foo) X(); func (f Foo) y()` the +`Foo.y()` method may be alive if `types Bar interface { Z(); y() }` is alive +even though the `X()` and `Z()` means that `Foo` doesn't implement `Bar` +and therefore `Foo.y()` can not be called via a `Bar.y()`. + +We will try to reduce the false positives in alive unexported methods by using +the parameter and result types of the methods. Meaning that + `y()`, `y(int)`, `y() int`, etc won't match just because they are named `y`. + +### Functions + +Functions with or without a receiver are dependent on the types used by the +parameters, results, and type uses inside the body of the function. +They are also dependent on any function invoked or used, and +any package level variable that is used. + +Unused functions without a receiver, that are exported or not, may be +considered dead since they aren't used in duck-typing and cannot be accessed +by name via reflections. + +### Variables + +Variables (or constants) depend on their type and anything used during +initialization. + +The exported or unexported variables are dead unless they are used by something +else that is alive or if the initialization has side effects. + +If the initialization has side effects the variable will be alive even +if unused. The side effect may be simply setting another variable's value +that is also unused, however it would be difficult to determine if the +side effects are used or not. +See [Side Effects](#side-effects) example. + +### Generics and Instances + +For functions and types with generics, the definitions are split into +unique instances. For example, `type StringKeys[T any] map[string]T` +could be used in code as `StringKeys[int]` and `StringKeys[*Cat]`. +We don't need all possible instances, only the ones which are realized +in code. Each instance depends on the realized parameter types (instance types). +In the example the instance types are `int` and `*Cat`. + +The instance of the generic type also defines the code with the specific +instance types (e.g. `map[string]int` and `map[string]*Cat`). When an +instance is depended on by alive code, only that instance is alive, not the +entire generic type. This means if `StringKey[*Cat]` is only used from dead +code, it is also dead and can be safely eliminated. + +The named generic types may have methods that are also copied for an instance +with the parameter types replaced by the instance types. For example, +`func (sk StringKeys[T]) values() []T { ... }` becomes +`func (sk StringKeys[int]) values() []int { ... }` when the instance type +is `int`. This method in the instance now duck-types to +`interface { values() []int }` and therefore must follow the rules for +unexported methods. +See [Instance Duck-typing](#instance-duck-typing) example for more information. + +Functions and named types may be generic, but methods and unnamed types +may not be. This makes somethings simpler. A method with a receiver is used, +only the receiver's instance types are needed. The generic type or function +may not be needed since only the instances are written out. + +This also means that inside of a generic function or named type there is only +one type parameter list being used. Even generic types used inside of the +generic function must be specified in terms of the type parameter for the +generic and doesn't contribute any type parameters of it's own. +For example, inside of `func Foo[K comparable, V any]() { ... }` every +usage of a generic type must specify a concrete type (`int`, `*Cat`, +`Bar[Bar[bool]]`) or use the parameter types `K` and `V`. This is simpler +than languages that allow a method of an object to have it's own type +parameters, e.g. `class X { void Y() { ... } ... }`. + +However, generics mean that the same method, receiver, type, etc names +will be used with different parameters types caused by different instance +types. The instance types are the type arguments being passed into those +parameter types for a specific instance. +When an interface is alive, the signatures for unexported methods +need to be instantiated with type arguments so that we know which instances +the interface is duck-typing to. + +### Links + +Links use compiler directives +([`//go:linkname`](https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives)) +to alias a `var` or `func` with another. +For example some code may have `func bar_foo()` as a function stub that is +linked with `foo() { ... }` as a function with a body, i.e. the target of the +link. The links are single directional but allow multiple stubs to link to the +same target. + +When a link is made, the dependencies for the linked code come from +the target. If the target is used by something alive then it is alive. +If a stub linked to a target is used by something alive then that stub and +the target are both alive. + +Since links cross package boundaries in ways that may violate encapsulation +and the dependency tree, it may be difficult to determine if a link is alive +or not. Therefore, currently all links are considered alive. + +## Design + +The design is created taking all the parts of the above idea together and +simplifying the justifications down to a simple set of rules. + +### Initially alive + +- The `main` method in the `main` package +- The `init` in every included file +- Any variable initialization that has a side effect +- Any linked function or variable +- Anything not named + +### Naming + +The following specifies what declarations should be named and how +the names should look. These names are later used to match (via string +comparisons) dependencies with declarations that should be set as alive. +Since the names are used to filter out alive code from all the code +these names may also be referred to as filters. + +Some names will have multiple name parts; an object name and method name. +This is kind of like a first name and last name when a first name alone isn't +specific enough. This helps with matching multiple dependency requirements +for a declaration, i.e. both name parts must be alive before the declaration +is considered alive. + +Currently, only unexported method declarations will have a method +name to support duck-typing with unexported signatures on interfaces. +If the unexported method is depended on, then both names will be in +the dependencies. If the receiver is alive and an alive interface has the +matching unexported signature, then both names will be depended on thus making +the unexported method alive. Since the unexported method is only visible in +the package in which it is defined, the package path is included in the +method name. + +| Declaration | exported | unexported | non-generic | generic | object name | method name | +|:------------|:--------:|:----------:|:-----------:|:-------:|:------------|:------------| +| variables | █ | █ | █ | n/a | `.` | | +| functions | █ | █ | █ | | `.` | | +| functions | █ | █ | | █ | `.[]` | | +| named type | █ | █ | █ | | `.` | | +| named type | █ | █ | | █ | `.[]` | | +| method | █ | | █ | | `.` | | +| method | █ | | | █ | `.[]` | | +| method | | █ | █ | | `.` | `.()()` | +| method | | █ | | █ | `.[]` | `.()()` | + +#### Name Specifics + +The following are specifics about the different types of names that show +up in the above table. This isn't the only way to represent this information. +These names can get long but don't have to. The goal is to make the names +as unique as possible whilst still ensuring that signatures in +interfaces will still match the correct methods. The less unique +the more false positives for alive will occur meaning more dead code is +kept alive. However, too unique could cause needed alive code to not match +and be eliminated causing the application to not run. + +`.`, `.`, `.` +and `.` all have the same form. They are +the package path, if there is one, followed by a `.` and the object name +or receiver name. For example [`rand.Shuffle`](https://pkg.go.dev/math/rand@go1.23.1#Shuffle) +will be named `math/rand.Shuffle`. The builtin [`error`](https://pkg.go.dev/builtin@go1.23.1#error) +will be named `error` without a package path. + +`.[]`, `.[]`, +and `.[]` are the same as above +except with comma separated type arguments in square brackets. +The type arguments are either the instance types, or type parameters +since the instance type could be a match for the type parameter on the +generic. For example `type Foo[T any] struct{}; type Bar[B any] { f Foo[B] }` +has `Foo[B]` used in `Bar` that is identical to `Foo[T]` even though +technically `Foo[B]` is an instance of `Foo[T]` with `B` as the type argument. + +Command compiles, i.e. compiles with a `main` entry point, and test builds +should not have any instance types that aren't resolved to concrete types, +however to handle partial compiles of packages, instance types may still +be a type parameter, including unions of approximate constraints, +i.e. `~int|~string`. + +Therefore, type arguments need to be reduced to only types. This means +something like [`maps.Keys`](https://pkg.go.dev/maps@go1.23.1#Keys), i.e. +`func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K]`, +will be named `maps.Keys[~map[comparable]any, comparable, any]` as a generic. +If the instances for `Map` are `map[string]int` and `map[int][]*cats.Cat`, +then respectively the names would be `maps.Keys[map[string]int, string, int]` +and `maps.Keys[map[int][]*cats.Cat, int, []*cats.Cat]`. If this function is used +in `func Foo[T ~string|~int](data map[string]T) { ... maps.Keys(data) ... }` +then the instance of `maps.Keys` that `Foo` depends on would be named +`maps.Keys[map[string]~int|~string, string, ~int|~string]`. + +For the method name of unexposed methods, +`.()()`, the prefix, +`.`, is in the same format as `.`. +The rest contains the signature, `()()`. +The signature is defined with only the types since +`(v, u int)(ok bool, err error)` should match `(x, y int)(bool, error)`. +To match both will have to be `(int, int)(bool, error)`. +Also the parameter types should include the veridic indicator, +e.g. `sum(...int)int`, since that affects how the signature is matched. +If there are no results then the results part is left off. Otherwise, +the result types only need parenthesis if there are more than one result, +e.g. `(int, int)`, `(int, int)bool`, and `(int, int)(bool, error)`. + +In either the object name or method name, if there is a recursive +type parameter, e.g. `func Foo[T Bar[T]]()` the second usage of the +type parameter will have it's type parameters as `...` to prevent an +infinite loop whilst also indicating which object in the type parameter +is recursive, e.g. `Foo[Bar[Bar[...]]]`. + +### Dependencies + +The dependencies are initialized via two paths. + +The first is dependencies that are specified in an expression. +For example a function that invokes another function will be dependent on +that invoked function. When a dependency is added it will be added as one +or more names to the declaration that depends on it. It follows the +[naming rules](#naming) so that the dependencies will match correctly. + +The second is structural dependencies that are specified automatically while +the declaration is being named. When an interface is named, it will +automatically add all unexported signatures as dependencies via +`.()()`. + +Currently we don't filter unused packages so there is no need to automatically +add dependencies on the packages themselves. This is also why the package +declarations aren't named and therefore are always alive. + +## Examples + +### Dead Package + +In this example, a point package defines a `Point` object. +The point package may be used by several repos as shared code so can not +have code manually removed from it to reduce its dependencies for specific +applications. + +For the current example, the `Distance` method is never used and therefore +dead. The `Distance` method is the only method dependent on the math package. +It might be safe to make the whole math package dead too and eliminate it in +this case, however, it is possible that some packages aren't used on purpose +and their reason for being included is to invoke the initialization functions +within the package. If a package has any inits or any variable definitions +with side effects, then the package can not be safely removed. + +```go +package point + +import "math" + +type Point struct { + X float64 + Y float64 +} + +func (p Point) Sub(other Point) Point { + p.X -= other.X + p.Y -= other.Y + return p +} + +func (p Point) ToQuadrant1() Point { + if p.X < 0.0 { + p.X = -p.X + } + if p.Y < 0.0 { + p.Y = -p.Y + } + return p +} + +func (p Point) Manhattan(other Point) float64 { + a := p.Sub(other).ToQuadrant1() + return a.X + a.Y +} + +func (p Point) Distance(other Point) float64 { + d := p.Sub(other) + return math.Sqrt(d.X*d.X + d.Y*d.Y) +} +``` + +```go +package main + +import "point" + +func main() { + a := point.Point{X: 10.2, Y: 45.3} + b := point.Point{X: -23.0, Y: 7.7} + println(`Manhatten a to b:`, a.Manhattan(b)) +} +``` + +### Grandmas and Zombies + +In this example, the following code sorts grandmas and zombies by if they are +`Dangerous`. The method `EatBrains` is never used. If we remove `EatBrains` +from `Zombie` then both the grandmas and zombies are moved to the safe +bunker. If we remove `EatBrains` from `Dangerous` then both grandmas and +zombies will be moved to the air lock because `Dangerous` will duck-type +to all `Person` instances. Unused exported methods and signatures must be +considered alive if the type is alive. + +```go +package main + +import "fmt" + +type Person interface { + MoveTo(loc string) +} + +type Dangerous interface { + Person + EatBrains() +} + +type Grandma struct{} + +func (g Grandma) MoveTo(loc string) { + fmt.Println(`grandma was moved to`, loc) +} + +type Zombie struct{} + +func (z Zombie) MoveTo(loc string) { + fmt.Println(`zombie was moved to`, loc) +} + +func (z Zombie) EatBrains() {} + +func main() { + people := []Person{Grandma{}, Zombie{}, Grandma{}, Zombie{}} + for _, person := range people { + if _, ok := person.(Dangerous); ok { + person.MoveTo(`air lock`) + } else { + person.MoveTo(`safe bunker`) + } + } +} +``` + +### Side Effects + +In this example unused variables are being initialized with expressions +that have side effects. The `max` value is 8 by the time `main` is called +because each initialization calls `count()` that increments `max`. +The expression doesn't have to have a function call and can be any combination +of operations. + +An initialization may have a side effect even if it doesn't set a value. For +example, simply printing a message to the console is a side effect that +can not be removed even if it is part of an unused initializer. + +```go +package main + +import "fmt" + +func count() int { + max++ + return max +} + +var ( + max = 0 + _ = count() // a + b, c = count(), count() + x = []int{count(), count(), count()}[0] + y, z = func() (int, int) { return count(), count() }() +) + +func main() { + fmt.Println(`max count`, max) // Outputs: max count 8 +} +``` + +### Instance Duck-typing + +In this example the type `StringKeys[T any]` is a map that stores +any kind of value with string keys. There is an interface `IntProvider` +that `StringKeys` will duck-type to iff the instance type is `int`, +i.e. `StringKeys[int]`. This exemplifies how the instance types used +in the type arguments affect the overall signature such that in some +cases a generic object may match an interface and in others it may not. + +Also notice that the structure was typed with `T` as the parameter type's +name whereas the methods use `S`. This shows that the name of the type +doesn't matter in the instancing. Therefore, outputting a methods name +(assuming it is unexported) should use the instance type not the parameter +name, e.g. `value() []int` or `value() []any` instead of `value() []S` or +`value() []T`. + +```go +package main + +import ( + "fmt" + "sort" +) + +type StringKeys[T any] map[string]T + +func (sk StringKeys[S]) Keys() []string { + keys := make([]string, 0, len(sk)) + for key := range sk { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func (sk StringKeys[S]) Values() []S { + values := make([]S, len(sk)) + for i, key := range sk.Keys() { + values[i] = sk[key] + } + return values +} + +type IntProvider interface { + Values() []int +} + +func Sum(data IntProvider) int { + sum := 0 + for _, value := range data.Values() { + sum += value + } + return sum +} + +func main() { + sInt := StringKeys[int]{ + `one`: 1, + `two`: 2, + `three`: 3, + `four`: 4, + } + fmt.Println(sInt.Keys()) // Outputs: [four one three two] + fmt.Println(sInt.Values()) // Outputs: [4 1 3 2] + fmt.Println(Sum(sInt)) // Outputs: 10 + + sFp := StringKeys[float64]{ + `one`: 1.1, + `two`: 2.2, + `three`: 3.3, + `four`: 4.4, + } + fmt.Println(sFp.Keys()) // Outputs: [four one three two] + fmt.Println(sFp.Values()) // [4.4 1.1 3.3 2.2] + //fmt.Println(Sum(sFp)) // Fails with "StringKeys[float64] does not implement IntProvider" +} +``` + +## Additional Notes + +This DCE is different from those found in +Muchnick, Steven S.. “Advanced Compiler Design and Implementation.” (1997), +Chapter 18 Control-Flow and Low-Level Optimization, +Section 10 Dead-Code Elimination. And different from related DCE designs +such as Knoop, Rüthing, and Steffen. "Partial dead code elimination." (1994), +SIGPLAN Not. 29, 6, 147–158. +See [DCE wiki](https://en.wikipedia.org/wiki/Dead-code_elimination) +for more information. + +Those discuss DCE at the block code level where the higher level +constructs such as functions and objects have been reduced to a graphs of +blocks with variables, procedures, and routines. Since we want to keep the +higher level constructs during transpilation, we simply are reducing +the higher level constructs not being used. + +Any variable internal to the body of a function or method that is unused or +only used for computing new values for itself, are left as is. +The Go compiler and linters have requirements that attempt to prevent this +kind of dead-code in a function body (so long as an underscore isn't used to quite +usage warnings) and prevent unreachable code. Therefore, we aren't going to +worry about trying to DCE inside of function bodies or in variable initializers. + +GopherJS does not implicitly perform JS Tree Shaking Algorithms, as discussed in +[How Modern Javascript eliminate dead code](https://blog.stackademic.com/how-modern-javascript-eliminates-dead-code-tree-shaking-algorithm-d7861e48df40) +(2023) at this time and provides no guarantees about the effectiveness +of running such an algorithm on the resulting JS. diff --git a/compiler/internal/dce/collector.go b/compiler/internal/dce/collector.go index 7d251029b..fea52468d 100644 --- a/compiler/internal/dce/collector.go +++ b/compiler/internal/dce/collector.go @@ -14,7 +14,7 @@ type Decl interface { // Collector is a tool to collect dependencies for a declaration // that'll be used in dead-code elimination (DCE). type Collector struct { - dependencies map[types.Object]struct{} + dce *Info } // CollectDCEDeps captures a list of Go objects (types, functions, etc.) @@ -22,25 +22,25 @@ type Collector struct { // as dependencies of the given dead-code elimination info. // // Only one CollectDCEDeps call can be active at a time. -// This will overwrite any previous dependencies collected for the given DCE. func (c *Collector) CollectDCEDeps(decl Decl, f func()) { - if c.dependencies != nil { + if c.dce != nil { panic(errors.New(`called CollectDCEDeps inside another CollectDCEDeps call`)) } - c.dependencies = make(map[types.Object]struct{}) - defer func() { c.dependencies = nil }() + c.dce = decl.Dce() + defer func() { c.dce = nil }() f() - - decl.Dce().setDeps(c.dependencies) } // DeclareDCEDep records that the code that is currently being transpiled -// depends on a given Go object. -func (c *Collector) DeclareDCEDep(o types.Object) { - if c.dependencies == nil { - return // Dependencies are not being collected. +// depends on a given Go object with optional type arguments. +// +// The given optional type arguments are used to when the object is a +// function with type parameters or anytime the object doesn't carry them. +// If not given, this attempts to get the type arguments from the object. +func (c *Collector) DeclareDCEDep(o types.Object, tArgs ...types.Type) { + if c.dce != nil { + c.dce.addDep(o, tArgs) } - c.dependencies[o] = struct{}{} } diff --git a/compiler/internal/dce/dce_test.go b/compiler/internal/dce/dce_test.go index c46a7f03c..226e90c7e 100644 --- a/compiler/internal/dce/dce_test.go +++ b/compiler/internal/dce/dce_test.go @@ -10,6 +10,8 @@ import ( "regexp" "sort" "testing" + + "github.com/gopherjs/gopherjs/compiler/typesutil" ) func Test_Collector_CalledOnce(t *testing.T) { @@ -65,20 +67,20 @@ func Test_Collector_Collecting(t *testing.T) { depCount(t, decl1, 2) depCount(t, decl2, 3) - // The second collection overwrites the first collection. + // The second collection adds to existing dependencies. c.CollectDCEDeps(decl2, func() { + c.DeclareDCEDep(obj4) c.DeclareDCEDep(obj5) }) depCount(t, decl1, 2) - depCount(t, decl2, 1) + depCount(t, decl2, 4) } func Test_Info_SetNameAndDep(t *testing.T) { tests := []struct { - name string - obj types.Object - want Info // expected Info after SetName - wantDep string // expected dep after addDep + name string + obj types.Object + want Info // expected Info after SetName }{ { name: `package`, @@ -86,32 +88,26 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim import Sarah "fmt"`), want: Info{ - importPath: `jim`, - objectFilter: `Sarah`, + objectFilter: `jim.Sarah`, }, - wantDep: `jim.Sarah`, }, { - name: `exposed var`, + name: `exported var`, obj: parseObject(t, `Toby`, `package jim var Toby float64`), want: Info{ - importPath: `jim`, - objectFilter: `Toby`, + objectFilter: `jim.Toby`, }, - wantDep: `jim.Toby`, }, { - name: `exposed const`, + name: `exported const`, obj: parseObject(t, `Ludo`, `package jim const Ludo int = 42`), want: Info{ - importPath: `jim`, - objectFilter: `Ludo`, + objectFilter: `jim.Ludo`, }, - wantDep: `jim.Ludo`, }, { name: `label`, @@ -126,91 +122,487 @@ func Test_Info_SetNameAndDep(t *testing.T) { } }`), want: Info{ - importPath: `jim`, - objectFilter: `Gobo`, + objectFilter: `jim.Gobo`, }, - wantDep: `jim.Gobo`, }, { - name: `exposed specific type`, + name: `exported specific type`, obj: parseObject(t, `Jen`, `package jim type Jen struct{}`), want: Info{ - importPath: `jim`, - objectFilter: `Jen`, + objectFilter: `jim.Jen`, }, - wantDep: `jim.Jen`, }, { - name: `exposed generic type`, + name: `exported generic type`, obj: parseObject(t, `Henson`, `package jim type Henson[T comparable] struct{}`), want: Info{ - importPath: `jim`, - objectFilter: `Henson`, + objectFilter: `jim.Henson[comparable]`, }, - wantDep: `jim.Henson`, }, { - name: `exposed specific function`, + name: `exported specific function`, obj: parseObject(t, `Jareth`, `package jim func Jareth() {}`), want: Info{ - importPath: `jim`, - objectFilter: `Jareth`, + objectFilter: `jim.Jareth`, }, - wantDep: `jim.Jareth`, }, { - name: `exposed generic function`, + name: `exported generic function`, obj: parseObject(t, `Didymus`, `package jim func Didymus[T comparable]() {}`), want: Info{ - importPath: `jim`, - objectFilter: `Didymus`, + objectFilter: `jim.Didymus[comparable]`, }, - wantDep: `jim.Didymus`, }, { - name: `exposed specific method`, + name: `exported specific method`, obj: parseObject(t, `Kira`, `package jim type Fizzgig string func (f Fizzgig) Kira() {}`), want: Info{ - importPath: `jim`, - objectFilter: `Fizzgig`, + objectFilter: `jim.Fizzgig`, }, - wantDep: `jim.Kira~`, }, { - name: `unexposed specific method`, + name: `unexported specific method without parameters or results`, obj: parseObject(t, `frank`, `package jim type Aughra int func (a Aughra) frank() {}`), want: Info{ - importPath: `jim`, - objectFilter: `Aughra`, - methodFilter: `frank~`, + objectFilter: `jim.Aughra`, + methodFilter: `jim.frank()`, + }, + }, + { + name: `unexported specific method with parameters and results`, + obj: parseObject(t, `frank`, + `package jim + type Aughra int + func (a Aughra) frank(other Aughra) (bool, error) { + return a == other, nil + }`), + want: Info{ + objectFilter: `jim.Aughra`, + methodFilter: `jim.frank(jim.Aughra)(bool, error)`, + }, + }, + { + name: `unexported specific method with variadic parameter`, + obj: parseObject(t, `frank`, + `package jim + type Aughra int + func (a Aughra) frank(others ...Aughra) int { + return len(others) + 1 + }`), + want: Info{ + objectFilter: `jim.Aughra`, + methodFilter: `jim.frank(...jim.Aughra) int`, }, - wantDep: `jim.frank~`, }, { - name: `specific method on unexposed type`, + name: `unexported generic method with type parameters and instance argument`, + obj: parseObject(t, `frank`, + `package jim + type Aughra[T ~float64] struct { + value T + } + func (a *Aughra[T]) frank(other *Aughra[float64]) bool { + return float64(a.value) == other.value + }`), + want: Info{ + objectFilter: `jim.Aughra[~float64]`, + methodFilter: `jim.frank(*jim.Aughra[float64]) bool`, + }, + }, + { + name: `unexported generic method with type parameters and generic argument`, + obj: parseObject(t, `frank`, + `package jim + type Aughra[T ~float64] struct { + value T + } + func (a *Aughra[T]) frank(other *Aughra[T]) bool { + return a.value == other.value + }`), + want: Info{ + objectFilter: `jim.Aughra[~float64]`, + methodFilter: `jim.frank(*jim.Aughra[~float64]) bool`, + }, + }, + { + name: `specific method on unexported type`, obj: parseObject(t, `Red`, `package jim type wembley struct{} func (w wembley) Red() {}`), want: Info{ - importPath: `jim`, - objectFilter: `wembley`, + objectFilter: `jim.wembley`, + }, + }, + { + name: `interface with unexported methods setting dependencies`, + obj: parseObject(t, `Hoggle`, + `package jim + type Hoggle interface{ + cowardly() bool + loyalTo(goblin string) bool + makePrinceOfTheBogOfEternalStench() error + }`), + want: Info{ + objectFilter: `jim.Hoggle`, + // The automatically defined dependencies for unexported methods + // in the interface that match with the methodFilter of unexported methods. + deps: map[string]struct{}{ + `jim.cowardly() bool`: {}, + `jim.loyalTo(string) bool`: {}, + `jim.makePrinceOfTheBogOfEternalStench() error`: {}, + }, + }, + }, + { + name: `unexported method resulting in an interface with exported methods`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() interface{ + WakkaWakka(joke string)(landed bool) + Firth()(string, error) + }`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() interface{ Firth()(string, error); WakkaWakka(string) bool }`, + }, + }, + { + name: `unexported method resulting in an interface with unexported methods`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() interface{ + wakkaWakka(joke string)(landed bool) + firth()(string, error) + }`), + want: Info{ + objectFilter: `jim.Fozzie`, + // The package path, i.e. `jim.`, is used on unexported methods + // to ensure the filter will not match another package's method. + methodFilter: `jim.bear() interface{ jim.firth()(string, error); jim.wakkaWakka(string) bool }`, + }, + }, + { + name: `unexported method resulting in an empty interface `, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() interface{}`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() any`, + }, + }, + { + name: `unexported method resulting in a function`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() func(joke string)(landed bool)`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() func(string) bool`, + }, + }, + { + name: `unexported method resulting in a struct`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() struct{ + Joke string + WakkaWakka bool + }`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() struct{ Joke string; WakkaWakka bool }`, + }, + }, + { + name: `unexported method resulting in a struct with type parameter`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie[T ~string|~int] struct{} + func (f *Fozzie[T]) bear() struct{ + Joke T + wakkaWakka bool + }`), + want: Info{ + objectFilter: `jim.Fozzie[~int|~string]`, + // The `Joke ~int|~string` part will likely not match other methods + // such as methods with `Joke string` or `Joke int`, however the + // interface should be defined for the instantiations of this type + // and those should have the correct field type for `Joke`. + methodFilter: `jim.bear() struct{ Joke ~int|~string; jim.wakkaWakka bool }`, + }, + }, + { + name: `unexported method resulting in an empty struct`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() struct{}`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() struct{}`, + }, + }, + { + name: `unexported method resulting in a slice`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear()(jokes []string)`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() []string`, + }, + }, + { + name: `unexported method resulting in an array`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear()(jokes [2]string)`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() [2]string`, + }, + }, + { + name: `unexported method resulting in a map`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear()(jokes map[string]bool)`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() map[string]bool`, + }, + }, + { + name: `unexported method resulting in a channel`, + obj: parseObject(t, `bear`, + `package jim + type Fozzie struct{} + func (f *Fozzie) bear() chan string`), + want: Info{ + objectFilter: `jim.Fozzie`, + methodFilter: `jim.bear() chan string`, + }, + }, + { + name: `unexported method resulting in a complex compound named type`, + obj: parseObject(t, `packRat`, + `package jim + type Gonzo[T any] struct{ + v T + } + func (g Gonzo[T]) Get() T { return g.v } + type Rizzo struct{} + func (r Rizzo) packRat(v int) Gonzo[Gonzo[Gonzo[int]]] { + return Gonzo[Gonzo[Gonzo[int]]]{v: Gonzo[Gonzo[int]]{v: Gonzo[int]{v: v}}} + } + var _ int = Rizzo{}.packRat(42).Get().Get().Get()`), + want: Info{ + objectFilter: `jim.Rizzo`, + methodFilter: `jim.packRat(int) jim.Gonzo[jim.Gonzo[jim.Gonzo[int]]]`, + }, + }, + { + name: `unexported method resulting in an instance with same type parameter`, + obj: parseObject(t, `sidekick`, + `package jim + type Beaker[T any] struct{} + type Honeydew[S any] struct{} + func (hd Honeydew[S]) sidekick() Beaker[S] { + return Beaker[S]{} + }`), + want: Info{ + objectFilter: `jim.Honeydew[any]`, + methodFilter: `jim.sidekick() jim.Beaker[any]`, + }, + }, + { + name: `unexported method in interface from named embedded interface`, + obj: parseObject(t, `Waldorf`, + `package jim + type Statler interface{ + boo() + } + type Waldorf interface{ + Statler + }`), + want: Info{ + objectFilter: `jim.Waldorf`, + deps: map[string]struct{}{ + `jim.boo()`: {}, + }, + }, + }, + { + name: `unexported method in interface from unnamed embedded interface`, + obj: parseObject(t, `Waldorf`, + `package jim + type Waldorf interface{ + interface{ + boo() + } + }`), + want: Info{ + objectFilter: `jim.Waldorf`, + deps: map[string]struct{}{ + `jim.boo()`: {}, + }, + }, + }, + { + name: `unexported method on instance of generic interface`, + obj: parseObject(t, `Waldorf`, + `package jim + type Statler[T any] interface{ + boo() T + } + type Waldorf Statler[string]`), + want: Info{ + objectFilter: `jim.Waldorf`, + deps: map[string]struct{}{ + `jim.boo() string`: {}, + }, + }, + }, + { + name: `struct with self referencing type parameter constraints`, + obj: parseObject(t, `Keys`, + `package jim + func Keys[K comparable, V any, M ~map[K]V](m M) []K { + keys := make([]K, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + return keys + }`), + want: Info{ + objectFilter: `jim.Keys[comparable, any, ~map[comparable]any]`, + }, + }, + { + name: `struct with self referencing type parameter constraints`, + obj: parseObject(t, `ElectricMayhem`, + `package jim + type ElectricMayhem[K comparable, V any, M ~map[K]V] interface { + keys() []K + values() []V + asMap() M + }`), + want: Info{ + objectFilter: `jim.ElectricMayhem[comparable, any, ~map[comparable]any]`, + deps: map[string]struct{}{ + `jim.keys() []comparable`: {}, + `jim.values() []any`: {}, + `jim.asMap() ~map[comparable]any`: {}, + }, + }, + }, + { + name: `function with recursive referencing type parameter constraints`, + obj: parseObject(t, `doWork`, + `package jim + type Doozer[T any] interface { + comparable + Work() T + } + + func doWork[T Doozer[T]](a T) T { + return a.Work() + }`), + want: Info{ + objectFilter: `jim.doWork[jim.Doozer[jim.Doozer[...]]]`, + }, + }, + { + name: `function with recursive referencing multiple type parameter constraints`, + obj: parseObject(t, `doWork`, + `package jim + type Doozer[T, U any] interface { + Work() T + Play() U + } + + func doWork[T Doozer[T, U], U any](a T) T { + return a.Work() + }`), + want: Info{ + objectFilter: `jim.doWork[jim.Doozer[jim.Doozer[...], any], any]`, + }, + }, + { + name: `function with multiple recursive referencing multiple type parameter constraints`, + obj: parseObject(t, `doWork`, + `package jim + type Doozer[T, U any] interface { + Work() T + Play() U + } + + func doWork[T Doozer[T, U], U Doozer[T, U]](a T) T { + return a.Work() + }`), + want: Info{ + objectFilter: `jim.doWork[jim.Doozer[jim.Doozer[...], jim.Doozer[...]], jim.Doozer[jim.Doozer[...], jim.Doozer[...]]]`, + }, + }, + { + name: `function with multiple recursive referencing type parameter constraints`, + obj: parseObject(t, `doWork`, + `package jim + type Doozer[T any] interface { + Work() T + } + + type Fraggle[U any] interface { + Play() U + } + + func doWork[T Doozer[T], U Fraggle[U]](a T) T { + return a.Work() + }`), + want: Info{ + objectFilter: `jim.doWork[jim.Doozer[jim.Doozer[...]], jim.Fraggle[jim.Fraggle[...]]]`, + }, + }, + { + name: `function with osculating recursive referencing type parameter constraints`, + obj: parseObject(t, `doWork`, + `package jim + type Doozer[T any] interface { + Work() T + } + + type Fraggle[U any] interface { + Play() U + } + + func doWork[T Doozer[U], U Fraggle[T]]() {}`), + want: Info{ + objectFilter: `jim.doWork[jim.Doozer[jim.Fraggle[jim.Doozer[...]]], jim.Fraggle[jim.Doozer[jim.Fraggle[...]]]]`, }, - wantDep: `jim.Red~`, }, } @@ -219,14 +611,14 @@ func Test_Info_SetNameAndDep(t *testing.T) { t.Run(tt.name, func(t *testing.T) { d := &testDecl{} equal(t, d.Dce().unnamed(), true) - equal(t, d.Dce().String(), `[unnamed] . -> []`) + equal(t, d.Dce().String(), `[unnamed] -> []`) t.Log(`object:`, types.ObjectString(tt.obj, nil)) d.Dce().SetName(tt.obj) equal(t, d.Dce().unnamed(), tt.want.unnamed()) - equal(t, d.Dce().importPath, tt.want.importPath) equal(t, d.Dce().objectFilter, tt.want.objectFilter) equal(t, d.Dce().methodFilter, tt.want.methodFilter) + equalSlices(t, d.Dce().getDeps(), tt.want.getDeps()) equal(t, d.Dce().String(), tt.want.String()) }) } @@ -238,11 +630,20 @@ func Test_Info_SetNameAndDep(t *testing.T) { d := &testDecl{} t.Log(`object:`, types.ObjectString(tt.obj, nil)) - d.Dce().setDeps(map[types.Object]struct{}{ - tt.obj: {}, + wantDeps := []string{} + if len(tt.want.objectFilter) > 0 { + wantDeps = append(wantDeps, tt.want.objectFilter) + } + if len(tt.want.methodFilter) > 0 { + wantDeps = append(wantDeps, tt.want.methodFilter) + } + sort.Strings(wantDeps) + + c := Collector{} + c.CollectDCEDeps(d, func() { + c.DeclareDCEDep(tt.obj) }) - equal(t, len(d.Dce().deps), 1) - equal(t, d.Dce().deps[0], tt.wantDep) + equalSlices(t, d.Dce().getDeps(), wantDeps) }) } }) @@ -262,6 +663,240 @@ func Test_Info_SetNameOnlyOnce(t *testing.T) { errorMatches(t, err, `^may only set the name once for path/to/mogwai\.Gizmo .*$`) } +func Test_Info_UsesDeps(t *testing.T) { + tests := []struct { + name string + id string // identifier to check for usage and instance + line int // line number to find the identifier on + src string + wantDeps []string + }{ + { + name: `usage of specific struct`, + id: `Sinclair`, + line: 5, + src: `package epsilon3 + type Sinclair struct{} + func (s Sinclair) command() { } + func main() { + Sinclair{}.command() //<-- line 5 + }`, + wantDeps: []string{`epsilon3.Sinclair`}, + }, + { + name: `usage of generic struct`, + id: `Sheridan`, + line: 5, + src: `package epsilon3 + type Sheridan[T comparable] struct{} + func (s Sheridan[T]) command() { } + func main() { + Sheridan[string]{}.command() //<-- line 5 + }`, + wantDeps: []string{`epsilon3.Sheridan[string]`}, + }, + { + name: `usage of unexported method of generic struct`, + id: `command`, + line: 5, + src: `package epsilon3 + type Sheridan[T comparable] struct{} + func (s Sheridan[T]) command() { } + func main() { + Sheridan[string]{}.command() //<-- line 5 + }`, + // unexported methods need the method filter for matching with + // unexported methods on interfaces. + wantDeps: []string{ + `epsilon3.Sheridan[string]`, + `epsilon3.command()`, + }, + }, + { + name: `usage of unexported method of generic struct pointer`, + id: `command`, + line: 5, + src: `package epsilon3 + type Sheridan[T comparable] struct{} + func (s *Sheridan[T]) command() { } + func main() { + (&Sheridan[string]{}).command() //<-- line 5 + }`, + // unexported methods need the method filter for matching with + // unexported methods on interfaces. + wantDeps: []string{ + `epsilon3.Sheridan[string]`, + `epsilon3.command()`, + }, + }, + { + name: `invocation of function with implicit type arguments`, + id: `Move`, + line: 5, + src: `package epsilon3 + type Ivanova[T any] struct{} + func Move[T ~string|~int](i Ivanova[T]) { } + func main() { + Move(Ivanova[string]{}) //<-- line 5 + }`, + wantDeps: []string{`epsilon3.Move[string]`}, + }, + { + name: `exported method on a complex generic type`, + id: `Get`, + line: 6, + src: `package epsilon3 + type Garibaldi[T any] struct{ v T } + func (g Garibaldi[T]) Get() T { return g.v } + func main() { + michael := Garibaldi[Garibaldi[Garibaldi[int]]]{v: Garibaldi[Garibaldi[int]]{v: Garibaldi[int]{v: 42}}} + _ = michael.Get() // <-- line 6 + }`, + wantDeps: []string{`epsilon3.Garibaldi[epsilon3.Garibaldi[epsilon3.Garibaldi[int]]]`}, + }, + { + name: `unexported method on a complex generic type`, + id: `get`, + line: 6, + src: `package epsilon3 + type Garibaldi[T any] struct{ v T } + func (g Garibaldi[T]) get() T { return g.v } + func main() { + michael := Garibaldi[Garibaldi[Garibaldi[int]]]{v: Garibaldi[Garibaldi[int]]{v: Garibaldi[int]{v: 42}}} + _ = michael.get() // <-- line 6 + }`, + wantDeps: []string{ + `epsilon3.Garibaldi[epsilon3.Garibaldi[epsilon3.Garibaldi[int]]]`, + `epsilon3.get() epsilon3.Garibaldi[epsilon3.Garibaldi[int]]`, + }, + }, + { + name: `invoke of method with an unnamed interface receiver`, + id: `heal`, + line: 8, + src: `package epsilon3 + type Franklin struct{} + func (g Franklin) heal() {} + func main() { + var stephen interface{ + heal() + } = Franklin{} + stephen.heal() // <-- line 8 + }`, + wantDeps: []string{ + `epsilon3.heal()`, + }, + }, + { + name: `invoke a method with a generic return type via instance`, + // Based on go/1.19.13/x64/test/dictionaryCapture-noinline.go + id: `lennier`, + line: 6, + src: `package epsilon3 + type delenn[T any] struct { a T } + func (d delenn[T]) lennier() T { return d.a } + func cocoon() int { + x := delenn[int]{a: 7} + f := delenn[int].lennier // <-- line 6 + return f(x) + }`, + wantDeps: []string{ + `epsilon3.delenn[int]`, + `epsilon3.lennier() int`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &testDecl{} + uses, inst := parseInstanceUse(t, tt.line, tt.id, tt.src) + tArgs := typeListToSlice(inst.TypeArgs) + t.Logf(`object: %s with [%s]`, types.ObjectString(uses, nil), (typesutil.TypeList)(tArgs).String()) + + c := Collector{} + c.CollectDCEDeps(d, func() { + c.DeclareDCEDep(uses, tArgs...) + }) + equalSlices(t, d.Dce().getDeps(), tt.wantDeps) + }) + } +} + +func Test_Info_SpecificCasesDeps(t *testing.T) { + tests := []struct { + name string + obj types.Object + tArgs []types.Type + wantDeps []string + }{ + { + name: `struct instantiation with generic object`, + obj: parseObject(t, `Mikey`, + `package astoria; + type Mikey[T comparable] struct{} + `), + tArgs: []types.Type{types.Typ[types.String]}, + wantDeps: []string{`astoria.Mikey[string]`}, + }, + { + name: `method instantiation with generic object`, + obj: parseObject(t, `brand`, + `package astoria; + type Mikey[T comparable] struct{ a T} + func (m Mikey[T]) brand() T { + return m.a + }`), + tArgs: []types.Type{types.Typ[types.String]}, + wantDeps: []string{ + `astoria.Mikey[string]`, + `astoria.brand() string`, + }, + }, + { + name: `method instantiation with generic object and multiple type parameters`, + obj: parseObject(t, `shuffle`, + `package astoria; + type Chunk[K comparable, V any] struct{ data map[K]V } + func (c Chunk[K, V]) shuffle(k K) V { + return c.data[k] + }`), + tArgs: []types.Type{types.Typ[types.String], types.Typ[types.Int]}, + wantDeps: []string{ + `astoria.Chunk[string, int]`, + `astoria.shuffle(string) int`, + }, + }, + { + name: `method instantiation with generic object renamed type parameters`, + obj: parseObject(t, `shuffle`, + `package astoria; + type Chunk[K comparable, V any] struct{ data map[K]V } + func (c Chunk[T, K]) shuffle(k T) K { + return c.data[k] + }`), + tArgs: []types.Type{types.Typ[types.String], types.Typ[types.Int]}, + wantDeps: []string{ + `astoria.Chunk[string, int]`, + `astoria.shuffle(string) int`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &testDecl{} + t.Logf(`object: %s with [%s]`, types.ObjectString(tt.obj, nil), (typesutil.TypeList)(tt.tArgs).String()) + + c := Collector{} + c.CollectDCEDeps(d, func() { + c.DeclareDCEDep(tt.obj, tt.tArgs...) + }) + equalSlices(t, d.Dce().getDeps(), tt.wantDeps) + }) + } +} + func Test_Info_SetAsAlive(t *testing.T) { pkg := testPackage(`fantasia`) @@ -269,11 +904,11 @@ func Test_Info_SetAsAlive(t *testing.T) { obj := quickVar(pkg, `Falkor`) decl := &testDecl{} equal(t, decl.Dce().isAlive(), true) // unnamed is automatically alive - equal(t, decl.Dce().String(), `[unnamed] . -> []`) + equal(t, decl.Dce().String(), `[unnamed] -> []`) decl.Dce().SetAsAlive() equal(t, decl.Dce().isAlive(), true) // still alive but now explicitly alive - equal(t, decl.Dce().String(), `[alive] [unnamed] . -> []`) + equal(t, decl.Dce().String(), `[alive] [unnamed] -> []`) decl.Dce().SetName(obj) equal(t, decl.Dce().isAlive(), true) // alive because SetAsAlive was called @@ -284,7 +919,7 @@ func Test_Info_SetAsAlive(t *testing.T) { obj := quickVar(pkg, `Artax`) decl := &testDecl{} equal(t, decl.Dce().isAlive(), true) // unnamed is automatically alive - equal(t, decl.Dce().String(), `[unnamed] . -> []`) + equal(t, decl.Dce().String(), `[unnamed] -> []`) decl.Dce().SetName(obj) equal(t, decl.Dce().isAlive(), false) // named so no longer automatically alive @@ -480,12 +1115,12 @@ func Test_Selector_SpecificMethods(t *testing.T) { want: []*testDecl{rincewind, rincewindRun, vimes, vimesRun, vimesRead, vetinari}, }, { - name: `exposed method`, + name: `exported method`, deps: []*testDecl{rincewind, rincewindRun}, want: []*testDecl{rincewind, rincewindRun, vetinari}, }, { - name: `unexposed method`, + name: `unexported method`, deps: []*testDecl{rincewind, rincewindHide}, want: []*testDecl{rincewind, rincewindRun, rincewindHide, vetinari}, }, @@ -493,6 +1128,7 @@ func Test_Selector_SpecificMethods(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + vetinari.Dce().deps = nil // reset deps c.CollectDCEDeps(vetinari, func() { for _, decl := range tt.deps { c.DeclareDCEDep(decl.obj) @@ -540,6 +1176,14 @@ func quickVar(pkg *types.Package, name string) *types.Var { return types.NewVar(token.NoPos, pkg, name, types.Typ[types.Int]) } +func newTypeInfo() *types.Info { + return &types.Info{ + Defs: map[*ast.Ident]types.Object{}, + Uses: map[*ast.Ident]types.Object{}, + Instances: map[*ast.Ident]types.Instance{}, + } +} + func parseObject(t *testing.T, name, source string) types.Object { t.Helper() objects := parseObjects(t, source) @@ -554,10 +1198,9 @@ func parseObject(t *testing.T, name, source string) types.Object { func parseObjects(t *testing.T, source string) []types.Object { t.Helper() - info := &types.Info{ - Defs: map[*ast.Ident]types.Object{}, - } - parseInfo(t, source, info) + fset := token.NewFileSet() + info := newTypeInfo() + parsePackage(t, source, fset, info) objects := make([]types.Object, 0, len(info.Defs)) for _, obj := range info.Defs { if obj != nil { @@ -570,9 +1213,22 @@ func parseObjects(t *testing.T, source string) []types.Object { return objects } -func parseInfo(t *testing.T, source string, info *types.Info) *types.Package { +func parseInstanceUse(t *testing.T, lineNo int, idName, source string) (types.Object, types.Instance) { t.Helper() fset := token.NewFileSet() + info := newTypeInfo() + parsePackage(t, source, fset, info) + for id, obj := range info.Uses { + if id.Name == idName && fset.Position(id.Pos()).Line == lineNo { + return obj, info.Instances[id] + } + } + t.Fatalf(`failed to find %s on line %d`, idName, lineNo) + return nil, types.Instance{} +} + +func parsePackage(t *testing.T, source string, fset *token.FileSet, info *types.Info) *types.Package { + t.Helper() f, err := parser.ParseFile(fset, `test.go`, source, 0) if err != nil { t.Fatal(`parsing source:`, err) @@ -626,6 +1282,17 @@ func depCount(t *testing.T, decl *testDecl, want int) { func equal[T comparable](t *testing.T, got, want T) { t.Helper() if got != want { - t.Errorf(`expected %#v but got %#v`, want, got) + t.Errorf("Unexpected value was gotten:\t\nexp: %#v\t\ngot: %#v", want, got) + } +} + +func equalSlices[T comparable](t *testing.T, got, want []T) { + t.Helper() + if len(got) != len(want) { + t.Errorf("expected %d but got %d\n\texp: %#v\n\tgot: %#v", len(want), len(got), want, got) + return + } + for i, wantElem := range want { + equal(t, got[i], wantElem) } } diff --git a/compiler/internal/dce/filters.go b/compiler/internal/dce/filters.go new file mode 100644 index 000000000..1994ded75 --- /dev/null +++ b/compiler/internal/dce/filters.go @@ -0,0 +1,340 @@ +package dce + +import ( + "go/types" + "sort" + "strconv" + "strings" +) + +// getFilters determines the DCE filters for the given object. +// This will return an object filter and optionally return a method filter. +// +// Typically, the object filter will always be set and the method filter +// will be empty unless the object is an unexported method. +// However, when the object is a method invocation on an unnamed interface type +// the object filter will be empty and only the method filter will be set. +// The later shouldn't happen when naming a declaration but only when creating +// dependencies. +func getFilters(o types.Object, tArgs []types.Type) (objectFilter, methodFilter string) { + if f, ok := o.(*types.Func); ok { + sig := f.Type().(*types.Signature) + if recv := sig.Recv(); recv != nil { + // The object is a method so the object filter is the receiver type + // if the receiver type is named, otherwise it's an unnamed interface. + typ := recv.Type() + if ptrType, ok := typ.(*types.Pointer); ok { + typ = ptrType.Elem() + } + if len(tArgs) <= 0 { + tArgs = getTypeArgs(typ) + } + if named, ok := typ.(*types.Named); ok { + objectFilter = getObjectFilter(named.Obj(), tArgs) + } + + // The method is not exported so we only need the method filter. + if !o.Exported() { + methodFilter = getMethodFilter(o, tArgs) + } + return + } + } + + // The object is not a method so we only need the object filter. + objectFilter = getObjectFilter(o, tArgs) + return +} + +// getObjectFilter returns the object filter that functions as the primary +// name when determining if a declaration is alive or not. +// See [naming design](./README.md#naming) for more information. +func getObjectFilter(o types.Object, tArgs []types.Type) string { + return (&filterGen{argTypeRemap: tArgs}).Object(o, tArgs) +} + +// getMethodFilter returns the method filter that functions as the secondary +// name when determining if a declaration is alive or not. +// See [naming design](./README.md#naming) for more information. +func getMethodFilter(o types.Object, tArgs []types.Type) string { + if sig, ok := o.Type().(*types.Signature); ok { + if len(tArgs) <= 0 { + if recv := sig.Recv(); recv != nil { + tArgs = getTypeArgs(recv.Type()) + } + } + gen := &filterGen{argTypeRemap: tArgs} + return objectName(o) + gen.Signature(sig) + } + return `` +} + +// objectName returns the name part of a filter name, +// including the package path, if available. +// +// This is different from `o.Id` since it always includes the package path +// when available and doesn't add "_." when not available. +func objectName(o types.Object) string { + if o.Pkg() != nil { + return o.Pkg().Path() + `.` + o.Name() + } + return o.Name() +} + +// getTypeArgs gets the type arguments for the given type +// wether they are instance types or type parameters. +func getTypeArgs(typ types.Type) []types.Type { + switch t := typ.(type) { + case *types.Pointer: + return getTypeArgs(t.Elem()) + case *types.Named: + if typeArgs := t.TypeArgs(); typeArgs != nil { + return typeListToSlice(typeArgs) + } + if typeParams := t.TypeParams(); typeParams != nil { + return typeParamListToSlice(typeParams) + } + case *types.Signature: + if typeParams := t.RecvTypeParams(); typeParams != nil { + return typeParamListToSlice(typeParams) + } + if typeParams := t.TypeParams(); typeParams != nil { + return typeParamListToSlice(typeParams) + } + } + return nil +} + +// typeListToSlice returns the list of type arguments for the instance types. +func typeListToSlice(instTypes *types.TypeList) []types.Type { + tArgs := make([]types.Type, instTypes.Len()) + for i := range tArgs { + tArgs[i] = instTypes.At(i) + } + return tArgs +} + +// typeParamListToSlice returns the list of type arguments for the type parameters. +func typeParamListToSlice(typeParams *types.TypeParamList) []types.Type { + tParams := make([]types.Type, typeParams.Len()) + for i := range tParams { + tParams[i] = typeParams.At(i).Constraint() + } + return tParams +} + +type processingGroup struct { + o types.Object + tArgs []types.Type +} + +func (p processingGroup) is(o types.Object, tArgs []types.Type) bool { + if len(p.tArgs) != len(tArgs) || p.o != o { + return false + } + for i, tArg := range tArgs { + if p.tArgs[i] != tArg { + return false + } + } + return true +} + +type filterGen struct { + // argTypeRemap is the instance types in the same order as the + // type parameters in the top level object such that the type parameters + // index can be used to get the instance type. + argTypeRemap []types.Type + inProgress []processingGroup +} + +func (gen *filterGen) startProcessing(o types.Object, tArgs []types.Type) bool { + for _, p := range gen.inProgress { + if p.is(o, tArgs) { + return false + } + } + gen.inProgress = append(gen.inProgress, processingGroup{o, tArgs}) + return true +} + +func (gen *filterGen) stopProcessing() { + gen.inProgress = gen.inProgress[:len(gen.inProgress)-1] +} + +// Object returns an object filter or filter part for an object. +func (gen *filterGen) Object(o types.Object, tArgs []types.Type) string { + filter := objectName(o) + + // Add additional type information for generics and instances. + if len(tArgs) <= 0 { + tArgs = getTypeArgs(o.Type()) + } + if len(tArgs) > 0 { + // Avoid infinite recursion in type arguments by + // tracking the current object and type arguments being processed + // and skipping if already in progress. + if gen.startProcessing(o, tArgs) { + filter += gen.TypeArgs(tArgs) + gen.stopProcessing() + } else { + filter += `[...]` + } + } + + return filter +} + +// Signature returns the filter part containing the signature +// parameters and results for a function or method, e.g. `(int)(bool,error)`. +func (gen *filterGen) Signature(sig *types.Signature) string { + filter := `(` + gen.Tuple(sig.Params(), sig.Variadic()) + `)` + switch sig.Results().Len() { + case 0: + break + case 1: + filter += ` ` + gen.Type(sig.Results().At(0).Type()) + default: + filter += `(` + gen.Tuple(sig.Results(), false) + `)` + } + return filter +} + +// TypeArgs returns the filter part containing the type +// arguments, e.g. `[any,int|string]`. +func (gen *filterGen) TypeArgs(tArgs []types.Type) string { + parts := make([]string, len(tArgs)) + for i, tArg := range tArgs { + parts[i] = gen.Type(tArg) + } + return `[` + strings.Join(parts, `, `) + `]` +} + +// Tuple returns the filter part containing parameter or result +// types for a function, e.g. `(int,string)`, `(int,...string)`. +func (gen *filterGen) Tuple(t *types.Tuple, variadic bool) string { + count := t.Len() + parts := make([]string, count) + for i := range parts { + argType := t.At(i).Type() + if i == count-1 && variadic { + if slice, ok := argType.(*types.Slice); ok { + argType = slice.Elem() + } + parts[i] = `...` + gen.Type(argType) + } else { + parts[i] = gen.Type(argType) + } + } + return strings.Join(parts, `, `) +} + +// Type returns the filter part for a single type. +func (gen *filterGen) Type(typ types.Type) string { + switch t := typ.(type) { + case types.Object: + return gen.Object(t, nil) + + case *types.Array: + return `[` + strconv.FormatInt(t.Len(), 10) + `]` + gen.Type(t.Elem()) + case *types.Chan: + return `chan ` + gen.Type(t.Elem()) + case *types.Interface: + return gen.Interface(t) + case *types.Map: + return `map[` + gen.Type(t.Key()) + `]` + gen.Type(t.Elem()) + case *types.Named: + // Get type args from named instance not generic object + return gen.Object(t.Obj(), getTypeArgs(t)) + case *types.Pointer: + return `*` + gen.Type(t.Elem()) + case *types.Signature: + return `func` + gen.Signature(t) + case *types.Slice: + return `[]` + gen.Type(t.Elem()) + case *types.Struct: + return gen.Struct(t) + case *types.TypeParam: + return gen.TypeParam(t) + default: + // Anything else, like basics, just stringify normally. + return t.String() + } +} + +// Union returns the filter part for a union of types from an type parameter +// constraint, e.g. `~string|int|~float64`. +func (gen *filterGen) Union(u *types.Union) string { + parts := make([]string, u.Len()) + for i := range parts { + term := u.Term(i) + part := gen.Type(term.Type()) + if term.Tilde() { + part = "~" + part + } + parts[i] = part + } + // Sort the union so that "string|int" matches "int|string". + sort.Strings(parts) + return strings.Join(parts, `|`) +} + +// Interface returns the filter part for an interface type or +// an interface for a type parameter constraint. +func (gen *filterGen) Interface(inter *types.Interface) string { + // Collect all method constraints with method names and signatures. + parts := make([]string, inter.NumMethods()) + for i := range parts { + fn := inter.Method(i) + parts[i] = fn.Id() + gen.Signature(fn.Type().(*types.Signature)) + } + // Add any union constraints. + for i := 0; i < inter.NumEmbeddeds(); i++ { + if union, ok := inter.EmbeddedType(i).(*types.Union); ok { + parts = append(parts, gen.Union(union)) + } + } + // Sort the parts of the interface since the order doesn't matter. + // e.g. `interface { a(); b() }` is the same as `interface { b(); a() }`. + sort.Strings(parts) + + if len(parts) <= 0 { + return `any` + } + if inter.NumMethods() <= 0 && len(parts) == 1 { + return parts[0] // single constraint union, i.e. `bool|~int|string` + } + return `interface{ ` + strings.Join(parts, `; `) + ` }` +} + +// Struct returns the filter part for a struct type. +func (gen *filterGen) Struct(s *types.Struct) string { + if s.NumFields() <= 0 { + return `struct{}` + } + parts := make([]string, s.NumFields()) + for i := range parts { + f := s.Field(i) + // The field name and order is required to be part of the filter since + // struct matching rely on field names too. Tags are not needed. + // See https://go.dev/ref/spec#Conversions + parts[i] = f.Id() + ` ` + gen.Type(f.Type()) + } + return `struct{ ` + strings.Join(parts, `; `) + ` }` +} + +// TypeParam returns the filter part for a type parameter. +// If there is an argument remap, it will use the remapped type +// so long as it doesn't map to itself. +func (gen *filterGen) TypeParam(t *types.TypeParam) string { + index := t.Index() + if index >= 0 && index < len(gen.argTypeRemap) { + if inst := gen.argTypeRemap[index]; inst != t { + return gen.Type(inst) + } + } + if t.Constraint() == nil { + return `any` + } + return gen.Type(t.Constraint()) +} diff --git a/compiler/internal/dce/info.go b/compiler/internal/dce/info.go index d5993a659..e9a63baf5 100644 --- a/compiler/internal/dce/info.go +++ b/compiler/internal/dce/info.go @@ -5,8 +5,6 @@ import ( "go/types" "sort" "strings" - - "github.com/gopherjs/gopherjs/compiler/typesutil" ) // Info contains information used by the dead-code elimination (DCE) logic to @@ -17,21 +15,22 @@ type Info struct { // and will not be eliminated. alive bool - // importPath is the package path of the package the declaration is in. - importPath string - - // 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. + // objectFilter is the primary DCE name for a declaration. + // This will be the variable, function, or type identifier. + // For methods it is the receiver type identifier. + // If empty, the declaration is assumed to be alive. objectFilter string - // The second part of the identified used by dead-code elimination for methods. - // Empty for other types of symbols. + // methodFilter is the secondary DCE name for a declaration. + // This will be empty if objectFilter is empty. + // This will be set to a qualified method name if the objectFilter + // can not determine if the declaration is alive on it's own. + // See ./README.md for more information. methodFilter string // List of fully qualified (including package path) DCE symbol identifiers the // symbol depends on for dead code elimination purposes. - deps []string + deps map[string]struct{} } // String gets a human-readable representation of the DCE info. @@ -43,17 +42,20 @@ func (d *Info) String() string { if d.unnamed() { tags += `[unnamed] ` } - fullName := d.importPath + `.` + d.objectFilter + fullName := `` + if len(d.objectFilter) > 0 { + fullName += d.objectFilter + ` ` + } if len(d.methodFilter) > 0 { - fullName += `.` + d.methodFilter + fullName += `& ` + d.methodFilter + ` ` } - return tags + fullName + ` -> [` + strings.Join(d.deps, `, `) + `]` + return tags + fullName + `-> [` + strings.Join(d.getDeps(), `, `) + `]` } // unnamed returns true if SetName has not been called for this declaration. // This indicates that the DCE is not initialized. func (d *Info) unnamed() bool { - return d.objectFilter == `` && d.methodFilter == `` + return d.objectFilter == `` } // isAlive returns true if the declaration is marked as alive. @@ -74,35 +76,56 @@ func (d *Info) SetAsAlive() { // SetName sets the name used by DCE to represent the declaration // this DCE info is attached to. -func (d *Info) SetName(o types.Object) { +// +// The given optional type arguments are used to when the object is a +// function with type parameters or anytime the object doesn't carry them. +// If not given, this attempts to get the type arguments from the object. +func (d *Info) SetName(o types.Object, tArgs ...types.Type) { if !d.unnamed() { panic(fmt.Errorf(`may only set the name once for %s`, d.String())) } - d.importPath = o.Pkg().Path() - if typesutil.IsMethod(o) { - recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj() - d.objectFilter = recv.Name() - if !o.Exported() { - d.methodFilter = o.Name() + `~` + // Determine name(s) for DCE. + d.objectFilter, d.methodFilter = getFilters(o, tArgs) + + // Add automatic dependencies for unexported methods on interfaces. + if n, ok := o.Type().(*types.Named); ok { + if it, ok := n.Underlying().(*types.Interface); ok { + for i := it.NumMethods() - 1; i >= 0; i-- { + if m := it.Method(i); !m.Exported() { + d.addDepName(getMethodFilter(m, tArgs)) + } + } } - } else { - d.objectFilter = o.Name() } } -// setDeps sets the declaration dependencies used by DCE +// addDep add a declaration dependencies used by DCE // for the declaration this DCE info is attached to. -// This overwrites any prior set dependencies. -func (d *Info) setDeps(objectSet map[types.Object]struct{}) { - deps := make([]string, 0, len(objectSet)) - for o := range objectSet { - qualifiedName := o.Pkg().Path() + "." + o.Name() - if typesutil.IsMethod(o) { - qualifiedName += "~" +func (d *Info) addDep(o types.Object, tArgs []types.Type) { + objectFilter, methodFilter := getFilters(o, tArgs) + d.addDepName(objectFilter) + d.addDepName(methodFilter) +} + +// addDepName adds a declaration dependency by name. +func (d *Info) addDepName(depName string) { + if len(depName) > 0 { + if d.deps == nil { + d.deps = make(map[string]struct{}) } - deps = append(deps, qualifiedName) + d.deps[depName] = struct{}{} + } +} + +// getDeps gets the dependencies for the declaration sorted by name. +func (id *Info) getDeps() []string { + deps := make([]string, len(id.deps)) + i := 0 + for dep := range id.deps { + deps[i] = dep + i++ } sort.Strings(deps) - d.deps = deps + return deps } diff --git a/compiler/internal/dce/selector.go b/compiler/internal/dce/selector.go index 4eea572e0..3dff49028 100644 --- a/compiler/internal/dce/selector.go +++ b/compiler/internal/dce/selector.go @@ -42,12 +42,12 @@ func (s *Selector[D]) Include(decl D, implementsLink bool) { info := &declInfo[D]{decl: decl} if dce.objectFilter != `` { - info.objectFilter = dce.importPath + `.` + dce.objectFilter + info.objectFilter = dce.objectFilter s.byFilter[info.objectFilter] = append(s.byFilter[info.objectFilter], info) } if dce.methodFilter != `` { - info.methodFilter = dce.importPath + `.` + dce.methodFilter + info.methodFilter = dce.methodFilter s.byFilter[info.methodFilter] = append(s.byFilter[info.methodFilter], info) } } @@ -72,7 +72,7 @@ func (s *Selector[D]) AliveDecls() map[D]struct{} { // Consider all decls the current one is known to depend on and possible add // them to the live queue. - for _, dep := range dce.deps { + for _, dep := range dce.getDeps() { if infos, ok := s.byFilter[dep]; ok { delete(s.byFilter, dep) for _, info := range infos { diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index 0a9ae75da..723172d4f 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -7,7 +7,6 @@ import ( "github.com/gopherjs/gopherjs/compiler/typesutil" "github.com/gopherjs/gopherjs/internal/govendor/subst" - "golang.org/x/exp/typeparams" ) // Resolver translates types defined in terms of type parameters into concrete @@ -141,7 +140,7 @@ func (c *visitor) Visit(n ast.Node) (w ast.Visitor) { for i := 0; i < t.NumMethods(); i++ { method := t.Method(i) c.instances.Add(Instance{ - Object: typeparams.OriginMethod(method), // TODO(nevkontakte): Can be replaced with method.Origin() in Go 1.19. + Object: method.Origin(), TArgs: c.resolver.SubstituteAll(instance.TypeArgs), }) } diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index f847a9825..763cd6428 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -147,6 +147,18 @@ func (iset *InstanceSet) ByObj() map[types.Object][]Instance { return result } +// ForObj returns instances for a given object type belong to. Order is not specified. +// This returns the same values as `ByObj()[obj]`. +func (iset *InstanceSet) ForObj(obj types.Object) []Instance { + result := []Instance{} + for _, inst := range iset.values { + if inst.Object == obj { + result = append(result, inst) + } + } + return result +} + // PackageInstanceSets stores an InstanceSet for each package in a program, keyed // by import path. type PackageInstanceSets map[string]*InstanceSet diff --git a/compiler/statements.go b/compiler/statements.go index d4ca76471..d8a2026c5 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -443,7 +443,8 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { } case token.TYPE: for _, spec := range decl.Specs { - o := fc.pkgCtx.Defs[spec.(*ast.TypeSpec).Name].(*types.TypeName) + id := spec.(*ast.TypeSpec).Name + o := fc.pkgCtx.Defs[id].(*types.TypeName) fc.pkgCtx.typeNames.Add(o) fc.pkgCtx.DeclareDCEDep(o) } diff --git a/compiler/utils.go b/compiler/utils.go index a69d0fe77..8fc2da572 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -447,7 +447,7 @@ func (fc *funcContext) knownInstances(o types.Object) []typeparams.Instance { return []typeparams.Instance{{Object: o}} } - return fc.pkgCtx.instanceSet.Pkg(o.Pkg()).ByObj()[o] + return fc.pkgCtx.instanceSet.Pkg(o.Pkg()).ForObj(o) } // instName returns a JS expression that refers to the provided instance of a @@ -458,6 +458,7 @@ func (fc *funcContext) instName(inst typeparams.Instance) string { if inst.IsTrivial() { return objName } + fc.pkgCtx.DeclareDCEDep(inst.Object, inst.TArgs...) return fmt.Sprintf("%s[%d /* %v */]", objName, fc.pkgCtx.instanceSet.ID(inst), inst.TArgs) } @@ -514,7 +515,7 @@ 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) diff --git a/go.mod b/go.mod index ccb130f48..0ea8a30b6 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 github.com/visualfc/goembed v0.3.3 - golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a golang.org/x/sync v0.5.0 golang.org/x/sys v0.10.0 golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 diff --git a/go.sum b/go.sum index 8e69980d0..d8e2f6a88 100644 --- a/go.sum +++ b/go.sum @@ -270,8 +270,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= -golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= From f565ca5a233a55b710ee93b130f31ec95c938ae0 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Tue, 24 Sep 2024 11:56:48 -0600 Subject: [PATCH 10/58] Adding compiler tests for DCE --- compiler/compiler_test.go | 597 +++++++++++++++++++++++++++++++++++--- go.mod | 5 +- go.sum | 1 + 3 files changed, 554 insertions(+), 49 deletions(-) diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 377f09d94..c17037e94 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -2,15 +2,17 @@ package compiler import ( "bytes" - "go/ast" - "go/build" - "go/parser" - "go/token" + "fmt" "go/types" + "path/filepath" + "regexp" + "sort" "testing" "github.com/google/go-cmp/cmp" - "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/packages" + + "github.com/gopherjs/gopherjs/compiler/internal/dce" ) func TestOrder(t *testing.T) { @@ -41,27 +43,336 @@ func Bfunc() int { return varA+varB } ` + files := []source{{"fileA.go", []byte(fileA)}, {"fileB.go", []byte(fileB)}} - compare(t, "foo", files, false) - compare(t, "foo", files, true) + compareOrder(t, files, false) + compareOrder(t, files, true) } -func compare(t *testing.T, path string, sourceFiles []source, minify bool) { - outputNormal, err := compile(path, sourceFiles, minify) - if err != nil { - t.Fatal(err) - } +func TestDeclSelection_KeepUnusedExportedMethods(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) Bar() { + println("bar") + } + func (f Foo) Baz() { // unused + println("baz") + } + func main() { + Foo{}.Bar() + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Baz`) +} + +func TestDeclSelection_RemoveUnusedUnexportedMethods(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) Bar() { + println("bar") + } + func (f Foo) baz() { // unused + println("baz") + } + func main() { + Foo{}.Bar() + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) + + sel.DeclCode.IsDead(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) +} + +func TestDeclSelection_KeepUnusedUnexportedMethodForInterface(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) Bar() { + println("foo") + } + func (f Foo) baz() {} // unused + + type Foo2 struct {} + func (f Foo2) Bar() { + println("foo2") + } + + type IFoo interface { + Bar() + baz() + } + func main() { + fs := []any{ Foo{}, Foo2{} } + for _, f := range fs { + if i, ok := f.(IFoo); ok { + i.Bar() + } + } + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) + + // `baz` is used to duck-type (via method list) against IFoo + // but the method itself is not used so can be removed. + sel.DeclCode.IsDead(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) + sel.MethodListCode.IsAlive(`^\s*Foo.methods = .* \{prop: "baz", name: "baz"`) +} + +func TestDeclSelection_KeepUnexportedMethodUsedViaInterfaceLit(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) Bar() { + println("foo") + } + func (f Foo) baz() { + println("baz") + } + func main() { + var f interface { + Bar() + baz() + } = Foo{} + f.baz() + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) +} + +func TestDeclSelection_KeepAliveUnexportedMethodsUsedInMethodExpressions(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) baz() { + println("baz") + } + func main() { + fb := Foo.baz + fb(Foo{}) + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) +} + +func TestDeclSelection_RemoveUnusedFuncInstance(t *testing.T) { + src := ` + package main + func Sum[T int | float64](values ...T) T { + var sum T + for _, v := range values { + sum += v + } + return sum + } + func Foo() { // unused + println(Sum(1, 2, 3)) + } + func main() { + println(Sum(1.1, 2.2, 3.3)) + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Sum\[\d+ /\* float64 \*/\]`) + sel.DeclCode.IsAlive(`^\s*sliceType(\$\d+)? = \$sliceType\(\$Float64\)`) + + sel.DeclCode.IsDead(`^\s*Foo = function`) + sel.DeclCode.IsDead(`^\s*sliceType(\$\d+)? = \$sliceType\(\$Int\)`) + + // TODO(gn): This should not be alive because it is not used. + sel.DeclCode.IsAlive(`^\s*Sum\[\d+ /\* int \*/\]`) +} + +func TestDeclSelection_RemoveUnusedStructTypeInstances(t *testing.T) { + src := ` + package main + type Foo[T any] struct { v T } + func (f Foo[T]) Bar() { + println(f.v) + } + + var _ = Foo[float64]{v: 3.14} // unused + + func main() { + Foo[int]{v: 7}.Bar() + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\[\d+ /\* int \*/\]\)\.prototype\.Bar`) + + // TODO(gn): This should not be alive because it is not used. + sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* float64 \*/\] = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\[\d+ /\* float64 \*/\]\)\.prototype\.Bar`) +} + +func TestDeclSelection_RemoveUnusedInterfaceTypeInstances(t *testing.T) { + src := ` + package main + type Foo[T any] interface { Bar(v T) } + + type Baz int + func (b Baz) Bar(v int) { + println(v + int(b)) + } + + var F64 = FooBar[float64] // unused + + func FooBar[T any](f Foo[T], v T) { + f.Bar(v) + } + + func main() { + FooBar[int](Baz(42), 12) // Baz implements Foo[int] + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Baz = \$newType`) + sel.DeclCode.IsAlive(`^\s*Baz\.prototype\.Bar`) + sel.InitCode.IsDead(`\$pkg\.F64 = FooBar\[\d+ /\* float64 \*/\]`) + + sel.DeclCode.IsAlive(`^\s*FooBar\[\d+ /\* int \*/\]`) + // TODO(gn): Below should be alive because it is an arg to FooBar[int]. + sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) + + // TODO(gn): Below should be dead because it is only used by a dead init. + sel.DeclCode.IsAlive(`^\s*FooBar\[\d+ /\* float64 \*/\]`) + sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* float64 \*/\] = \$newType`) +} + +func TestDeclSelection_RemoveUnusedMethodWithDifferentSignature(t *testing.T) { + src := ` + package main + type Foo struct{} + func (f Foo) Bar() { println("Foo") } + func (f Foo) baz(x int) { println(x) } // unused + + type Foo2 struct{} + func (f Foo2) Bar() { println("Foo2") } + func (f Foo2) baz(x string) { println(x) } + + func main() { + f1 := Foo{} + f1.Bar() + + f2 := Foo2{} + f2.Bar() + f2.baz("foo") + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\)\.prototype\.Bar`) + // TODO(gn): Below should be dead because it is not used even though + // its name matches a used unexported method. + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\)\.prototype\.baz`) + + sel.DeclCode.IsAlive(`^\s*Foo2 = \$newType`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo2\)\.prototype\.Bar`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo2\)\.prototype\.baz`) +} + +func TestDeclSelection_RemoveUnusedUnexportedMethodInstance(t *testing.T) { + src := ` + package main + type Foo[T any] struct{} + func (f Foo[T]) Bar() { println("Foo") } + func (f Foo[T]) baz(x T) { Baz[T]{v: x}.Bar() } + + type Baz[T any] struct{ v T } + func (b Baz[T]) Bar() { println("Baz", b.v) } + + func main() { + f1 := Foo[int]{} + f1.Bar() + f1.baz(7) + + f2 := Foo[uint]{} // Foo[uint].baz is unused + f2.Bar() + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* int \*/\]\)\.prototype\.Bar`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* int \*/\]\)\.prototype\.baz`) + sel.DeclCode.IsAlive(`^\s*Baz\[\d+ /\* int \*/\] = \$newType`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Baz\[\d+ /\* int \*/\]\)\.prototype\.Bar`) + + sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* uint \*/\] = \$newType`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* uint \*/\]\)\.prototype\.Bar`) + // TODO(gn): All three below should be dead because Foo[uint].baz is unused. + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* uint \*/\]\)\.prototype\.baz`) + sel.DeclCode.IsAlive(`^\s*Baz\[\d+ /\* uint \*/\] = \$newType`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Baz\[\d+ /\* uint \*/\]\)\.prototype\.Bar`) +} + +func TestDeclSelection_RemoveUnusedTypeConstraint(t *testing.T) { + src := ` + package main + type Foo interface{ int | string } + + type Bar[T Foo] struct{ v T } + func (b Bar[T]) Baz() { println(b.v) } + + var ghost = Bar[int]{v: 7} // unused + + func main() { + println("do nothing") + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsDead(`^\s*Foo = \$newType`) + sel.DeclCode.IsDead(`^\s*Bar\[\d+ /\* int \*/\] = \$newType`) + sel.DeclCode.IsDead(`^\s*\$ptrType\(Bar\[\d+ /\* int \*/\]\)\.prototype\.Baz`) + sel.InitCode.IsDead(`ghost = new Bar\[\d+ /\* int \*/\]\.ptr\(7\)`) +} + +func compareOrder(t *testing.T, sourceFiles []source, minify bool) { + t.Helper() + outputNormal := compile(t, sourceFiles, minify) // reverse the array for i, j := 0, len(sourceFiles)-1; i < j; i, j = i+1, j-1 { sourceFiles[i], sourceFiles[j] = sourceFiles[j], sourceFiles[i] } - outputReversed, err := compile(path, sourceFiles, minify) - if err != nil { - t.Fatal(err) - } + outputReversed := compile(t, sourceFiles, minify) if diff := cmp.Diff(string(outputNormal), string(outputReversed)); diff != "" { t.Errorf("files in different order produce different JS:\n%s", diff) @@ -73,44 +384,116 @@ type source struct { contents []byte } -func compile(path string, sourceFiles []source, minify bool) ([]byte, error) { - conf := loader.Config{} - conf.Fset = token.NewFileSet() - conf.ParserMode = parser.ParseComments +func compile(t *testing.T, sourceFiles []source, minify bool) []byte { + t.Helper() + rootPkg := parseSources(t, sourceFiles, nil) + archives := compileProject(t, rootPkg, minify) - context := build.Default // make a copy of build.Default - conf.Build = &context - conf.Build.BuildTags = []string{"js"} + path := rootPkg.PkgPath + a, ok := archives[path] + if !ok { + t.Fatalf(`root package not found in archives: %s`, path) + } - var astFiles []*ast.File - for _, sourceFile := range sourceFiles { - astFile, err := parser.ParseFile(conf.Fset, sourceFile.name, sourceFile.contents, parser.ParseComments) - if err != nil { - return nil, err - } - astFiles = append(astFiles, astFile) + b := renderPackage(t, a, minify) + if len(b) == 0 { + t.Fatal(`compile had no output`) } - conf.CreateFromFiles(path, astFiles...) - prog, err := conf.Load() + return b +} + +// parseSources parses the given source files and returns the root package +// that contains the given source files. +// +// The source file should all be from the same package as the files for the +// root package. At least one source file must be given. +// +// The auxillary files can be for different packages but should have paths +// added to the source name so that they can be grouped together by package. +// To import an auxillary package, the path should be prepended by +// `github.com/gopherjs/gopherjs/compiler`. +func parseSources(t *testing.T, sourceFiles []source, auxFiles []source) *packages.Package { + t.Helper() + const mode = packages.NeedName | + packages.NeedFiles | + packages.NeedImports | + packages.NeedDeps | + packages.NeedTypes | + packages.NeedSyntax + + dir, err := filepath.Abs(`./`) if err != nil { - return nil, err + t.Fatal(`error getting working directory:`, err) } + patterns := make([]string, len(sourceFiles)) + overlay := make(map[string][]byte, len(sourceFiles)) + for i, src := range sourceFiles { + filename := src.name + patterns[i] = filename + absName := filepath.Join(dir, filename) + overlay[absName] = []byte(src.contents) + } + for _, src := range auxFiles { + absName := filepath.Join(dir, src.name) + overlay[absName] = []byte(src.contents) + } + + config := &packages.Config{ + Mode: mode, + Overlay: overlay, + Dir: dir, + } + + pkgs, err := packages.Load(config, patterns...) + if err != nil { + t.Fatal(`error loading packages:`, err) + } + + hasErrors := false + packages.Visit(pkgs, nil, func(pkg *packages.Package) { + for _, err := range pkg.Errors { + hasErrors = true + fmt.Println(err) + } + }) + if hasErrors { + t.FailNow() + } + + if len(pkgs) != 1 { + t.Fatal(`expected one and only one root package but got`, len(pkgs)) + } + return pkgs[0] +} + +// compileProject compiles the given root package and all packages imported by the root. +// This returns the compiled archives of all packages keyed by their import path. +func compileProject(t *testing.T, root *packages.Package, minify bool) map[string]*Archive { + t.Helper() + pkgMap := map[string]*packages.Package{} + packages.Visit([]*packages.Package{root}, nil, func(pkg *packages.Package) { + pkgMap[pkg.PkgPath] = pkg + }) + archiveCache := map[string]*Archive{} var importContext *ImportContext importContext = &ImportContext{ - Packages: make(map[string]*types.Package), + Packages: map[string]*types.Package{}, Import: func(path string) (*Archive, error) { // find in local cache if a, ok := archiveCache[path]; ok { return a, nil } - pi := prog.Package(path) - importContext.Packages[path] = pi.Pkg + pkg, ok := pkgMap[path] + if !ok { + t.Fatal(`package not found:`, path) + } + importContext.Packages[path] = pkg.Types // compile package - a, err := Compile(path, pi.Files, prog.Fset, importContext, minify) + a, err := Compile(path, pkg.Syntax, pkg.Fset, importContext, minify) if err != nil { return nil, err } @@ -119,18 +502,15 @@ func compile(path string, sourceFiles []source, minify bool) ([]byte, error) { }, } - a, err := importContext.Import(path) - if err != nil { - return nil, err - } - b, err := renderPackage(a) + _, err := importContext.Import(root.PkgPath) if err != nil { - return nil, err + t.Fatal(`failed to compile:`, err) } - return b, nil + return archiveCache } -func renderPackage(archive *Archive) ([]byte, error) { +func renderPackage(t *testing.T, archive *Archive, minify bool) []byte { + t.Helper() selection := make(map[*Decl]struct{}) for _, d := range archive.Declarations { selection[d] = struct{}{} @@ -138,9 +518,130 @@ func renderPackage(archive *Archive) ([]byte, error) { buf := &bytes.Buffer{} - if err := WritePkgCode(archive, selection, goLinknameSet{}, false, &SourceMapFilter{Writer: buf}); err != nil { - return nil, err + if err := WritePkgCode(archive, selection, goLinknameSet{}, minify, &SourceMapFilter{Writer: buf}); err != nil { + t.Fatal(err) } - return buf.Bytes(), nil + return buf.Bytes() +} + +type selectionTester struct { + t *testing.T + mainPkg *Archive + archives map[string]*Archive + packages []*Archive + dceSelection map[*Decl]struct{} + + DeclCode *selectionCodeTester + InitCode *selectionCodeTester + MethodListCode *selectionCodeTester +} + +func declSelection(t *testing.T, sourceFiles []source, auxFiles []source) *selectionTester { + t.Helper() + root := parseSources(t, sourceFiles, auxFiles) + archives := compileProject(t, root, false) + mainPkg := archives[root.PkgPath] + + paths := make([]string, 0, len(archives)) + for path := range archives { + paths = append(paths, path) + } + sort.Strings(paths) + packages := make([]*Archive, 0, len(archives)-1) + for _, path := range paths { + packages = append(packages, archives[path]) + } + + sel := &dce.Selector[*Decl]{} + for _, pkg := range packages { + for _, d := range pkg.Declarations { + sel.Include(d, false) + } + } + dceSelection := sel.AliveDecls() + + st := &selectionTester{ + t: t, + mainPkg: mainPkg, + archives: archives, + packages: packages, + dceSelection: dceSelection, + } + + st.DeclCode = &selectionCodeTester{st, `DeclCode`, func(d *Decl) []byte { return d.DeclCode }} + st.InitCode = &selectionCodeTester{st, `InitCode`, func(d *Decl) []byte { return d.InitCode }} + st.MethodListCode = &selectionCodeTester{st, `MethodListCode`, func(d *Decl) []byte { return d.MethodListCode }} + return st +} + +func (st *selectionTester) PrintDeclStatus() { + st.t.Helper() + for _, pkg := range st.packages { + fmt.Println(`Package`, pkg.ImportPath) + for _, decl := range pkg.Declarations { + if _, ok := st.dceSelection[decl]; ok { + fmt.Printf(" [Alive] %q\n", string(decl.FullName)) + } else { + fmt.Printf(" [Dead] %q\n", string(decl.FullName)) + } + if len(decl.DeclCode) > 0 { + fmt.Printf(" DeclCode: %q\n", string(decl.DeclCode)) + } + if len(decl.InitCode) > 0 { + fmt.Printf(" InitCode: %q\n", string(decl.InitCode)) + } + if len(decl.MethodListCode) > 0 { + fmt.Printf(" MethodListCode: %q\n", string(decl.MethodListCode)) + } + if len(decl.TypeInitCode) > 0 { + fmt.Printf(" TypeInitCode: %q\n", string(decl.TypeInitCode)) + } + if len(decl.Vars) > 0 { + fmt.Println(` Vars:`, decl.Vars) + } + } + } +} + +type selectionCodeTester struct { + st *selectionTester + codeName string + getCode func(*Decl) []byte +} + +func (ct *selectionCodeTester) IsAlive(pattern string) { + ct.st.t.Helper() + decl := ct.FindDeclMatch(pattern) + if _, ok := ct.st.dceSelection[decl]; !ok { + ct.st.t.Error(`expected the`, ct.codeName, `code to be alive:`, pattern) + } +} + +func (ct *selectionCodeTester) IsDead(pattern string) { + ct.st.t.Helper() + decl := ct.FindDeclMatch(pattern) + if _, ok := ct.st.dceSelection[decl]; ok { + ct.st.t.Error(`expected the`, ct.codeName, `code to be dead:`, pattern) + } +} + +func (ct *selectionCodeTester) FindDeclMatch(pattern string) *Decl { + ct.st.t.Helper() + regex := regexp.MustCompile(pattern) + var found *Decl + for _, pkg := range ct.st.packages { + for _, d := range pkg.Declarations { + if regex.Match(ct.getCode(d)) { + if found != nil { + ct.st.t.Fatal(`multiple`, ct.codeName, `found containing pattern:`, pattern) + } + found = d + } + } + } + if found == nil { + ct.st.t.Fatal(ct.codeName, `not found with pattern:`, pattern) + } + return found } diff --git a/go.mod b/go.mod index ccb130f48..cfa813b63 100644 --- a/go.mod +++ b/go.mod @@ -20,4 +20,7 @@ require ( golang.org/x/tools v0.16.0 ) -require github.com/inconshreveable/mousetrap v1.0.0 // indirect +require ( + github.com/inconshreveable/mousetrap v1.0.0 // indirect + golang.org/x/mod v0.14.0 // indirect +) diff --git a/go.sum b/go.sum index 8e69980d0..65b1d6a2c 100644 --- a/go.sum +++ b/go.sum @@ -298,6 +298,7 @@ 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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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= From 6fe8b6e09e54ea9b201852a68d29925b5c482216 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Fri, 27 Sep 2024 10:12:01 -0600 Subject: [PATCH 11/58] Update for integration tests --- compiler/compiler_test.go | 29 ++++++------ compiler/internal/dce/README.md | 40 ++++++++++++----- compiler/internal/dce/dce_test.go | 75 +------------------------------ compiler/internal/dce/info.go | 19 ++------ 4 files changed, 46 insertions(+), 117 deletions(-) diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index c17037e94..9c831d456 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -204,9 +204,7 @@ func TestDeclSelection_RemoveUnusedFuncInstance(t *testing.T) { sel.DeclCode.IsDead(`^\s*Foo = function`) sel.DeclCode.IsDead(`^\s*sliceType(\$\d+)? = \$sliceType\(\$Int\)`) - - // TODO(gn): This should not be alive because it is not used. - sel.DeclCode.IsAlive(`^\s*Sum\[\d+ /\* int \*/\]`) + sel.DeclCode.IsDead(`^\s*Sum\[\d+ /\* int \*/\]`) } func TestDeclSelection_RemoveUnusedStructTypeInstances(t *testing.T) { @@ -229,9 +227,8 @@ func TestDeclSelection_RemoveUnusedStructTypeInstances(t *testing.T) { sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\[\d+ /\* int \*/\]\)\.prototype\.Bar`) - // TODO(gn): This should not be alive because it is not used. - sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* float64 \*/\] = \$newType`) - sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\[\d+ /\* float64 \*/\]\)\.prototype\.Bar`) + sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* float64 \*/\] = \$newType`) + sel.DeclCode.IsDead(`^\s*\$ptrType\(Foo\[\d+ /\* float64 \*/\]\)\.prototype\.Bar`) } func TestDeclSelection_RemoveUnusedInterfaceTypeInstances(t *testing.T) { @@ -262,11 +259,12 @@ func TestDeclSelection_RemoveUnusedInterfaceTypeInstances(t *testing.T) { sel.InitCode.IsDead(`\$pkg\.F64 = FooBar\[\d+ /\* float64 \*/\]`) sel.DeclCode.IsAlive(`^\s*FooBar\[\d+ /\* int \*/\]`) - // TODO(gn): Below should be alive because it is an arg to FooBar[int]. + // The Foo[int] instance is defined as a parameter in FooBar[int] that is alive. + // However, Foo[int] isn't used directly in the code so it can be removed. + // JS will simply duck-type the Baz object to Foo[int] without Foo[int] specifically defined. sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) - // TODO(gn): Below should be dead because it is only used by a dead init. - sel.DeclCode.IsAlive(`^\s*FooBar\[\d+ /\* float64 \*/\]`) + sel.DeclCode.IsDead(`^\s*FooBar\[\d+ /\* float64 \*/\]`) sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* float64 \*/\] = \$newType`) } @@ -295,9 +293,7 @@ func TestDeclSelection_RemoveUnusedMethodWithDifferentSignature(t *testing.T) { sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\)\.prototype\.Bar`) - // TODO(gn): Below should be dead because it is not used even though - // its name matches a used unexported method. - sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\)\.prototype\.baz`) + sel.DeclCode.IsDead(`\s*\$ptrType\(Foo\)\.prototype\.baz`) sel.DeclCode.IsAlive(`^\s*Foo2 = \$newType`) sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo2\)\.prototype\.Bar`) @@ -334,10 +330,11 @@ func TestDeclSelection_RemoveUnusedUnexportedMethodInstance(t *testing.T) { sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* uint \*/\] = \$newType`) sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* uint \*/\]\)\.prototype\.Bar`) - // TODO(gn): All three below should be dead because Foo[uint].baz is unused. - sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* uint \*/\]\)\.prototype\.baz`) - sel.DeclCode.IsAlive(`^\s*Baz\[\d+ /\* uint \*/\] = \$newType`) - sel.DeclCode.IsAlive(`\s*\$ptrType\(Baz\[\d+ /\* uint \*/\]\)\.prototype\.Bar`) + + // All three below are dead because Foo[uint].baz is unused. + sel.DeclCode.IsDead(`\s*\$ptrType\(Foo\[\d+ /\* uint \*/\]\)\.prototype\.baz`) + sel.DeclCode.IsDead(`^\s*Baz\[\d+ /\* uint \*/\] = \$newType`) + sel.DeclCode.IsDead(`\s*\$ptrType\(Baz\[\d+ /\* uint \*/\]\)\.prototype\.Bar`) } func TestDeclSelection_RemoveUnusedTypeConstraint(t *testing.T) { diff --git a/compiler/internal/dce/README.md b/compiler/internal/dce/README.md index 4fe2882c0..db6408d62 100644 --- a/compiler/internal/dce/README.md +++ b/compiler/internal/dce/README.md @@ -35,9 +35,10 @@ may be safely eliminated, i.e. not outputted to the JS file(s). The following is the logic behind the DCE mechanism. Not all of the following is used since some conditions are difficult to determine even with a lot of -additional information. To ensure that the JS output is fully functional, -we bias the DCE towards things being alive. We'd rather keep something we -don't need than remove something that is needed. +additional information, and because GopherJS stores some additional information +making some parts of DCE unnecessary. To ensure that the JS output is fully +functional, we bias the DCE towards things being alive. We'd rather keep +something we don't need than remove something that is needed. ### Package @@ -92,10 +93,14 @@ All the types in the function signatures and embedded interfaces are the dependents of the interface. Interfaces may contain exported and unexported function signatures. -If an interface is alive then all of the functions, even the unexported -functions, are alive. +If an interface is alive then all of the functions are alive. Since there are many ways to wrap a type with an interface, any alive type that -duck-types to an interface must have all of the matching methods alive. +duck-types to an interface must have all of the matching methods also alive. + +In theory the unexported functions are also alive however, for GopherJS there +is an exception because duck-typing is handled separately the method +definitions. Those difference are discussed in [Dependencies](#dependencies) +but for this idea we discuss DCE more generally. Since the exported methods in an alive type will be alive, see [Named Types](#named-types), the only ones here that need to be considered @@ -247,6 +252,13 @@ the unexported method alive. Since the unexported method is only visible in the package in which it is defined, the package path is included in the method name. +To simplify the above for GopherJS, we don't look at the receiver for +an unexported method before indicating it is alive. Meaning if there is no +interface, only two named objects with identical unexported methods, the use +of either will indicate a use of both. This will cause slightly more unexported +method to be alive while reducing the complication of type checking which object +or type of object is performing the call. + | Declaration | exported | unexported | non-generic | generic | object name | method name | |:------------|:--------:|:----------:|:-----------:|:-------:|:------------|:------------| | variables | █ | █ | █ | n/a | `.` | | @@ -324,18 +336,22 @@ is recursive, e.g. `Foo[Bar[Bar[...]]]`. ### Dependencies -The dependencies are initialized via two paths. - -The first is dependencies that are specified in an expression. +The dependencies that are specified in an expression. For example a function that invokes another function will be dependent on that invoked function. When a dependency is added it will be added as one or more names to the declaration that depends on it. It follows the [naming rules](#naming) so that the dependencies will match correctly. -The second is structural dependencies that are specified automatically while -the declaration is being named. When an interface is named, it will -automatically add all unexported signatures as dependencies via +In theory, structural dependencies would be needed to be added +automatically while the declaration is being named. When an interface is named, +it would automatically add all unexported signatures as dependencies via `.()()`. +However, we do not need to do that in GopherJS because we aren't using +the existence of realized methods in duck-typing. GopherJS stores full set +of method information when describing the type so that even when things like +unexported methods in interfaces are removed, duck-typing will still work +correctly. This reduces the size of the code by not keeping a potentially +long method body when the signature is all that is needed. Currently we don't filter unused packages so there is no need to automatically add dependencies on the packages themselves. This is also why the package diff --git a/compiler/internal/dce/dce_test.go b/compiler/internal/dce/dce_test.go index 226e90c7e..da4f54466 100644 --- a/compiler/internal/dce/dce_test.go +++ b/compiler/internal/dce/dce_test.go @@ -248,26 +248,6 @@ func Test_Info_SetNameAndDep(t *testing.T) { objectFilter: `jim.wembley`, }, }, - { - name: `interface with unexported methods setting dependencies`, - obj: parseObject(t, `Hoggle`, - `package jim - type Hoggle interface{ - cowardly() bool - loyalTo(goblin string) bool - makePrinceOfTheBogOfEternalStench() error - }`), - want: Info{ - objectFilter: `jim.Hoggle`, - // The automatically defined dependencies for unexported methods - // in the interface that match with the methodFilter of unexported methods. - deps: map[string]struct{}{ - `jim.cowardly() bool`: {}, - `jim.loyalTo(string) bool`: {}, - `jim.makePrinceOfTheBogOfEternalStench() error`: {}, - }, - }, - }, { name: `unexported method resulting in an interface with exported methods`, obj: parseObject(t, `bear`, @@ -439,54 +419,6 @@ func Test_Info_SetNameAndDep(t *testing.T) { methodFilter: `jim.sidekick() jim.Beaker[any]`, }, }, - { - name: `unexported method in interface from named embedded interface`, - obj: parseObject(t, `Waldorf`, - `package jim - type Statler interface{ - boo() - } - type Waldorf interface{ - Statler - }`), - want: Info{ - objectFilter: `jim.Waldorf`, - deps: map[string]struct{}{ - `jim.boo()`: {}, - }, - }, - }, - { - name: `unexported method in interface from unnamed embedded interface`, - obj: parseObject(t, `Waldorf`, - `package jim - type Waldorf interface{ - interface{ - boo() - } - }`), - want: Info{ - objectFilter: `jim.Waldorf`, - deps: map[string]struct{}{ - `jim.boo()`: {}, - }, - }, - }, - { - name: `unexported method on instance of generic interface`, - obj: parseObject(t, `Waldorf`, - `package jim - type Statler[T any] interface{ - boo() T - } - type Waldorf Statler[string]`), - want: Info{ - objectFilter: `jim.Waldorf`, - deps: map[string]struct{}{ - `jim.boo() string`: {}, - }, - }, - }, { name: `struct with self referencing type parameter constraints`, obj: parseObject(t, `Keys`, @@ -503,7 +435,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { }, }, { - name: `struct with self referencing type parameter constraints`, + name: `interface with self referencing type parameter constraints`, obj: parseObject(t, `ElectricMayhem`, `package jim type ElectricMayhem[K comparable, V any, M ~map[K]V] interface { @@ -513,11 +445,6 @@ func Test_Info_SetNameAndDep(t *testing.T) { }`), want: Info{ objectFilter: `jim.ElectricMayhem[comparable, any, ~map[comparable]any]`, - deps: map[string]struct{}{ - `jim.keys() []comparable`: {}, - `jim.values() []any`: {}, - `jim.asMap() ~map[comparable]any`: {}, - }, }, }, { diff --git a/compiler/internal/dce/info.go b/compiler/internal/dce/info.go index e9a63baf5..244da8f17 100644 --- a/compiler/internal/dce/info.go +++ b/compiler/internal/dce/info.go @@ -22,14 +22,14 @@ type Info struct { objectFilter string // methodFilter is the secondary DCE name for a declaration. - // This will be empty if objectFilter is empty. + // This usually will be empty if objectFilter is empty. // This will be set to a qualified method name if the objectFilter // can not determine if the declaration is alive on it's own. // See ./README.md for more information. methodFilter string - // List of fully qualified (including package path) DCE symbol identifiers the - // symbol depends on for dead code elimination purposes. + // Set of fully qualified (including package path) DCE symbol + // and/or method names that this DCE declaration depends on. deps map[string]struct{} } @@ -55,7 +55,7 @@ func (d *Info) String() string { // unnamed returns true if SetName has not been called for this declaration. // This indicates that the DCE is not initialized. func (d *Info) unnamed() bool { - return d.objectFilter == `` + return d.objectFilter == `` && d.methodFilter == `` } // isAlive returns true if the declaration is marked as alive. @@ -87,17 +87,6 @@ func (d *Info) SetName(o types.Object, tArgs ...types.Type) { // Determine name(s) for DCE. d.objectFilter, d.methodFilter = getFilters(o, tArgs) - - // Add automatic dependencies for unexported methods on interfaces. - if n, ok := o.Type().(*types.Named); ok { - if it, ok := n.Underlying().(*types.Interface); ok { - for i := it.NumMethods() - 1; i >= 0; i-- { - if m := it.Method(i); !m.Exported() { - d.addDepName(getMethodFilter(m, tArgs)) - } - } - } - } } // addDep add a declaration dependencies used by DCE From b9976d5a0365607ce570d88def07be9f9e7cc55d Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Tue, 1 Oct 2024 10:08:27 -0600 Subject: [PATCH 12/58] Updating to match some future changes --- compiler/internal/dce/collector.go | 22 ++++----- compiler/internal/dce/dce_test.go | 77 +++++++++++++++-------------- compiler/internal/dce/filters.go | 32 ++++++++++++ compiler/internal/dce/info.go | 78 ++++++++++++++++-------------- compiler/internal/dce/selector.go | 6 +-- 5 files changed, 130 insertions(+), 85 deletions(-) create mode 100644 compiler/internal/dce/filters.go diff --git a/compiler/internal/dce/collector.go b/compiler/internal/dce/collector.go index 7d251029b..ade313b4e 100644 --- a/compiler/internal/dce/collector.go +++ b/compiler/internal/dce/collector.go @@ -14,7 +14,7 @@ type Decl interface { // Collector is a tool to collect dependencies for a declaration // that'll be used in dead-code elimination (DCE). type Collector struct { - dependencies map[types.Object]struct{} + dce *Info } // CollectDCEDeps captures a list of Go objects (types, functions, etc.) @@ -22,25 +22,25 @@ type Collector struct { // as dependencies of the given dead-code elimination info. // // Only one CollectDCEDeps call can be active at a time. -// This will overwrite any previous dependencies collected for the given DCE. func (c *Collector) CollectDCEDeps(decl Decl, f func()) { - if c.dependencies != nil { + if c.dce != nil { panic(errors.New(`called CollectDCEDeps inside another CollectDCEDeps call`)) } - c.dependencies = make(map[types.Object]struct{}) - defer func() { c.dependencies = nil }() + c.dce = decl.Dce() + defer func() { c.dce = nil }() f() - - decl.Dce().setDeps(c.dependencies) } // DeclareDCEDep records that the code that is currently being transpiled -// depends on a given Go object. +// depends on a given Go object with optional type arguments. +// +// The given optional type arguments are used to when the object is a +// function with type parameters or anytime the object doesn't carry them. +// If not given, this attempts to get the type arguments from the object. func (c *Collector) DeclareDCEDep(o types.Object) { - if c.dependencies == nil { - return // Dependencies are not being collected. + if c.dce != nil { + c.dce.addDep(o) } - c.dependencies[o] = struct{}{} } diff --git a/compiler/internal/dce/dce_test.go b/compiler/internal/dce/dce_test.go index c46a7f03c..d75e85879 100644 --- a/compiler/internal/dce/dce_test.go +++ b/compiler/internal/dce/dce_test.go @@ -65,12 +65,13 @@ func Test_Collector_Collecting(t *testing.T) { depCount(t, decl1, 2) depCount(t, decl2, 3) - // The second collection overwrites the first collection. + // The second collection adds to existing dependencies. c.CollectDCEDeps(decl2, func() { + c.DeclareDCEDep(obj4) c.DeclareDCEDep(obj5) }) depCount(t, decl1, 2) - depCount(t, decl2, 1) + depCount(t, decl2, 4) } func Test_Info_SetNameAndDep(t *testing.T) { @@ -86,8 +87,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim import Sarah "fmt"`), want: Info{ - importPath: `jim`, - objectFilter: `Sarah`, + objectFilter: `jim.Sarah`, }, wantDep: `jim.Sarah`, }, @@ -97,8 +97,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim var Toby float64`), want: Info{ - importPath: `jim`, - objectFilter: `Toby`, + objectFilter: `jim.Toby`, }, wantDep: `jim.Toby`, }, @@ -108,8 +107,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim const Ludo int = 42`), want: Info{ - importPath: `jim`, - objectFilter: `Ludo`, + objectFilter: `jim.Ludo`, }, wantDep: `jim.Ludo`, }, @@ -126,8 +124,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { } }`), want: Info{ - importPath: `jim`, - objectFilter: `Gobo`, + objectFilter: `jim.Gobo`, }, wantDep: `jim.Gobo`, }, @@ -137,8 +134,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim type Jen struct{}`), want: Info{ - importPath: `jim`, - objectFilter: `Jen`, + objectFilter: `jim.Jen`, }, wantDep: `jim.Jen`, }, @@ -148,8 +144,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim type Henson[T comparable] struct{}`), want: Info{ - importPath: `jim`, - objectFilter: `Henson`, + objectFilter: `jim.Henson`, }, wantDep: `jim.Henson`, }, @@ -159,8 +154,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim func Jareth() {}`), want: Info{ - importPath: `jim`, - objectFilter: `Jareth`, + objectFilter: `jim.Jareth`, }, wantDep: `jim.Jareth`, }, @@ -170,8 +164,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim func Didymus[T comparable]() {}`), want: Info{ - importPath: `jim`, - objectFilter: `Didymus`, + objectFilter: `jim.Didymus`, }, wantDep: `jim.Didymus`, }, @@ -182,8 +175,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { type Fizzgig string func (f Fizzgig) Kira() {}`), want: Info{ - importPath: `jim`, - objectFilter: `Fizzgig`, + objectFilter: `jim.Fizzgig`, }, wantDep: `jim.Kira~`, }, @@ -194,9 +186,8 @@ func Test_Info_SetNameAndDep(t *testing.T) { type Aughra int func (a Aughra) frank() {}`), want: Info{ - importPath: `jim`, - objectFilter: `Aughra`, - methodFilter: `frank~`, + objectFilter: `jim.Aughra`, + methodFilter: `jim.frank~`, }, wantDep: `jim.frank~`, }, @@ -207,8 +198,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { type wembley struct{} func (w wembley) Red() {}`), want: Info{ - importPath: `jim`, - objectFilter: `wembley`, + objectFilter: `jim.wembley`, }, wantDep: `jim.Red~`, }, @@ -219,12 +209,11 @@ func Test_Info_SetNameAndDep(t *testing.T) { t.Run(tt.name, func(t *testing.T) { d := &testDecl{} equal(t, d.Dce().unnamed(), true) - equal(t, d.Dce().String(), `[unnamed] . -> []`) + equal(t, d.Dce().String(), `[unnamed] -> []`) t.Log(`object:`, types.ObjectString(tt.obj, nil)) d.Dce().SetName(tt.obj) equal(t, d.Dce().unnamed(), tt.want.unnamed()) - equal(t, d.Dce().importPath, tt.want.importPath) equal(t, d.Dce().objectFilter, tt.want.objectFilter) equal(t, d.Dce().methodFilter, tt.want.methodFilter) equal(t, d.Dce().String(), tt.want.String()) @@ -238,11 +227,17 @@ func Test_Info_SetNameAndDep(t *testing.T) { d := &testDecl{} t.Log(`object:`, types.ObjectString(tt.obj, nil)) - d.Dce().setDeps(map[types.Object]struct{}{ - tt.obj: {}, + wantDeps := []string{} + if len(tt.wantDep) > 0 { + wantDeps = append(wantDeps, tt.wantDep) + } + sort.Strings(wantDeps) + + c := Collector{} + c.CollectDCEDeps(d, func() { + c.DeclareDCEDep(tt.obj) }) - equal(t, len(d.Dce().deps), 1) - equal(t, d.Dce().deps[0], tt.wantDep) + equalSlices(t, d.Dce().getDeps(), wantDeps) }) } }) @@ -269,11 +264,11 @@ func Test_Info_SetAsAlive(t *testing.T) { obj := quickVar(pkg, `Falkor`) decl := &testDecl{} equal(t, decl.Dce().isAlive(), true) // unnamed is automatically alive - equal(t, decl.Dce().String(), `[unnamed] . -> []`) + equal(t, decl.Dce().String(), `[unnamed] -> []`) decl.Dce().SetAsAlive() equal(t, decl.Dce().isAlive(), true) // still alive but now explicitly alive - equal(t, decl.Dce().String(), `[alive] [unnamed] . -> []`) + equal(t, decl.Dce().String(), `[alive] [unnamed] -> []`) decl.Dce().SetName(obj) equal(t, decl.Dce().isAlive(), true) // alive because SetAsAlive was called @@ -284,7 +279,7 @@ func Test_Info_SetAsAlive(t *testing.T) { obj := quickVar(pkg, `Artax`) decl := &testDecl{} equal(t, decl.Dce().isAlive(), true) // unnamed is automatically alive - equal(t, decl.Dce().String(), `[unnamed] . -> []`) + equal(t, decl.Dce().String(), `[unnamed] -> []`) decl.Dce().SetName(obj) equal(t, decl.Dce().isAlive(), false) // named so no longer automatically alive @@ -493,6 +488,7 @@ func Test_Selector_SpecificMethods(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + vetinari.Dce().deps = nil // reset deps c.CollectDCEDeps(vetinari, func() { for _, decl := range tt.deps { c.DeclareDCEDep(decl.obj) @@ -626,6 +622,17 @@ func depCount(t *testing.T, decl *testDecl, want int) { func equal[T comparable](t *testing.T, got, want T) { t.Helper() if got != want { - t.Errorf(`expected %#v but got %#v`, want, got) + t.Errorf("Unexpected value was gotten:\n\texp: %#v\n\tgot: %#v", want, got) + } +} + +func equalSlices[T comparable](t *testing.T, got, want []T) { + t.Helper() + if len(got) != len(want) { + t.Errorf("expected %d but got %d\n\texp: %#v\n\tgot: %#v", len(want), len(got), want, got) + return + } + for i, wantElem := range want { + equal(t, got[i], wantElem) } } diff --git a/compiler/internal/dce/filters.go b/compiler/internal/dce/filters.go new file mode 100644 index 000000000..3d0e1eab5 --- /dev/null +++ b/compiler/internal/dce/filters.go @@ -0,0 +1,32 @@ +package dce + +import ( + "go/types" + + "github.com/gopherjs/gopherjs/compiler/typesutil" +) + +// getFilters determines the DCE filters for the given object. +// This will return an object filter and optionally return a method filter. +func getFilters(o types.Object) (objectFilter, methodFilter string) { + importPath := o.Pkg().Path() + if typesutil.IsMethod(o) { + recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj() + objectFilter = importPath + `.` + recv.Name() + if !o.Exported() { + methodFilter = importPath + `.` + o.Name() + `~` + } + } else { + objectFilter = importPath + `.` + o.Name() + } + return +} + +// getDepFilter returns the filter for the given object to be used as a dependency. +func getDepFilter(o types.Object) string { + qualifiedName := o.Pkg().Path() + "." + o.Name() + if typesutil.IsMethod(o) { + qualifiedName += "~" + } + return qualifiedName +} diff --git a/compiler/internal/dce/info.go b/compiler/internal/dce/info.go index d5993a659..c80ee8c99 100644 --- a/compiler/internal/dce/info.go +++ b/compiler/internal/dce/info.go @@ -5,8 +5,6 @@ import ( "go/types" "sort" "strings" - - "github.com/gopherjs/gopherjs/compiler/typesutil" ) // Info contains information used by the dead-code elimination (DCE) logic to @@ -17,21 +15,21 @@ type Info struct { // and will not be eliminated. alive bool - // importPath is the package path of the package the declaration is in. - importPath string - - // 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. + // objectFilter is the primary DCE name for a declaration. + // This will be the variable, function, or type identifier. + // For methods it is the receiver type identifier. + // If empty, the declaration is assumed to be alive. objectFilter string - // The second part of the identified used by dead-code elimination for methods. - // Empty for other types of symbols. + // methodFilter is the secondary DCE name for a declaration. + // This will be empty if objectFilter is empty. + // This will be set to a qualified method name if the objectFilter + // can not determine if the declaration is alive on it's own. methodFilter string - // List of fully qualified (including package path) DCE symbol identifiers the - // symbol depends on for dead code elimination purposes. - deps []string + // Set of fully qualified (including package path) DCE symbol + // and/or method names that this DCE declaration depends on. + deps map[string]struct{} } // String gets a human-readable representation of the DCE info. @@ -43,11 +41,14 @@ func (d *Info) String() string { if d.unnamed() { tags += `[unnamed] ` } - fullName := d.importPath + `.` + d.objectFilter + names := []string{} + if len(d.objectFilter) > 0 { + names = append(names, d.objectFilter+` `) + } if len(d.methodFilter) > 0 { - fullName += `.` + d.methodFilter + names = append(names, d.methodFilter+` `) } - return tags + fullName + ` -> [` + strings.Join(d.deps, `, `) + `]` + return tags + strings.Join(names, `& `) + `-> [` + strings.Join(d.getDeps(), `, `) + `]` } // unnamed returns true if SetName has not been called for this declaration. @@ -79,30 +80,35 @@ func (d *Info) SetName(o types.Object) { panic(fmt.Errorf(`may only set the name once for %s`, d.String())) } - d.importPath = o.Pkg().Path() - if typesutil.IsMethod(o) { - recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj() - d.objectFilter = recv.Name() - if !o.Exported() { - d.methodFilter = o.Name() + `~` - } - } else { - d.objectFilter = o.Name() - } + // Determine name(s) for DCE. + d.objectFilter, d.methodFilter = getFilters(o) } -// setDeps sets the declaration dependencies used by DCE +// addDep add a declaration dependencies used by DCE // for the declaration this DCE info is attached to. -// This overwrites any prior set dependencies. -func (d *Info) setDeps(objectSet map[types.Object]struct{}) { - deps := make([]string, 0, len(objectSet)) - for o := range objectSet { - qualifiedName := o.Pkg().Path() + "." + o.Name() - if typesutil.IsMethod(o) { - qualifiedName += "~" +func (d *Info) addDep(o types.Object) { + qualifiedName := getDepFilter(o) + d.addDepName(qualifiedName) +} + +// addDepName adds a declaration dependency by name. +func (d *Info) addDepName(depName string) { + if len(depName) > 0 { + if d.deps == nil { + d.deps = make(map[string]struct{}) } - deps = append(deps, qualifiedName) + d.deps[depName] = struct{}{} + } +} + +// getDeps gets the dependencies for the declaration sorted by name. +func (id *Info) getDeps() []string { + deps := make([]string, len(id.deps)) + i := 0 + for dep := range id.deps { + deps[i] = dep + i++ } sort.Strings(deps) - d.deps = deps + return deps } diff --git a/compiler/internal/dce/selector.go b/compiler/internal/dce/selector.go index 4eea572e0..3dff49028 100644 --- a/compiler/internal/dce/selector.go +++ b/compiler/internal/dce/selector.go @@ -42,12 +42,12 @@ func (s *Selector[D]) Include(decl D, implementsLink bool) { info := &declInfo[D]{decl: decl} if dce.objectFilter != `` { - info.objectFilter = dce.importPath + `.` + dce.objectFilter + info.objectFilter = dce.objectFilter s.byFilter[info.objectFilter] = append(s.byFilter[info.objectFilter], info) } if dce.methodFilter != `` { - info.methodFilter = dce.importPath + `.` + dce.methodFilter + info.methodFilter = dce.methodFilter s.byFilter[info.methodFilter] = append(s.byFilter[info.methodFilter], info) } } @@ -72,7 +72,7 @@ func (s *Selector[D]) AliveDecls() map[D]struct{} { // Consider all decls the current one is known to depend on and possible add // them to the live queue. - for _, dep := range dce.deps { + for _, dep := range dce.getDeps() { if infos, ok := s.byFilter[dep]; ok { delete(s.byFilter, dep) for _, info := range infos { From 2539cd522ce100850aa1871daac8bc1bfabaa7b2 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Wed, 9 Oct 2024 17:16:20 -0600 Subject: [PATCH 13/58] making requested changes to compiler tests --- compiler/compiler_test.go | 234 ++++++++++++------------------ internal/srctesting/srctesting.go | 74 ++++++++++ 2 files changed, 165 insertions(+), 143 deletions(-) diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index c17037e94..5276cbea5 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -2,9 +2,7 @@ package compiler import ( "bytes" - "fmt" "go/types" - "path/filepath" "regexp" "sort" "testing" @@ -13,38 +11,40 @@ import ( "golang.org/x/tools/go/packages" "github.com/gopherjs/gopherjs/compiler/internal/dce" + "github.com/gopherjs/gopherjs/internal/srctesting" ) func TestOrder(t *testing.T) { fileA := ` -package foo + package foo -var Avar = "a" + var Avar = "a" -type Atype struct{} + type Atype struct{} -func Afunc() int { - var varA = 1 - var varB = 2 - return varA+varB -} -` + func Afunc() int { + var varA = 1 + var varB = 2 + return varA+varB + }` fileB := ` -package foo + package foo -var Bvar = "b" + var Bvar = "b" -type Btype struct{} + type Btype struct{} -func Bfunc() int { - var varA = 1 - var varB = 2 - return varA+varB -} -` + func Bfunc() int { + var varA = 1 + var varB = 2 + return varA+varB + }` - files := []source{{"fileA.go", []byte(fileA)}, {"fileB.go", []byte(fileB)}} + files := []srctesting.Source{ + {Name: "fileA.go", Contents: []byte(fileA)}, + {Name: "fileB.go", Contents: []byte(fileB)}, + } compareOrder(t, files, false) compareOrder(t, files, true) @@ -64,7 +64,7 @@ func TestDeclSelection_KeepUnusedExportedMethods(t *testing.T) { Foo{}.Bar() }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) @@ -86,7 +86,7 @@ func TestDeclSelection_RemoveUnusedUnexportedMethods(t *testing.T) { Foo{}.Bar() }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) @@ -122,14 +122,14 @@ func TestDeclSelection_KeepUnusedUnexportedMethodForInterface(t *testing.T) { } }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) - // `baz` is used to duck-type (via method list) against IFoo - // but the method itself is not used so can be removed. + // `baz` signature metadata is used to check a type assertion against IFoo, + // but the method itself is never called, so it can be removed. sel.DeclCode.IsDead(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) sel.MethodListCode.IsAlive(`^\s*Foo.methods = .* \{prop: "baz", name: "baz"`) } @@ -152,7 +152,7 @@ func TestDeclSelection_KeepUnexportedMethodUsedViaInterfaceLit(t *testing.T) { f.baz() }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) @@ -172,7 +172,7 @@ func TestDeclSelection_KeepAliveUnexportedMethodsUsedInMethodExpressions(t *test fb(Foo{}) }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) @@ -196,7 +196,7 @@ func TestDeclSelection_RemoveUnusedFuncInstance(t *testing.T) { println(Sum(1.1, 2.2, 3.3)) }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Sum\[\d+ /\* float64 \*/\]`) @@ -205,7 +205,7 @@ func TestDeclSelection_RemoveUnusedFuncInstance(t *testing.T) { sel.DeclCode.IsDead(`^\s*Foo = function`) sel.DeclCode.IsDead(`^\s*sliceType(\$\d+)? = \$sliceType\(\$Int\)`) - // TODO(gn): This should not be alive because it is not used. + // TODO(grantnelson-wf): This should not be alive because it is not used. sel.DeclCode.IsAlive(`^\s*Sum\[\d+ /\* int \*/\]`) } @@ -223,13 +223,13 @@ func TestDeclSelection_RemoveUnusedStructTypeInstances(t *testing.T) { Foo[int]{v: 7}.Bar() }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\[\d+ /\* int \*/\]\)\.prototype\.Bar`) - // TODO(gn): This should not be alive because it is not used. + // TODO(grantnelson-wf): This should not be alive because it is not used. sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* float64 \*/\] = \$newType`) sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\[\d+ /\* float64 \*/\]\)\.prototype\.Bar`) } @@ -254,7 +254,7 @@ func TestDeclSelection_RemoveUnusedInterfaceTypeInstances(t *testing.T) { FooBar[int](Baz(42), 12) // Baz implements Foo[int] }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Baz = \$newType`) @@ -262,10 +262,10 @@ func TestDeclSelection_RemoveUnusedInterfaceTypeInstances(t *testing.T) { sel.InitCode.IsDead(`\$pkg\.F64 = FooBar\[\d+ /\* float64 \*/\]`) sel.DeclCode.IsAlive(`^\s*FooBar\[\d+ /\* int \*/\]`) - // TODO(gn): Below should be alive because it is an arg to FooBar[int]. + // TODO(grantnelson-wf): Below should be alive because it is an arg to FooBar[int]. sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) - // TODO(gn): Below should be dead because it is only used by a dead init. + // TODO(grantnelson-wf): Below should be dead because it is only used by a dead init. sel.DeclCode.IsAlive(`^\s*FooBar\[\d+ /\* float64 \*/\]`) sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* float64 \*/\] = \$newType`) } @@ -290,12 +290,12 @@ func TestDeclSelection_RemoveUnusedMethodWithDifferentSignature(t *testing.T) { f2.baz("foo") }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\)\.prototype\.Bar`) - // TODO(gn): Below should be dead because it is not used even though + // TODO(grantnelson-wf): Below should be dead because it is not used even though // its name matches a used unexported method. sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\)\.prototype\.baz`) @@ -323,7 +323,7 @@ func TestDeclSelection_RemoveUnusedUnexportedMethodInstance(t *testing.T) { f2.Bar() }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) @@ -334,7 +334,7 @@ func TestDeclSelection_RemoveUnusedUnexportedMethodInstance(t *testing.T) { sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* uint \*/\] = \$newType`) sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* uint \*/\]\)\.prototype\.Bar`) - // TODO(gn): All three below should be dead because Foo[uint].baz is unused. + // TODO(grantnelson-wf): All three below should be dead because Foo[uint].baz is unused. sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* uint \*/\]\)\.prototype\.baz`) sel.DeclCode.IsAlive(`^\s*Baz\[\d+ /\* uint \*/\] = \$newType`) sel.DeclCode.IsAlive(`\s*\$ptrType\(Baz\[\d+ /\* uint \*/\]\)\.prototype\.Bar`) @@ -354,7 +354,7 @@ func TestDeclSelection_RemoveUnusedTypeConstraint(t *testing.T) { println("do nothing") }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsDead(`^\s*Foo = \$newType`) @@ -363,7 +363,7 @@ func TestDeclSelection_RemoveUnusedTypeConstraint(t *testing.T) { sel.InitCode.IsDead(`ghost = new Bar\[\d+ /\* int \*/\]\.ptr\(7\)`) } -func compareOrder(t *testing.T, sourceFiles []source, minify bool) { +func compareOrder(t *testing.T, sourceFiles []srctesting.Source, minify bool) { t.Helper() outputNormal := compile(t, sourceFiles, minify) @@ -379,14 +379,9 @@ func compareOrder(t *testing.T, sourceFiles []source, minify bool) { } } -type source struct { - name string - contents []byte -} - -func compile(t *testing.T, sourceFiles []source, minify bool) []byte { +func compile(t *testing.T, sourceFiles []srctesting.Source, minify bool) []byte { t.Helper() - rootPkg := parseSources(t, sourceFiles, nil) + rootPkg := srctesting.ParseSources(t, sourceFiles, nil) archives := compileProject(t, rootPkg, minify) path := rootPkg.PkgPath @@ -402,71 +397,6 @@ func compile(t *testing.T, sourceFiles []source, minify bool) []byte { return b } -// parseSources parses the given source files and returns the root package -// that contains the given source files. -// -// The source file should all be from the same package as the files for the -// root package. At least one source file must be given. -// -// The auxillary files can be for different packages but should have paths -// added to the source name so that they can be grouped together by package. -// To import an auxillary package, the path should be prepended by -// `github.com/gopherjs/gopherjs/compiler`. -func parseSources(t *testing.T, sourceFiles []source, auxFiles []source) *packages.Package { - t.Helper() - const mode = packages.NeedName | - packages.NeedFiles | - packages.NeedImports | - packages.NeedDeps | - packages.NeedTypes | - packages.NeedSyntax - - dir, err := filepath.Abs(`./`) - if err != nil { - t.Fatal(`error getting working directory:`, err) - } - - patterns := make([]string, len(sourceFiles)) - overlay := make(map[string][]byte, len(sourceFiles)) - for i, src := range sourceFiles { - filename := src.name - patterns[i] = filename - absName := filepath.Join(dir, filename) - overlay[absName] = []byte(src.contents) - } - for _, src := range auxFiles { - absName := filepath.Join(dir, src.name) - overlay[absName] = []byte(src.contents) - } - - config := &packages.Config{ - Mode: mode, - Overlay: overlay, - Dir: dir, - } - - pkgs, err := packages.Load(config, patterns...) - if err != nil { - t.Fatal(`error loading packages:`, err) - } - - hasErrors := false - packages.Visit(pkgs, nil, func(pkg *packages.Package) { - for _, err := range pkg.Errors { - hasErrors = true - fmt.Println(err) - } - }) - if hasErrors { - t.FailNow() - } - - if len(pkgs) != 1 { - t.Fatal(`expected one and only one root package but got`, len(pkgs)) - } - return pkgs[0] -} - // compileProject compiles the given root package and all packages imported by the root. // This returns the compiled archives of all packages keyed by their import path. func compileProject(t *testing.T, root *packages.Package, minify bool) map[string]*Archive { @@ -537,9 +467,9 @@ type selectionTester struct { MethodListCode *selectionCodeTester } -func declSelection(t *testing.T, sourceFiles []source, auxFiles []source) *selectionTester { +func declSelection(t *testing.T, sourceFiles []srctesting.Source, auxFiles []srctesting.Source) *selectionTester { t.Helper() - root := parseSources(t, sourceFiles, auxFiles) + root := srctesting.ParseSources(t, sourceFiles, auxFiles) archives := compileProject(t, root, false) mainPkg := archives[root.PkgPath] @@ -548,7 +478,7 @@ func declSelection(t *testing.T, sourceFiles []source, auxFiles []source) *selec paths = append(paths, path) } sort.Strings(paths) - packages := make([]*Archive, 0, len(archives)-1) + packages := make([]*Archive, 0, len(archives)) for _, path := range paths { packages = append(packages, archives[path]) } @@ -561,87 +491,105 @@ func declSelection(t *testing.T, sourceFiles []source, auxFiles []source) *selec } dceSelection := sel.AliveDecls() - st := &selectionTester{ + return &selectionTester{ t: t, mainPkg: mainPkg, archives: archives, packages: packages, dceSelection: dceSelection, + DeclCode: &selectionCodeTester{ + t: t, + packages: packages, + dceSelection: dceSelection, + codeName: `DeclCode`, + getCode: func(d *Decl) []byte { return d.DeclCode }, + }, + InitCode: &selectionCodeTester{ + t: t, + packages: packages, + dceSelection: dceSelection, + codeName: `InitCode`, + getCode: func(d *Decl) []byte { return d.InitCode }, + }, + MethodListCode: &selectionCodeTester{ + t: t, + packages: packages, + dceSelection: dceSelection, + codeName: `MethodListCode`, + getCode: func(d *Decl) []byte { return d.MethodListCode }, + }, } - - st.DeclCode = &selectionCodeTester{st, `DeclCode`, func(d *Decl) []byte { return d.DeclCode }} - st.InitCode = &selectionCodeTester{st, `InitCode`, func(d *Decl) []byte { return d.InitCode }} - st.MethodListCode = &selectionCodeTester{st, `MethodListCode`, func(d *Decl) []byte { return d.MethodListCode }} - return st } func (st *selectionTester) PrintDeclStatus() { st.t.Helper() for _, pkg := range st.packages { - fmt.Println(`Package`, pkg.ImportPath) + st.t.Logf(`Package %s`, pkg.ImportPath) for _, decl := range pkg.Declarations { if _, ok := st.dceSelection[decl]; ok { - fmt.Printf(" [Alive] %q\n", string(decl.FullName)) + st.t.Logf(` [Alive] %q`, decl.FullName) } else { - fmt.Printf(" [Dead] %q\n", string(decl.FullName)) + st.t.Logf(` [Dead] %q`, decl.FullName) } if len(decl.DeclCode) > 0 { - fmt.Printf(" DeclCode: %q\n", string(decl.DeclCode)) + st.t.Logf(` DeclCode: %q`, string(decl.DeclCode)) } if len(decl.InitCode) > 0 { - fmt.Printf(" InitCode: %q\n", string(decl.InitCode)) + st.t.Logf(` InitCode: %q`, string(decl.InitCode)) } if len(decl.MethodListCode) > 0 { - fmt.Printf(" MethodListCode: %q\n", string(decl.MethodListCode)) + st.t.Logf(` MethodListCode: %q`, string(decl.MethodListCode)) } if len(decl.TypeInitCode) > 0 { - fmt.Printf(" TypeInitCode: %q\n", string(decl.TypeInitCode)) + st.t.Logf(` TypeInitCode: %q`, string(decl.TypeInitCode)) } if len(decl.Vars) > 0 { - fmt.Println(` Vars:`, decl.Vars) + st.t.Logf(` Vars: %v`, decl.Vars) } } } } type selectionCodeTester struct { - st *selectionTester - codeName string - getCode func(*Decl) []byte + t *testing.T + packages []*Archive + dceSelection map[*Decl]struct{} + codeName string + getCode func(*Decl) []byte } func (ct *selectionCodeTester) IsAlive(pattern string) { - ct.st.t.Helper() + ct.t.Helper() decl := ct.FindDeclMatch(pattern) - if _, ok := ct.st.dceSelection[decl]; !ok { - ct.st.t.Error(`expected the`, ct.codeName, `code to be alive:`, pattern) + if _, ok := ct.dceSelection[decl]; !ok { + ct.t.Error(`expected the`, ct.codeName, `code to be alive:`, pattern) } } func (ct *selectionCodeTester) IsDead(pattern string) { - ct.st.t.Helper() + ct.t.Helper() decl := ct.FindDeclMatch(pattern) - if _, ok := ct.st.dceSelection[decl]; ok { - ct.st.t.Error(`expected the`, ct.codeName, `code to be dead:`, pattern) + if _, ok := ct.dceSelection[decl]; ok { + ct.t.Error(`expected the`, ct.codeName, `code to be dead:`, pattern) } } func (ct *selectionCodeTester) FindDeclMatch(pattern string) *Decl { - ct.st.t.Helper() + ct.t.Helper() regex := regexp.MustCompile(pattern) var found *Decl - for _, pkg := range ct.st.packages { + for _, pkg := range ct.packages { for _, d := range pkg.Declarations { if regex.Match(ct.getCode(d)) { if found != nil { - ct.st.t.Fatal(`multiple`, ct.codeName, `found containing pattern:`, pattern) + ct.t.Fatal(`multiple`, ct.codeName, `found containing pattern:`, pattern) } found = d } } } if found == nil { - ct.st.t.Fatal(ct.codeName, `not found with pattern:`, pattern) + ct.t.Fatal(ct.codeName, `not found with pattern:`, pattern) } return found } diff --git a/internal/srctesting/srctesting.go b/internal/srctesting/srctesting.go index a74d31958..961dffd0b 100644 --- a/internal/srctesting/srctesting.go +++ b/internal/srctesting/srctesting.go @@ -10,8 +10,11 @@ import ( "go/parser" "go/token" "go/types" + "path/filepath" "strings" "testing" + + "golang.org/x/tools/go/packages" ) // Fixture provides utilities for parsing and type checking Go code in tests. @@ -171,3 +174,74 @@ func LookupObj(pkg *types.Package, name string) types.Object { } return obj } + +type Source struct { + Name string + Contents []byte +} + +// ParseSources parses the given source files and returns the root package +// that contains the given source files. +// +// The source file should all be from the same package as the files for the +// root package. At least one source file must be given. +// The root package's path will be `command-line-arguments`. +// +// The auxillary files can be for different packages but should have paths +// added to the source name so that they can be grouped together by package. +// To import an auxillary package, the path should be prepended by +// `github.com/gopherjs/gopherjs/compiler`. +func ParseSources(t *testing.T, sourceFiles []Source, auxFiles []Source) *packages.Package { + t.Helper() + const mode = packages.NeedName | + packages.NeedFiles | + packages.NeedImports | + packages.NeedDeps | + packages.NeedTypes | + packages.NeedSyntax + + dir, err := filepath.Abs(`./`) + if err != nil { + t.Fatal(`error getting working directory:`, err) + } + + patterns := make([]string, len(sourceFiles)) + overlay := make(map[string][]byte, len(sourceFiles)) + for i, src := range sourceFiles { + filename := src.Name + patterns[i] = filename + absName := filepath.Join(dir, filename) + overlay[absName] = []byte(src.Contents) + } + for _, src := range auxFiles { + absName := filepath.Join(dir, src.Name) + overlay[absName] = []byte(src.Contents) + } + + config := &packages.Config{ + Mode: mode, + Overlay: overlay, + Dir: dir, + } + + pkgs, err := packages.Load(config, patterns...) + if err != nil { + t.Fatal(`error loading packages:`, err) + } + + hasErrors := false + packages.Visit(pkgs, nil, func(pkg *packages.Package) { + for _, err := range pkg.Errors { + hasErrors = true + t.Error(err) + } + }) + if hasErrors { + t.FailNow() + } + + if len(pkgs) != 1 { + t.Fatal(`expected one and only one root package but got`, len(pkgs)) + } + return pkgs[0] +} From a07a61f1474f0cda456f2945bb15c8a8031f1181 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Fri, 11 Oct 2024 12:04:38 -0600 Subject: [PATCH 14/58] Adding more functions to InstanceMap --- compiler/internal/typeparams/map.go | 153 +++++++++++---- compiler/internal/typeparams/map_test.go | 228 ++++++++++++++++++++++- 2 files changed, 334 insertions(+), 47 deletions(-) diff --git a/compiler/internal/typeparams/map.go b/compiler/internal/typeparams/map.go index aa16130e2..f8aa93bf8 100644 --- a/compiler/internal/typeparams/map.go +++ b/compiler/internal/typeparams/map.go @@ -1,8 +1,10 @@ package typeparams import ( + "fmt" "go/types" - "sync" + "sort" + "strings" "golang.org/x/tools/go/types/typeutil" ) @@ -25,40 +27,35 @@ type ( // instance equality, objects are compared by pointer equality, and type // arguments with types.Identical(). To reduce access complexity, we bucket // entries by a combined hash of type args. This type is generally inspired by -// typeutil.Map. +// golang.org/x/tools/go/types/typeutil/map.go type InstanceMap[V any] struct { - bootstrap sync.Once - data map[types.Object]mapBuckets[V] - len int - hasher typeutil.Hasher - zero V + data map[types.Object]mapBuckets[V] + len int + hasher typeutil.Hasher } -func (im *InstanceMap[V]) init() { - im.bootstrap.Do(func() { - im.data = map[types.Object]mapBuckets[V]{} - im.hasher = typeutil.MakeHasher() - }) +// findIndex returns bucket and index of the entry with the given key. +// If the given key isn't found, an empty bucket and -1 are returned. +func (im *InstanceMap[V]) findIndex(key Instance) (mapBucket[V], int) { + if im != nil && im.data != nil { + bucket := im.data[key.Object][typeHash(im.hasher, key.TArgs...)] + for i, candidate := range bucket { + if candidate != nil && typeArgsEq(candidate.key.TArgs, key.TArgs) { + return bucket, i + } + } + } + return nil, -1 } +// get returns the stored value for the provided key and +// a bool indicating whether the key was present in the map or not. func (im *InstanceMap[V]) get(key Instance) (V, bool) { - im.init() - - buckets, ok := im.data[key.Object] - if !ok { - return im.zero, false - } - bucket := buckets[typeHash(im.hasher, key.TArgs...)] - if len(bucket) == 0 { - return im.zero, false - } - - for _, candidate := range bucket { - if typeArgsEq(candidate.key.TArgs, key.TArgs) { - return candidate.value, true - } + if bucket, i := im.findIndex(key); i >= 0 { + return bucket[i].value, true } - return im.zero, false + var zero V + return zero, false } // Get returns the stored value for the provided key. If the key is missing from @@ -76,8 +73,11 @@ func (im *InstanceMap[V]) Has(key Instance) bool { // Set new value for the key in the map. Returns the previous value that was // stored in the map, or zero value if the key wasn't present before. -func (im *InstanceMap[V]) Set(key Instance, value V) (old V) { - im.init() +func (im *InstanceMap[V]) Set(key Instance, value V) V { + if im.data == nil { + im.data = map[types.Object]mapBuckets[V]{} + im.hasher = typeutil.MakeHasher() + } if _, ok := im.data[key.Object]; !ok { im.data[key.Object] = mapBuckets[V]{} @@ -85,26 +85,99 @@ func (im *InstanceMap[V]) Set(key Instance, value V) (old V) { bucketID := typeHash(im.hasher, key.TArgs...) // If there is already an identical key in the map, override the entry value. - for _, candidate := range im.data[key.Object][bucketID] { - if typeArgsEq(candidate.key.TArgs, key.TArgs) { - old = candidate.value + hole := -1 + bucket := im.data[key.Object][bucketID] + for i, candidate := range bucket { + if candidate == nil { + hole = i + } else if typeArgsEq(candidate.key.TArgs, key.TArgs) { + old := candidate.value candidate.value = value return old } } - // Otherwise append a new entry. - im.data[key.Object][bucketID] = append(im.data[key.Object][bucketID], &mapEntry[V]{ - key: key, - value: value, - }) + // If there is a hole in the bucket, reuse it. + if hole >= 0 { + im.data[key.Object][bucketID][hole] = &mapEntry[V]{ + key: key, + value: value, + } + } else { + // Otherwise append a new entry. + im.data[key.Object][bucketID] = append(bucket, &mapEntry[V]{ + key: key, + value: value, + }) + } im.len++ - return im.zero + var zero V + return zero } // Len returns the number of elements in the map. func (im *InstanceMap[V]) Len() int { - return im.len + if im != nil { + return im.len + } + return 0 +} + +// Delete removes the entry with the given key, if any. +// It returns true if the entry was found. +func (im *InstanceMap[V]) Delete(key Instance) bool { + if bucket, i := im.findIndex(key); i >= 0 { + // We can't compact the bucket as it + // would disturb iterators. + bucket[i] = nil + im.len-- + return true + } + return false +} + +// Iterate calls function f on each entry in the map in unspecified order. +// +// Return true from f to continue the iteration, or false to stop it. +// +// If f should mutate the map, Iterate provides the same guarantees as +// Go maps: if f deletes a map entry that Iterate has not yet reached, +// f will not be invoked for it, but if f inserts a map entry that +// Iterate has not yet reached, whether or not f will be invoked for +// it is unspecified. +func (im *InstanceMap[V]) Iterate(f func(key Instance, value V)) { + if im != nil && im.data != nil { + for _, mapBucket := range im.data { + for _, bucket := range mapBucket { + for _, e := range bucket { + if e != nil { + f(e.key, e.value) + } + } + } + } + } +} + +// Keys returns a new slice containing the set of map keys. +// The order is unspecified. +func (im *InstanceMap[V]) Keys() []Instance { + keys := make([]Instance, 0, im.Len()) + im.Iterate(func(key Instance, _ V) { + keys = append(keys, key) + }) + return keys +} + +// String returns a string representation of the map's entries. +// The entries are sorted by string representation of the entry. +func (im *InstanceMap[V]) String() string { + entries := make([]string, 0, im.Len()) + im.Iterate(func(key Instance, value V) { + entries = append(entries, fmt.Sprintf("%v:%v", key, value)) + }) + sort.Strings(entries) + return `{` + strings.Join(entries, `, `) + `}` } // typeHash returns a combined hash of several types. diff --git a/compiler/internal/typeparams/map_test.go b/compiler/internal/typeparams/map_test.go index 5018ab0d8..ed65587c7 100644 --- a/compiler/internal/typeparams/map_test.go +++ b/compiler/internal/typeparams/map_test.go @@ -31,18 +31,30 @@ func TestInstanceMap(t *testing.T) { } i3 := Instance{ Object: i1.Object, - TArgs: []types.Type{types.Typ[types.String]}, // Different type args. + TArgs: []types.Type{ // Different type args, same number. + types.Typ[types.Int], + types.Typ[types.Int], + }, + } + i4 := Instance{ + Object: i1.Object, + TArgs: []types.Type{ // This hash matches i3's hash. + types.Typ[types.String], + types.Typ[types.String], + }, + } + i5 := Instance{ + Object: i1.Object, + TArgs: []types.Type{}, // This hash matches i3's hash. } - - _ = i1 - _ = i1clone - _ = i3 - _ = i2 m := InstanceMap[string]{} // Check operations on a missing key. t.Run("empty", func(t *testing.T) { + if got, want := m.String(), `{}`; got != want { + t.Errorf("Got: empty map string %q. Want: map string %q.", got, want) + } if got := m.Has(i1); got { t.Errorf("Got: empty map contains %s. Want: empty map contains nothing.", i1) } @@ -58,6 +70,12 @@ func TestInstanceMap(t *testing.T) { if got := m.Len(); got != 1 { t.Errorf("Got: map length %d. Want: 1.", got) } + if got, want := m.String(), `{{type i1 int, int8}:abc}`; got != want { + t.Errorf("Got: map string %q. Want: map string %q.", got, want) + } + if got, want := m.Keys(), []Instance{i1}; !keysMatch(got, want) { + t.Errorf("Got: map keys %v. Want: [i1].", got) + } }) // Check operations on the existing key. @@ -77,9 +95,15 @@ func TestInstanceMap(t *testing.T) { if got := m.Get(i1clone); got != "def" { t.Errorf(`Got: getting set key returned %q. Want: "def"`, got) } + if got, want := m.String(), `{{type i1 int, int8}:def}`; got != want { + t.Errorf("Got: map string %q. Want: map string %q.", got, want) + } + if got, want := m.Keys(), []Instance{i1}; !keysMatch(got, want) { + t.Errorf("Got: map keys %v. Want: [i1].", got) + } }) - // Check for key collisions. + // Check for key collisions with different object pointer. t.Run("different object", func(t *testing.T) { if got := m.Has(i2); got { t.Errorf("Got: a new key %q is reported as present. Want: not present.", i2) @@ -94,6 +118,8 @@ func TestInstanceMap(t *testing.T) { t.Errorf("Got: map length %d. Want: 2.", got) } }) + + // Check for collisions with different type arguments and different hash. t.Run("different tArgs", func(t *testing.T) { if got := m.Has(i3); got { t.Errorf("Got: a new key %q is reported as present. Want: not present.", i3) @@ -108,4 +134,192 @@ func TestInstanceMap(t *testing.T) { t.Errorf("Got: map length %d. Want: 3.", got) } }) + + // Check for collisions with different type arguments, same hash, count. + t.Run("different tArgs hash", func(t *testing.T) { + if got := m.Has(i4); got { + t.Errorf("Got: a new key %q is reported as present. Want: not present.", i3) + } + if got := m.Set(i4, "789"); got != "" { + t.Errorf("Got: a new key %q overrode an old value %q. Want: zero value.", i3, got) + } + if got := m.Get(i4); got != "789" { + t.Errorf(`Got: getting set key %q returned: %q. Want: "789"`, i3, got) + } + if got := m.Len(); got != 4 { + t.Errorf("Got: map length %d. Want: 4.", got) + } + }) + + // Check for collisions with different type arguments and same hash, but different count. + t.Run("different tArgs count", func(t *testing.T) { + if got := m.Has(i5); got { + t.Errorf("Got: a new key %q is reported as present. Want: not present.", i3) + } + if got := m.Set(i5, "ghi"); got != "" { + t.Errorf("Got: a new key %q overrode an old value %q. Want: zero value.", i3, got) + } + if got := m.Get(i5); got != "ghi" { + t.Errorf(`Got: getting set key %q returned: %q. Want: "ghi"`, i3, got) + } + if got := m.Len(); got != 5 { + t.Errorf("Got: map length %d. Want: 5.", got) + } + if got, want := m.String(), `{{type i1 int, int8}:def, {type i1 int, int}:456, {type i1 string, string}:789, {type i1 }:ghi, {type i2 int, int8}:123}`; got != want { + t.Errorf("Got: map string %q. Want: map string %q.", got, want) + } + if got, want := m.Keys(), []Instance{i1, i2, i3, i4, i5}; !keysMatch(got, want) { + t.Errorf("Got: map keys %v. Want: [i1, i2, i3, i4, i5].", got) + } + }) + + // Check an existing entry can be deleted. + t.Run("delete existing", func(t *testing.T) { + if got := m.Delete(i3); !got { + t.Errorf("Got: deleting existing key %q returned not deleted. Want: found and deleted.", i3) + } + if got := m.Len(); got != 4 { + t.Errorf("Got: map length %d. Want: 4.", got) + } + if got := m.Has(i3); got { + t.Errorf("Got: a deleted key %q is reported as present. Want: not present.", i3) + } + if got, want := m.Keys(), []Instance{i1, i2, i4, i5}; !keysMatch(got, want) { + t.Errorf("Got: map keys %v. Want: [i1, i2, i4, i5].", got) + } + }) + + // Check deleting an existing entry has no effect. + t.Run("delete already deleted", func(t *testing.T) { + if got := m.Delete(i3); got { + t.Errorf("Got: deleting not present key %q returned as deleted. Want: not found.", i3) + } + if got := m.Len(); got != 4 { + t.Errorf("Got: map length %d. Want: 4.", got) + } + if got, want := m.Keys(), []Instance{i1, i2, i4, i5}; !keysMatch(got, want) { + t.Errorf("Got: map keys %v. Want: [i1, i2, i4, i5].", got) + } + }) + + // Check adding back a deleted value works (should fill hole in bucket). + t.Run("set deleted key", func(t *testing.T) { + if got := m.Set(i3, "jkl"); got != "" { + t.Errorf("Got: a new key %q overrode an old value %q. Want: zero value.", i3, got) + } + if got := m.Len(); got != 5 { + t.Errorf("Got: map length %d. Want: 5.", got) + } + if got, want := m.Keys(), []Instance{i1, i2, i3, i4, i5}; !keysMatch(got, want) { + t.Errorf("Got: map keys %v. Want: [i1, i2, i3, i4, i5].", got) + } + }) + + // Check deleting while iterating over the map. + t.Run("deleting while iterating", func(t *testing.T) { + notSeen := []Instance{i1, i2, i3, i4, i5} + seen := []Instance{} + kept := []Instance{} + var skipped Instance + m.Iterate(func(key Instance, value string) { + // update seen and not seen + seen = append(seen, key) + i := keyAt(notSeen, key) + if i < 0 { + t.Fatalf(`Got: failed to find current key %q in not seen. Want: it to be not seen yet.`, key) + } + notSeen = append(notSeen[:i], notSeen[i+1:]...) + + if len(seen) == 3 { + // delete the first seen key, the current key, and an unseen key + if got := m.Delete(seen[0]); !got { + t.Errorf("Got: deleting seen key %q returned not deleted. Want: found and deleted.", seen[0]) + } + if got := m.Delete(key); !got { + t.Errorf("Got: deleting current key %q returned not deleted. Want: found and deleted.", key) + } + skipped = notSeen[0] // skipped has not yet been seen so it should not be iterated over + if got := m.Delete(skipped); !got { + t.Errorf("Got: deleting not seen key %q returned not deleted. Want: found and deleted.", skipped) + } + kept = append(kept, seen[1], notSeen[1]) + } + }) + + if got := len(seen); got != 4 { + t.Errorf("Got: seen %d keys. Want: 4.", got) + } + if got := len(notSeen); got != 1 { + t.Errorf("Got: seen %d keys. Want: 1.", got) + } + if got := keyAt(notSeen, skipped); got != 0 { + t.Errorf("Got: a deleted unseen key %q was not the skipped key %q. Want: it to be skipped.", notSeen[0], skipped) + } + if got := m.Len(); got != 2 { + t.Errorf("Got: map length %d. Want: 2.", got) + } + if got := m.Keys(); !keysMatch(got, kept) { + t.Errorf("Got: map keys %v did not match kept keys. Want: %v.", got, kept) + } + }) +} + +func TestNilInstanceMap(t *testing.T) { + i1 := Instance{ + Object: types.NewTypeName(token.NoPos, nil, "i1", nil), + TArgs: []types.Type{ + types.Typ[types.Int], + types.Typ[types.Int8], + }, + } + + var m *InstanceMap[string] + if got, want := m.String(), `{}`; got != want { + t.Errorf("Got: nil map string %q. Want: map string %q.", got, want) + } + if got := m.Has(i1); got { + t.Errorf("Got: nil map contains %s. Want: nil map contains nothing.", i1) + } + if got := m.Get(i1); got != "" { + t.Errorf("Got: missing key returned %q. Want: zero value.", got) + } + if got := m.Len(); got != 0 { + t.Errorf("Got: nil map length %d. Want: 0.", got) + } + if got := m.Keys(); len(got) > 0 { + t.Errorf("Got: map keys %v did not match kept keys. Want: [].", got) + } + + // The only thing that a nil map can't safely handle is setting a key. + func() { + defer func() { + recover() + }() + m.Set(i1, "abc") + t.Errorf("Got: setting a new key on nil map did not panic, %s. Want: panic.", m.String()) + }() +} + +func keysMatch(a, b []Instance) bool { + if len(a) != len(b) { + return false + } + found := make([]bool, len(b)) + for _, v := range a { + i := keyAt(b, v) + if i < 0 || found[i] { + return false + } + found[i] = true + } + return true +} + +func keyAt(keys []Instance, target Instance) int { + for i, v := range keys { + if v.Object == target.Object && typeArgsEq(v.TArgs, target.TArgs) { + return i + } + } + return -1 } From 91464467b7099fd5da9b5a67d3066b5d23aca0d5 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Mon, 14 Oct 2024 09:45:53 -0600 Subject: [PATCH 15/58] fixing godoc link --- compiler/internal/typeparams/map.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/internal/typeparams/map.go b/compiler/internal/typeparams/map.go index f8aa93bf8..4f6645421 100644 --- a/compiler/internal/typeparams/map.go +++ b/compiler/internal/typeparams/map.go @@ -27,7 +27,7 @@ type ( // instance equality, objects are compared by pointer equality, and type // arguments with types.Identical(). To reduce access complexity, we bucket // entries by a combined hash of type args. This type is generally inspired by -// golang.org/x/tools/go/types/typeutil/map.go +// [golang.org/x/tools/go/types/typeutil#Map] type InstanceMap[V any] struct { data map[types.Object]mapBuckets[V] len int From 4d87a0b1d322ab5fa074848c52bae58d459bb4b1 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 17 Oct 2024 08:58:29 -0600 Subject: [PATCH 16/58] Fixing grammar in readme --- compiler/internal/dce/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/internal/dce/README.md b/compiler/internal/dce/README.md index db6408d62..515c0190d 100644 --- a/compiler/internal/dce/README.md +++ b/compiler/internal/dce/README.md @@ -98,7 +98,7 @@ Since there are many ways to wrap a type with an interface, any alive type that duck-types to an interface must have all of the matching methods also alive. In theory the unexported functions are also alive however, for GopherJS there -is an exception because duck-typing is handled separately the method +is an exception because duck-typing is handled separately from the method definitions. Those difference are discussed in [Dependencies](#dependencies) but for this idea we discuss DCE more generally. From 46f06c2825f536cc6d8a20bd55ae52dee9d5f86e Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 17 Oct 2024 11:59:04 -0600 Subject: [PATCH 17/58] Fixing grammar in readme --- compiler/internal/dce/README.md | 87 ++++++++++++++++++-------------- compiler/internal/dce/filters.go | 14 ++--- 2 files changed, 55 insertions(+), 46 deletions(-) diff --git a/compiler/internal/dce/README.md b/compiler/internal/dce/README.md index 515c0190d..01ec1e8c6 100644 --- a/compiler/internal/dce/README.md +++ b/compiler/internal/dce/README.md @@ -8,7 +8,7 @@ something alive, is also considered alive. Once all dependencies are taken into consideration we have the set of alive declarations. Anything not considered alive is considered dead and -may be safely eliminated, i.e. not outputted to the JS file(s). +may be safely eliminated, i.e. not outputted to JS. - [Idea](#idea) - [Package](#package) @@ -123,7 +123,13 @@ and therefore `Foo.y()` can not be called via a `Bar.y()`. We will try to reduce the false positives in alive unexported methods by using the parameter and result types of the methods. Meaning that - `y()`, `y(int)`, `y() int`, etc won't match just because they are named `y`. +`y()`, `y(int)`, `y() int`, etc won't match just because they are named `y`. +This also helps with a generic type's unexported methods that use +type parameters, e.g. `Foo.y(T)`. Since the generic type may be instantiated +with `int` and `string`, the different instances of the method are `Foo.y(int)` +and `Foo.y(string)`. By using the parameter and result types, it is possible +to remove the unused unexported method instantiations even when some +instantiations of the same method are used. ### Functions @@ -156,19 +162,19 @@ For functions and types with generics, the definitions are split into unique instances. For example, `type StringKeys[T any] map[string]T` could be used in code as `StringKeys[int]` and `StringKeys[*Cat]`. We don't need all possible instances, only the ones which are realized -in code. Each instance depends on the realized parameter types (instance types). -In the example the instance types are `int` and `*Cat`. +in code. Each instance depends on the realized parameter types (type arguments). +In the example the type arguments are `int` and `*Cat`. The instance of the generic type also defines the code with the specific -instance types (e.g. `map[string]int` and `map[string]*Cat`). When an +type arguments (e.g. `map[string]int` and `map[string]*Cat`). When an instance is depended on by alive code, only that instance is alive, not the entire generic type. This means if `StringKey[*Cat]` is only used from dead -code, it is also dead and can be safely eliminated. +code then it is also dead and can be safely eliminated. The named generic types may have methods that are also copied for an instance -with the parameter types replaced by the instance types. For example, +with the parameter types replaced by the type arguments. For example, `func (sk StringKeys[T]) values() []T { ... }` becomes -`func (sk StringKeys[int]) values() []int { ... }` when the instance type +`func (sk StringKeys[int]) values() []int { ... }` when the type argument is `int`. This method in the instance now duck-types to `interface { values() []int }` and therefore must follow the rules for unexported methods. @@ -176,7 +182,7 @@ See [Instance Duck-typing](#instance-duck-typing) example for more information. Functions and named types may be generic, but methods and unnamed types may not be. This makes somethings simpler. A method with a receiver is used, -only the receiver's instance types are needed. The generic type or function +only the receiver's type arguments are needed. The generic type or function may not be needed since only the instances are written out. This also means that inside of a generic function or named type there is only @@ -190,12 +196,12 @@ than languages that allow a method of an object to have it's own type parameters, e.g. `class X { void Y() { ... } ... }`. However, generics mean that the same method, receiver, type, etc names -will be used with different parameters types caused by different instance -types. The instance types are the type arguments being passed into those -parameter types for a specific instance. +will be used with different parameters types caused by different type +arguments. The type arguments are being passed into those parameter types +for a specific instance. When an interface is alive, the signatures for unexported methods need to be instantiated with type arguments so that we know which instances -the interface is duck-typing to. +the interface is duck-typing to. See [Interfaces](#interfaces) for more detail. ### Links @@ -227,7 +233,7 @@ simplifying the justifications down to a simple set of rules. - The `init` in every included file - Any variable initialization that has a side effect - Any linked function or variable -- Anything not named +- Anything not given a DCE named, e.g. packages ### Naming @@ -256,7 +262,7 @@ To simplify the above for GopherJS, we don't look at the receiver for an unexported method before indicating it is alive. Meaning if there is no interface, only two named objects with identical unexported methods, the use of either will indicate a use of both. This will cause slightly more unexported -method to be alive while reducing the complication of type checking which object +methods to be alive while reducing the complication of type checking which object or type of object is performing the call. | Declaration | exported | unexported | non-generic | generic | object name | method name | @@ -284,23 +290,25 @@ and be eliminated causing the application to not run. `.`, `.`, `.` and `.` all have the same form. They are -the package path, if there is one, followed by a `.` and the object name -or receiver name. For example [`rand.Shuffle`](https://pkg.go.dev/math/rand@go1.23.1#Shuffle) +the package path followed by a `.`, if there is a package path, +and the object name or receiver name. +For example [`rand.Shuffle`](https://pkg.go.dev/math/rand@go1.23.1#Shuffle) will be named `math/rand.Shuffle`. The builtin [`error`](https://pkg.go.dev/builtin@go1.23.1#error) will be named `error` without a package path. `.[]`, `.[]`, and `.[]` are the same as above -except with comma separated type arguments in square brackets. -The type arguments are either the instance types, or type parameters -since the instance type could be a match for the type parameter on the -generic. For example `type Foo[T any] struct{}; type Bar[B any] { f Foo[B] }` +except with comma separated type parameters or type arguments in square brackets. +The type parameter names are not used, instead the constraint types are since +the names for type parameters may not match even if the constraints match. +For example `type Foo[T any] struct{}; type Bar[B any] { f Foo[B] }` has `Foo[B]` used in `Bar` that is identical to `Foo[T]` even though -technically `Foo[B]` is an instance of `Foo[T]` with `B` as the type argument. +technically `Foo[B]` is an instance of `Foo[T]` with the `B` type parameter +as the type argument. Command compiles, i.e. compiles with a `main` entry point, and test builds -should not have any instance types that aren't resolved to concrete types, -however to handle partial compiles of packages, instance types may still +should not have any type parameters that aren't resolved to concrete types, +however to handle partial compiles of packages, there may still be a type parameter, including unions of approximate constraints, i.e. `~int|~string`. @@ -321,12 +329,12 @@ For the method name of unexposed methods, The rest contains the signature, `()()`. The signature is defined with only the types since `(v, u int)(ok bool, err error)` should match `(x, y int)(bool, error)`. -To match both will have to be `(int, int)(bool, error)`. +To match both, both will have to be `(int, int)(bool, error)`. Also the parameter types should include the veridic indicator, -e.g. `sum(...int)int`, since that affects how the signature is matched. +e.g. `sum(...int) int`, since that affects how the signature is matched. If there are no results then the results part is left off. Otherwise, the result types only need parenthesis if there are more than one result, -e.g. `(int, int)`, `(int, int)bool`, and `(int, int)(bool, error)`. +e.g. `(int, int)`, `(int, int) bool`, and `(int, int)(bool, error)`. In either the object name or method name, if there is a recursive type parameter, e.g. `func Foo[T Bar[T]]()` the second usage of the @@ -336,7 +344,7 @@ is recursive, e.g. `Foo[Bar[Bar[...]]]`. ### Dependencies -The dependencies that are specified in an expression. +The dependencies are specified in an expression. For example a function that invokes another function will be dependent on that invoked function. When a dependency is added it will be added as one or more names to the declaration that depends on it. It follows the @@ -348,7 +356,7 @@ it would automatically add all unexported signatures as dependencies via `.()()`. However, we do not need to do that in GopherJS because we aren't using the existence of realized methods in duck-typing. GopherJS stores full set -of method information when describing the type so that even when things like +of method information when describing the type so that, even when things like unexported methods in interfaces are removed, duck-typing will still work correctly. This reduces the size of the code by not keeping a potentially long method body when the signature is all that is needed. @@ -419,7 +427,7 @@ import "point" func main() { a := point.Point{X: 10.2, Y: 45.3} b := point.Point{X: -23.0, Y: 7.7} - println(`Manhatten a to b:`, a.Manhattan(b)) + println(`Manhattan a to b:`, a.Manhattan(b)) } ``` @@ -476,7 +484,7 @@ func main() { ### Side Effects In this example unused variables are being initialized with expressions -that have side effects. The `max` value is 8 by the time `main` is called +that has side effects. The `max` value is 8 by the time `main` is called because each initialization calls `count()` that increments `max`. The expression doesn't have to have a function call and can be any combination of operations. @@ -512,17 +520,17 @@ func main() { In this example the type `StringKeys[T any]` is a map that stores any kind of value with string keys. There is an interface `IntProvider` -that `StringKeys` will duck-type to iff the instance type is `int`, -i.e. `StringKeys[int]`. This exemplifies how the instance types used +that `StringKeys` will duck-type to iff the type argument is `int`, +i.e. `StringKeys[int]`. This exemplifies how the type arguments used in the type arguments affect the overall signature such that in some cases a generic object may match an interface and in others it may not. Also notice that the structure was typed with `T` as the parameter type's name whereas the methods use `S`. This shows that the name of the type doesn't matter in the instancing. Therefore, outputting a methods name -(assuming it is unexported) should use the instance type not the parameter -name, e.g. `value() []int` or `value() []any` instead of `value() []S` or -`value() []T`. +(assuming it is unexported) should use the type argument type, +not the type parameter name, e.g. `value() []int` or `value() []any` +instead of `value() []S` or `value() []T`. ```go package main @@ -606,9 +614,10 @@ the higher level constructs not being used. Any variable internal to the body of a function or method that is unused or only used for computing new values for itself, are left as is. The Go compiler and linters have requirements that attempt to prevent this -kind of dead-code in a function body (so long as an underscore isn't used to quite -usage warnings) and prevent unreachable code. Therefore, we aren't going to -worry about trying to DCE inside of function bodies or in variable initializers. +kind of dead-code in a function body (unless an underscore is used to quite +usage warnings, e.g. `_ = unusedVar`) and prevent unreachable code. +Therefore, we aren't going to worry about trying to DCE inside of function +bodies or in variable initializers. GopherJS does not implicitly perform JS Tree Shaking Algorithms, as discussed in [How Modern Javascript eliminate dead code](https://blog.stackademic.com/how-modern-javascript-eliminates-dead-code-tree-shaking-algorithm-d7861e48df40) diff --git a/compiler/internal/dce/filters.go b/compiler/internal/dce/filters.go index 1994ded75..58768ff39 100644 --- a/compiler/internal/dce/filters.go +++ b/compiler/internal/dce/filters.go @@ -82,7 +82,7 @@ func objectName(o types.Object) string { } // getTypeArgs gets the type arguments for the given type -// wether they are instance types or type parameters. +// wether they are type arguments or type parameters. func getTypeArgs(typ types.Type) []types.Type { switch t := typ.(type) { case *types.Pointer: @@ -105,11 +105,11 @@ func getTypeArgs(typ types.Type) []types.Type { return nil } -// typeListToSlice returns the list of type arguments for the instance types. -func typeListToSlice(instTypes *types.TypeList) []types.Type { - tArgs := make([]types.Type, instTypes.Len()) +// typeListToSlice returns the list of type arguments for the type arguments. +func typeListToSlice(typeArgs *types.TypeList) []types.Type { + tArgs := make([]types.Type, typeArgs.Len()) for i := range tArgs { - tArgs[i] = instTypes.At(i) + tArgs[i] = typeArgs.At(i) } return tArgs } @@ -141,9 +141,9 @@ func (p processingGroup) is(o types.Object, tArgs []types.Type) bool { } type filterGen struct { - // argTypeRemap is the instance types in the same order as the + // argTypeRemap is the type arguments in the same order as the // type parameters in the top level object such that the type parameters - // index can be used to get the instance type. + // index can be used to get the type argument. argTypeRemap []types.Type inProgress []processingGroup } From 04193e9756171278c6bfaaebb595a62ba68f7c56 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Tue, 22 Oct 2024 15:39:06 -0600 Subject: [PATCH 18/58] Move analysis package into internal --- compiler/decls.go | 2 +- compiler/expressions.go | 2 +- compiler/functions.go | 2 +- compiler/{ => internal}/analysis/bool.go | 0 compiler/{ => internal}/analysis/break.go | 0 compiler/{ => internal}/analysis/escape.go | 0 compiler/{ => internal}/analysis/info.go | 0 compiler/{ => internal}/analysis/info_test.go | 0 compiler/{ => internal}/analysis/sideeffect.go | 0 compiler/package.go | 2 +- compiler/statements.go | 2 +- compiler/utils.go | 2 +- 12 files changed, 6 insertions(+), 6 deletions(-) rename compiler/{ => internal}/analysis/bool.go (100%) rename compiler/{ => internal}/analysis/break.go (100%) rename compiler/{ => internal}/analysis/escape.go (100%) rename compiler/{ => internal}/analysis/info.go (100%) rename compiler/{ => internal}/analysis/info_test.go (100%) rename compiler/{ => internal}/analysis/sideeffect.go (100%) diff --git a/compiler/decls.go b/compiler/decls.go index b6427a697..dbd47b6d8 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -12,7 +12,7 @@ import ( "sort" "strings" - "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" diff --git a/compiler/expressions.go b/compiler/expressions.go index dcf1b7844..e2234275d 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -11,8 +11,8 @@ import ( "strconv" "strings" - "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) diff --git a/compiler/functions.go b/compiler/functions.go index 31a9974eb..1641174b4 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -12,8 +12,8 @@ import ( "sort" "strings" - "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) diff --git a/compiler/analysis/bool.go b/compiler/internal/analysis/bool.go similarity index 100% rename from compiler/analysis/bool.go rename to compiler/internal/analysis/bool.go diff --git a/compiler/analysis/break.go b/compiler/internal/analysis/break.go similarity index 100% rename from compiler/analysis/break.go rename to compiler/internal/analysis/break.go diff --git a/compiler/analysis/escape.go b/compiler/internal/analysis/escape.go similarity index 100% rename from compiler/analysis/escape.go rename to compiler/internal/analysis/escape.go diff --git a/compiler/analysis/info.go b/compiler/internal/analysis/info.go similarity index 100% rename from compiler/analysis/info.go rename to compiler/internal/analysis/info.go diff --git a/compiler/analysis/info_test.go b/compiler/internal/analysis/info_test.go similarity index 100% rename from compiler/analysis/info_test.go rename to compiler/internal/analysis/info_test.go diff --git a/compiler/analysis/sideeffect.go b/compiler/internal/analysis/sideeffect.go similarity index 100% rename from compiler/analysis/sideeffect.go rename to compiler/internal/analysis/sideeffect.go diff --git a/compiler/package.go b/compiler/package.go index 430542b65..9fcf9d0b0 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" diff --git a/compiler/statements.go b/compiler/statements.go index d4ca76471..495cbcd1a 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -9,9 +9,9 @@ import ( "go/types" "strings" - "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" "github.com/gopherjs/gopherjs/compiler/filter" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/typesutil" ) diff --git a/compiler/utils.go b/compiler/utils.go index a69d0fe77..8404c8ef4 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -18,7 +18,7 @@ import ( "text/template" "unicode" - "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) From 948c73a9586be2016b25082738c70e3a8448b1eb Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Tue, 22 Oct 2024 15:53:26 -0600 Subject: [PATCH 19/58] Update function info blocking to use instances --- compiler/decls.go | 6 +- compiler/functions.go | 14 +- compiler/internal/analysis/info.go | 207 +++- compiler/internal/analysis/info_test.go | 1252 +++++++++++++++++++++- compiler/internal/typeparams/instance.go | 12 + compiler/package.go | 15 +- internal/srctesting/srctesting.go | 2 +- 7 files changed, 1415 insertions(+), 93 deletions(-) diff --git a/compiler/decls.go b/compiler/decls.go index dbd47b6d8..37fdcca6e 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -317,7 +317,7 @@ func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance) o := fc.pkgCtx.Defs[fun.Name].(*types.Func) d := &Decl{ FullName: o.FullName(), - Blocking: fc.pkgCtx.IsBlocking(o), + Blocking: fc.pkgCtx.IsBlocking(inst), LinkingName: symbol.New(o), } d.Dce().SetName(o) @@ -349,7 +349,7 @@ func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance) func (fc *funcContext) callInitFunc(init *types.Func) ast.Stmt { id := fc.newIdentFor(init) call := &ast.CallExpr{Fun: id} - if fc.pkgCtx.IsBlocking(init) { + if fc.pkgCtx.IsBlocking(typeparams.Instance{Object: init}) { fc.Blocking[call] = true } return &ast.ExprStmt{X: call} @@ -373,7 +373,7 @@ func (fc *funcContext) callMainFunc(main *types.Func) ast.Stmt { }, }, } - if fc.pkgCtx.IsBlocking(main) { + if fc.pkgCtx.IsBlocking(typeparams.Instance{Object: main}) { fc.Blocking[call] = true fc.Flattened[ifStmt] = true } diff --git a/compiler/functions.go b/compiler/functions.go index 1641174b4..657bf9926 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -72,7 +72,7 @@ func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, inst typep // 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)] + info := fc.pkgCtx.FuncInfo(inst) c := fc.nestedFunctionContext(info, inst) return c @@ -82,7 +82,7 @@ func (fc *funcContext) namedFuncContext(inst typeparams.Instance) *funcContext { // 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] + info := fc.pkgCtx.FuncLitInfo(fun) sig := fc.pkgCtx.TypeOf(fun).(*types.Signature) o := types.NewFunc(fun.Pos(), fc.pkgCtx.Pkg, fc.newLitFuncName(), sig) inst := typeparams.Instance{Object: o} @@ -237,7 +237,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, } bodyOutput := string(fc.CatchOutput(1, func() { - if len(fc.Blocking) != 0 { + if fc.HasBlocking() { fc.pkgCtx.Scopes[body] = fc.pkgCtx.Scopes[typ] fc.handleEscapingVars(body) } @@ -283,14 +283,14 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.HasDefer { fc.localVars = append(fc.localVars, "$deferred") suffix = " }" + suffix - if len(fc.Blocking) != 0 { + if fc.HasBlocking() { suffix = " }" + suffix } } localVarDefs := "" // Function-local var declaration at the top. - if len(fc.Blocking) != 0 { + if fc.HasBlocking() { 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. @@ -314,7 +314,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.HasDefer { prefix = prefix + " var $err = null; try {" deferSuffix := " } catch(err) { $err = err;" - if len(fc.Blocking) != 0 { + if fc.HasBlocking() { deferSuffix += " $s = -1;" } if fc.resultNames == nil && fc.sig.HasResults() { @@ -324,7 +324,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.resultNames != nil { deferSuffix += fmt.Sprintf(" if (!$curGoroutine.asleep) { return %s; }", fc.translateResults(fc.resultNames)) } - if len(fc.Blocking) != 0 { + if fc.HasBlocking() { deferSuffix += " if($curGoroutine.asleep) {" } suffix = deferSuffix + suffix diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go index 44ea6c165..f6edfd825 100644 --- a/compiler/internal/analysis/info.go +++ b/compiler/internal/analysis/info.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) @@ -50,22 +51,24 @@ func (ap astPath) String() string { type Info struct { *types.Info Pkg *types.Package + typeCtx *types.Context + instanceSets *typeparams.PackageInstanceSets HasPointer map[*types.Var]bool - FuncDeclInfos map[*types.Func]*FuncInfo - FuncLitInfos map[*ast.FuncLit]*FuncInfo + funcInstInfos *typeparams.InstanceMap[*FuncInfo] + funcLitInfos map[*ast.FuncLit]*FuncInfo InitFuncInfo *FuncInfo // Context for package variable initialization. - isImportedBlocking func(*types.Func) bool // For functions from other packages. + isImportedBlocking func(typeparams.Instance) bool // For functions from other packages. allInfos []*FuncInfo } -func (info *Info) newFuncInfo(n ast.Node) *FuncInfo { +func (info *Info) newFuncInfo(n ast.Node, inst *typeparams.Instance) *FuncInfo { funcInfo := &FuncInfo{ pkgInfo: info, Flattened: make(map[ast.Node]bool), Blocking: make(map[ast.Node]bool), GotoLabel: make(map[*types.Label]bool), - localNamedCallees: make(map[*types.Func][]astPath), + localInstCallees: new(typeparams.InstanceMap[[]astPath]), literalFuncCallees: make(map[*ast.FuncLit][]astPath), } @@ -76,14 +79,19 @@ func (info *Info) newFuncInfo(n ast.Node) *FuncInfo { // Function body comes from elsewhere (for example, from a go:linkname // directive), conservatively assume that it may be blocking. // TODO(nevkontakte): It is possible to improve accuracy of this detection. - // Since GopherJS supports inly "import-style" go:linkname, at this stage + // Since GopherJS supports only "import-style" go:linkname, at this stage // the compiler already determined whether the implementation function is // blocking, and we could check that. funcInfo.Blocking[n] = true } - info.FuncDeclInfos[info.Defs[n.Name].(*types.Func)] = funcInfo + + if inst == nil { + inst = &typeparams.Instance{Object: info.Defs[n.Name]} + } + info.funcInstInfos.Set(*inst, funcInfo) + case *ast.FuncLit: - info.FuncLitInfos[n] = funcInfo + info.funcLitInfos[n] = funcInfo } // And add it to the list of all functions. @@ -92,12 +100,42 @@ func (info *Info) newFuncInfo(n ast.Node) *FuncInfo { return funcInfo } +func (info *Info) newFuncInfoInstances(fd *ast.FuncDecl) []*FuncInfo { + obj := info.Defs[fd.Name] + instances := info.instanceSets.Pkg(info.Pkg).ForObj(obj) + if len(instances) == 0 { + // No instances found, this is a non-generic function. + return []*FuncInfo{info.newFuncInfo(fd, nil)} + } + + funcInfos := make([]*FuncInfo, 0, len(instances)) + for _, inst := range instances { + fi := info.newFuncInfo(fd, &inst) + if sig, ok := obj.Type().(*types.Signature); ok { + tp := typeparams.ToSlice(typeparams.SignatureTypeParams(sig)) + fi.resolver = typeparams.NewResolver(info.typeCtx, tp, inst.TArgs) + } + funcInfos = append(funcInfos, fi) + } + return funcInfos +} + // IsBlocking returns true if the function may contain blocking calls or operations. -func (info *Info) IsBlocking(fun *types.Func) bool { - if funInfo := info.FuncDeclInfos[fun]; funInfo != nil { - return len(funInfo.Blocking) > 0 +func (info *Info) IsBlocking(inst typeparams.Instance) bool { + if funInfo := info.FuncInfo(inst); funInfo != nil { + return funInfo.HasBlocking() } - panic(fmt.Errorf(`info did not have function declaration for %s`, fun.FullName())) + panic(fmt.Errorf(`info did not have function declaration instance for %q`, inst)) +} + +// FuncInfo returns information about the given function declaration instance, or nil if not found. +func (info *Info) FuncInfo(inst typeparams.Instance) *FuncInfo { + return info.funcInstInfos.Get(inst) +} + +// FuncLitInfo returns information about the given function literal, or nil if not found. +func (info *Info) FuncLitInfo(fun *ast.FuncLit) *FuncInfo { + return info.funcLitInfos[fun] } // VarsWithInitializers returns a set of package-level variables that have @@ -112,16 +150,18 @@ func (info *Info) VarsWithInitializers() map[*types.Var]bool { return result } -func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(*types.Func) bool) *Info { +func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info, typeCtx *types.Context, typesPkg *types.Package, instanceSets *typeparams.PackageInstanceSets, isBlocking func(typeparams.Instance) bool) *Info { info := &Info{ Info: typesInfo, Pkg: typesPkg, + typeCtx: typeCtx, + instanceSets: instanceSets, HasPointer: make(map[*types.Var]bool), isImportedBlocking: isBlocking, - FuncDeclInfos: make(map[*types.Func]*FuncInfo), - FuncLitInfos: make(map[*ast.FuncLit]*FuncInfo), + funcInstInfos: new(typeparams.InstanceMap[*FuncInfo]), + funcLitInfos: make(map[*ast.FuncLit]*FuncInfo), } - info.InitFuncInfo = info.newFuncInfo(nil) + info.InitFuncInfo = info.newFuncInfo(nil, nil) // Traverse the full AST of the package and collect information about existing // functions. @@ -152,19 +192,19 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info done := true for _, caller := range info.allInfos { // Check calls to named functions and function-typed variables. - for callee, callSites := range caller.localNamedCallees { - if info.IsBlocking(callee) { + caller.localInstCallees.Iterate(func(callee typeparams.Instance, callSites []astPath) { + if info.FuncInfo(callee).HasBlocking() { for _, callSite := range callSites { caller.markBlocking(callSite) } - delete(caller.localNamedCallees, callee) + caller.localInstCallees.Delete(callee) done = false } - } + }) // Check direct calls to function literals. for callee, callSites := range caller.literalFuncCallees { - if len(info.FuncLitInfos[callee].Blocking) > 0 { + if info.FuncLitInfo(callee).HasBlocking() { for _, callSite := range callSites { caller.markBlocking(callSite) } @@ -202,7 +242,7 @@ type FuncInfo struct { // Blocking indicates that either the AST node itself or its descendant may // block goroutine execution (for example, a channel operation). Blocking map[ast.Node]bool - // GotoLavel indicates a label referenced by a goto statement, rather than a + // GotoLabel indicates a label referenced by a goto statement, rather than a // named loop. GotoLabel map[*types.Label]bool // List of continue statements in the function. @@ -211,18 +251,30 @@ type FuncInfo struct { returnStmts []astPath // List of other named functions from the current package this function calls. // If any of them are blocking, this function will become blocking too. - localNamedCallees map[*types.Func][]astPath + localInstCallees *typeparams.InstanceMap[[]astPath] // List of function literals directly called from this function (for example: // `func() { /* do stuff */ }()`). This is distinct from function literals // assigned to named variables (for example: `doStuff := func() {}; // doStuff()`), which are handled by localNamedCallees. If any of them are // identified as blocking, this function will become blocking too. literalFuncCallees map[*ast.FuncLit][]astPath + // resolver is used by this function instance to resolve any type arguments + // for internal function calls. + // This may be nil if not an instance of a generic function. + resolver *typeparams.Resolver pkgInfo *Info // Function's parent package. visitorStack astPath } +// HasBlocking indicates if this function may block goroutine execution. +// +// For example, a channel operation in a function or a call to another +// possibly blocking function may block the function. +func (fi *FuncInfo) HasBlocking() bool { + return fi == nil || len(fi.Blocking) != 0 +} + func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { if node == nil { if len(fi.visitorStack) != 0 { @@ -233,9 +285,19 @@ func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { fi.visitorStack = append(fi.visitorStack, node) switch n := node.(type) { - case *ast.FuncDecl, *ast.FuncLit: - // Analyze the function in its own context. - return fi.pkgInfo.newFuncInfo(n) + case *ast.FuncDecl: + // Analyze all the instances of the function declarations + // in their own context with their own type arguments. + fis := fi.pkgInfo.newFuncInfoInstances(n) + if n.Body != nil { + for _, fi := range fis { + ast.Walk(fi, n.Body) + } + } + return nil + case *ast.FuncLit: + // Analyze the function literal in its own context. + return fi.pkgInfo.newFuncInfo(n, nil) case *ast.BranchStmt: switch n.Tok { case token.GOTO: @@ -334,13 +396,19 @@ func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { switch f := astutil.RemoveParens(n.Fun).(type) { case *ast.Ident: - fi.callToNamedFunc(fi.pkgInfo.Uses[f]) + fi.callToNamedFunc(fi.instanceForIdent(f)) case *ast.SelectorExpr: - if sel := fi.pkgInfo.Selections[f]; sel != nil && typesutil.IsJsObject(sel.Recv()) { - // js.Object methods are known to be non-blocking, but we still must - // check its arguments. + if sel := fi.pkgInfo.Selections[f]; sel != nil { + if typesutil.IsJsObject(sel.Recv()) { + // js.Object methods are known to be non-blocking, but we still must + // check its arguments. + } else { + // selection is a method call like `foo.Bar()`, where `foo` might + // be generic and needs to be substituted with the type argument. + fi.callToNamedFunc(fi.instanceFoSelection(sel)) + } } else { - fi.callToNamedFunc(fi.pkgInfo.Uses[f.Sel]) + fi.callToNamedFunc(fi.instanceForIdent(f.Sel)) } case *ast.FuncLit: // Collect info about the function literal itself. @@ -353,6 +421,34 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { // Register literal function call site in case it is identified as blocking. fi.literalFuncCallees[f] = append(fi.literalFuncCallees[f], fi.visitorStack.copy()) return nil // No need to walk under this CallExpr, we already did it manually. + case *ast.IndexExpr: + // Collect info about the instantiated type or function, or index expression. + if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { + // This is a type conversion to an instance of a generic type, + // not a call. Type assertion itself is not blocking, but we will + // visit the input expression. + } else if astutil.IsTypeExpr(f.Index, fi.pkgInfo.Info) { + // This is a call of an instantiation of a generic function, + // e.g. `foo[int]` in `func foo[T any]() { ... }; func main() { foo[int]() }` + fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident))) + } else { + // The called function is gotten with an index or key from a map, array, or slice. + // e.g. `m := map[string]func(){}; m["key"]()`, `s := []func(); s[0]()`. + // Since we can't predict if the returned function will be blocking + // or not, we have to be conservative and assume that function might be blocking. + fi.markBlocking(fi.visitorStack) + } + case *ast.IndexListExpr: + // Collect info about the instantiated type or function. + if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { + // This is a type conversion to an instance of a generic type, + // not a call. Type assertion itself is not blocking, but we will + // visit the input expression. + } else { + // This is a call of an instantiation of a generic function, + // e.g. `foo[int, bool]` in `func foo[T1, T2 any]() { ... }; func main() { foo[int, bool]() }` + fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident))) + } default: if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { // This is a type conversion, not a call. Type assertion itself is not @@ -367,8 +463,47 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { return fi } -func (fi *FuncInfo) callToNamedFunc(callee types.Object) { - switch o := callee.(type) { +func (fi *FuncInfo) instanceForIdent(fnId *ast.Ident) typeparams.Instance { + tArgs := fi.pkgInfo.Info.Instances[fnId].TypeArgs + return typeparams.Instance{ + Object: fi.pkgInfo.Uses[fnId], + TArgs: fi.resolver.SubstituteAll(tArgs), + } +} + +func (fi *FuncInfo) instanceFoSelection(sel *types.Selection) typeparams.Instance { + if _, ok := sel.Obj().Type().(*types.Signature); ok { + // Substitute the selection to ensure that the receiver has the correct + // type arguments propagated down from the caller. + resolved := fi.resolver.SubstituteSelection(sel) + sig := resolved.Obj().Type().(*types.Signature) + + // Using the substituted receiver type, find the instance of this call. + // This does require looking up the original method in the receiver type + // that may or may not have been the receiver prior to the substitution. + if recv := sig.Recv(); recv != nil { + typ := recv.Type() + if ptrType, ok := typ.(*types.Pointer); ok { + typ = ptrType.Elem() + } + + if rt, ok := typ.(*types.Named); ok { + origMethod, _, _ := types.LookupFieldOrMethod(rt.Origin(), true, rt.Obj().Pkg(), resolved.Obj().Name()) + if origMethod == nil { + panic(fmt.Errorf(`failed to lookup field %q in type %v`, resolved.Obj().Name(), rt.Origin())) + } + return typeparams.Instance{ + Object: origMethod, + TArgs: fi.resolver.SubstituteAll(rt.TypeArgs()), + } + } + } + } + return typeparams.Instance{Object: sel.Obj()} +} + +func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance) { + switch o := callee.Object.(type) { case *types.Func: o = o.Origin() if recv := o.Type().(*types.Signature).Recv(); recv != nil { @@ -379,14 +514,16 @@ func (fi *FuncInfo) callToNamedFunc(callee types.Object) { } } if o.Pkg() != fi.pkgInfo.Pkg { - if fi.pkgInfo.isImportedBlocking(o) { + if fi.pkgInfo.isImportedBlocking(callee) { fi.markBlocking(fi.visitorStack) } return } // We probably don't know yet whether the callee function is blocking. // Record the calls site for the later stage. - fi.localNamedCallees[o] = append(fi.localNamedCallees[o], fi.visitorStack.copy()) + paths := fi.localInstCallees.Get(callee) + paths = append(paths, fi.visitorStack.copy()) + fi.localInstCallees.Set(callee, paths) case *types.Var: // Conservatively assume that a function in a variable might be blocking. fi.markBlocking(fi.visitorStack) diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go index 588f09a6c..f4475cf0f 100644 --- a/compiler/internal/analysis/info_test.go +++ b/compiler/internal/analysis/info_test.go @@ -5,74 +5,1238 @@ import ( "go/types" "testing" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/internal/srctesting" ) -// See: https://github.com/gopherjs/gopherjs/issues/955. -func TestBlockingFunctionLiteral(t *testing.T) { - src := ` -package test +func TestBlocking_Simple(t *testing.T) { + bt := newBlockingTest(t, + `package test -func blocking() { - c := make(chan bool) - <-c + func notBlocking() { + println("hi") + }`) + bt.assertNotBlocking(`notBlocking`) } -func indirectlyBlocking() { - func() { blocking() }() +func TestBlocking_Recursive(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func notBlocking(i int) { + if i > 0 { + println(i) + notBlocking(i - 1) + } + }`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_AlternatingRecursive(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func near(i int) { + if i > 0 { + println(i) + far(i) + } + } + + func far(i int) { + near(i - 1) + }`) + bt.assertNotBlocking(`near`) + bt.assertNotBlocking(`far`) +} + +func TestBlocking_Channels(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func readFromChannel(c chan bool) { + <-c + } + + func readFromChannelAssign(c chan bool) { + v := <-c + println(v) + } + + func readFromChannelAsArg(c chan bool) { + println(<-c) + } + + func sendToChannel(c chan bool) { + c <- true + } + + func rangeOnChannel(c chan bool) { + for v := range c { + println(v) + } + } + + func rangeOnSlice(c []bool) { + for v := range c { + println(v) + } + }`) + bt.assertBlocking(`readFromChannel`) + bt.assertBlocking(`sendToChannel`) + bt.assertBlocking(`rangeOnChannel`) + bt.assertBlocking(`readFromChannelAssign`) + bt.assertBlocking(`readFromChannelAsArg`) + bt.assertNotBlocking(`rangeOnSlice`) +} + +func TestBlocking_Selects(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func selectReadWithoutDefault(a, b chan bool) { + select { + case <-a: + println("a") + case v := <-b: + println("b", v) + } + } + + func selectReadWithDefault(a, b chan bool) { + select { + case <-a: + println("a") + case v := <-b: + println("b", v) + default: + println("nothing") + } + } + + func selectSendWithoutDefault(a, b chan bool) { + select { + case a <- true: + println("a") + case b <- false: + println("b") + } + } + + func selectSendWithDefault(a, b chan bool) { + select { + case a <- true: + println("a") + case b <- false: + println("b") + default: + println("nothing") + } + }`) + bt.assertBlocking(`selectReadWithoutDefault`) + bt.assertBlocking(`selectSendWithoutDefault`) + bt.assertNotBlocking(`selectReadWithDefault`) + bt.assertNotBlocking(`selectSendWithDefault`) +} + +func TestBlocking_GoRoutines_WithFuncLiterals(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func notBlocking(c chan bool) { + go func(c chan bool) { // line 4 + println(<-c) + }(c) + } + + func blocking(c chan bool) { + go func(v bool) { // line 10 + println(v) + }(<-c) + }`) + bt.assertNotBlocking(`notBlocking`) + bt.assertBlockingLit(4) + + bt.assertBlocking(`blocking`) + bt.assertNotBlockingLit(10) +} + +func TestBlocking_GoRoutines_WithNamedFuncs(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingRoutine(c chan bool) { + println(<-c) + } + + func nonBlockingRoutine(v bool) { + println(v) + } + + func notBlocking(c chan bool) { + go blockingRoutine(c) + } + + func blocking(c chan bool) { + go nonBlockingRoutine(<-c) + }`) + bt.assertBlocking(`blockingRoutine`) + bt.assertNotBlocking(`nonBlockingRoutine`) + + bt.assertNotBlocking(`notBlocking`) + bt.assertBlocking(`blocking`) +} + +func TestBlocking_Defers_WithoutReturns_WithFuncLiterals(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingBody(c chan bool) { + defer func(c chan bool) { // line 4 + println(<-c) + }(c) + } + + func blockingArg(c chan bool) { + defer func(v bool) { // line 10 + println(v) + }(<-c) + } + + func notBlocking(c chan bool) { + defer func(v bool) { // line 16 + println(v) + }(true) + }`) + bt.assertBlocking(`blockingBody`) + bt.assertBlockingLit(4) + + bt.assertBlocking(`blockingArg`) + bt.assertNotBlockingLit(10) + + bt.assertNotBlocking(`notBlocking`) + bt.assertNotBlockingLit(16) +} + +func TestBlocking_Defers_WithoutReturns_WithNamedFuncs(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingPrint(c chan bool) { + println(<-c) + } + + func nonBlockingPrint(v bool) { + println(v) + } + + func blockingBody(c chan bool) { + defer blockingPrint(c) + } + + func blockingArg(c chan bool) { + defer nonBlockingPrint(<-c) + } + + func notBlocking(c chan bool) { + defer nonBlockingPrint(true) + }`) + bt.assertBlocking(`blockingPrint`) + bt.assertNotBlocking(`nonBlockingPrint`) + + bt.assertBlocking(`blockingBody`) + bt.assertBlocking(`blockingArg`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_Defers_WithReturns_WithFuncLiterals(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingBody(c chan bool) int { + defer func(c chan bool) { // line 4 + println(<-c) + }(c) + return 42 + } + + func blockingArg(c chan bool) int { + defer func(v bool) { // line 11 + println(v) + }(<-c) + return 42 + } + + func notBlocking(c chan bool) int { + defer func(v bool) { // line 18 + println(v) + }(true) + return 42 + }`) + bt.assertBlocking(`blockingBody`) + bt.assertBlockingLit(4) + + bt.assertBlocking(`blockingArg`) + bt.assertNotBlockingLit(11) + + // TODO: The following is blocking because currently any defer with a return + // is assumed to be blocking. This limitation should be fixed in the future. + bt.assertBlocking(`notBlocking`) + bt.assertNotBlockingLit(18) +} + +func TestBlocking_Defers_WithReturns_WithNamedFuncs(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blockingPrint(c chan bool) { + println(<-c) + } + + func nonBlockingPrint(v bool) { + println(v) + } + + func blockingBody(c chan bool) int { + defer blockingPrint(c) + return 42 + } + + func blockingArg(c chan bool) int { + defer nonBlockingPrint(<-c) + return 42 + } + + func notBlocking(c chan bool) int { + defer nonBlockingPrint(true) + return 42 + }`) + bt.assertBlocking(`blockingPrint`) + bt.assertNotBlocking(`nonBlockingPrint`) + + bt.assertBlocking(`blockingBody`) + bt.assertBlocking(`blockingArg`) + + // TODO: The following is blocking because currently any defer with a return + // is assumed to be blocking. This limitation should be fixed in the future. + bt.assertBlocking(`notBlocking`) +} + +func TestBlocking_Returns_WithoutDefers(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blocking(c chan bool) bool { + return <-c + } + + func indirectlyBlocking(c chan bool) bool { + return blocking(c) + } + + func notBlocking(c chan bool) bool { + return true + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indirectlyBlocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_FunctionLiteral(t *testing.T) { + // See: https://github.com/gopherjs/gopherjs/issues/955. + bt := newBlockingTest(t, + `package test + + func blocking() { + c := make(chan bool) + <-c + } + + func indirectlyBlocking() { + func() { blocking() }() // line 9 + } + + func directlyBlocking() { + func() { // line 13 + c := make(chan bool) + <-c + }() + } + + func notBlocking() { + func() { println() } () // line 20 + }`) + bt.assertBlocking(`blocking`) + + bt.assertBlocking(`indirectlyBlocking`) + bt.assertBlockingLit(9) + + bt.assertBlocking(`directlyBlocking`) + bt.assertBlockingLit(13) + + bt.assertNotBlocking(`notBlocking`) + bt.assertNotBlockingLit(20) +} + +func TestBlocking_LinkedFunction(t *testing.T) { + bt := newBlockingTest(t, + `package test + + // linked to some other function + func blocking() + + func indirectlyBlocking() { + blocking() + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indirectlyBlocking`) +} + +func TestBlocking_Instances_WithSingleTypeArg(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blocking[T any]() { + c := make(chan T) + <-c + } + + func notBlocking[T any]() { + var v T + println(v) + } + + func bInt() { + blocking[int]() + } + + func nbUint() { + notBlocking[uint]() + }`) + bt.assertFuncInstCount(4) + // blocking and notBlocking as generics do not have FuncInfo, + // only non-generic and instances have FuncInfo. + + bt.assertBlockingInst(`test.blocking[int]`) + bt.assertBlocking(`bInt`) + bt.assertNotBlockingInst(`test.notBlocking[uint]`) + bt.assertNotBlocking(`nbUint`) +} + +func TestBlocking_Instances_WithMultipleTypeArgs(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func blocking[K comparable, V any, M ~map[K]V]() { + c := make(chan M) + <-c + } + + func notBlocking[K comparable, V any, M ~map[K]V]() { + var m M + println(m) + } + + func bInt() { + blocking[string, int, map[string]int]() + } + + func nbUint() { + notBlocking[string, uint, map[string]uint]() + }`) + bt.assertFuncInstCount(4) + // blocking and notBlocking as generics do not have FuncInfo, + // only non-generic and instances have FuncInfo. + + bt.assertBlockingInst(`test.blocking[string, int, map[string]int]`) + bt.assertBlocking(`bInt`) + bt.assertNotBlockingInst(`test.notBlocking[string, uint, map[string]uint]`) + bt.assertNotBlocking(`nbUint`) +} + +func TestBlocking_Indexed_FunctionSlice(t *testing.T) { + // This calls notBlocking but since the function pointers + // are in the slice they will both be considered as blocking. + bt := newBlockingTest(t, + `package test + + func blocking() { + c := make(chan int) + <-c + } + + func notBlocking() { + println() + } + + var funcs = []func() { blocking, notBlocking } + + func indexer(i int) { + funcs[i]() + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indexer`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_Indexed_FunctionMap(t *testing.T) { + // This calls notBlocking but since the function pointers + // are in the map they will both be considered as blocking. + bt := newBlockingTest(t, + `package test + + func blocking() { + c := make(chan int) + <-c + } + + func notBlocking() { + println() + } + + var funcs = map[string]func() { + "b": blocking, + "nb": notBlocking, + } + + func indexer(key string) { + funcs[key]() + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indexer`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_Indexed_FunctionArray(t *testing.T) { + // This calls notBlocking but since the function pointers + // are in the array they will both be considered as blocking. + bt := newBlockingTest(t, + `package test + + func blocking() { + c := make(chan int) + <-c + } + + func notBlocking() { + println() + } + + var funcs = [2]func() { blocking, notBlocking } + + func indexer(i int) { + funcs[i]() + }`) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`indexer`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_Casting_InterfaceInstanceWithSingleTypeParam(t *testing.T) { + // This checks that casting to an instance type with a single type parameter + // is treated as a cast and not accidentally treated as a function call. + bt := newBlockingTest(t, + `package test + + type Foo[T any] interface { + Baz() T + } + + type Bar struct { + name string + } + + func (b Bar) Baz() string { + return b.name + } + + func caster() Foo[string] { + b := Bar{name: "foo"} + return Foo[string](b) + }`) + bt.assertNotBlocking(`caster`) +} + +func TestBlocking_Casting_InterfaceInstanceWithMultipleTypeParams(t *testing.T) { + // This checks that casting to an instance type with multiple type parameters + // is treated as a cast and not accidentally treated as a function call. + bt := newBlockingTest(t, + `package test + + type Foo[K comparable, V any] interface { + Baz(K) V + } + + type Bar struct { + dat map[string]int + } + + func (b Bar) Baz(key string) int { + return b.dat[key] + } + + func caster() Foo[string, int] { + b := Bar{ dat: map[string]int{ "foo": 2 }} + return Foo[string, int](b) + }`) + bt.assertNotBlocking(`caster`) +} + +func TestBlocking_Casting_Interface(t *testing.T) { + // This checks that non-generic casting of type is treated as a + // cast and not accidentally treated as a function call. + bt := newBlockingTest(t, + `package test + + type Foo interface { + Baz() string + } + + type Bar struct { + name string + } + + func (b Bar) Baz() string { + return b.name + } + + func caster() Foo { + b := Bar{"foo"} + return Foo(b) + }`) + bt.assertNotBlocking(`caster`) +} + +func TestBlocking_ComplexCasting(t *testing.T) { + // This checks a complex casting to a type is treated as a + // cast and not accidentally treated as a function call. + bt := newBlockingTest(t, + `package test + + type Foo interface { + Bar() string + } + + func doNothing(f Foo) Foo { + return interface{ Bar() string }(f) + }`) + bt.assertNotBlocking(`doNothing`) } -func directlyBlocking() { - func() { - c := make(chan bool) - <-c - }() +func TestBlocking_ComplexCall(t *testing.T) { + // This checks a complex call of a function is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + type Foo func() string + + func bar(f any) string { + return f.(Foo)() + }`) + bt.assertBlocking(`bar`) } -func notBlocking() { - func() { println() } () +func TestBlocking_CallWithNamedInterfaceReceiver(t *testing.T) { + // This checks that calling a named interface function is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + type Foo interface { + Baz() + } + + func bar(f Foo) { + f.Baz() + }`) + bt.assertBlocking(`bar`) } -` + +func TestBlocking_CallWithUnnamedInterfaceReceiver(t *testing.T) { + // This checks that calling an unnamed interface function is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + func bar(f interface { Baz() }) { + f.Baz() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_VarFunctionCall(t *testing.T) { + // This checks that calling a function in a var is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + var foo = func() { // line 3 + println("hi") + } + + func bar() { + foo() + }`) + bt.assertNotBlockingLit(3) + bt.assertBlocking(`bar`) +} + +func TestBlocking_FieldFunctionCallOnNamed(t *testing.T) { + // This checks that calling a function in a field is defaulted to blocking. + // This should be the same as the previous test but with a field since + // all function pointers are treated as blocking. + bt := newBlockingTest(t, + `package test + + type foo struct { + Baz func() + } + + func bar(f foo) { + f.Baz() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_FieldFunctionCallOnUnnamed(t *testing.T) { + // Same as previous test but with an unnamed struct. + bt := newBlockingTest(t, + `package test + + func bar(f struct { Baz func() }) { + f.Baz() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_ParamFunctionCall(t *testing.T) { + // Same as previous test but with an unnamed function parameter. + bt := newBlockingTest(t, + `package test + + func bar(baz func()) { + baz() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_FunctionUnwrapping(t *testing.T) { + // Test that calling a function that calls a function etc. + // is defaulted to blocking. + bt := newBlockingTest(t, + `package test + + func bar(baz func()func()func()) { + baz()()() + }`) + bt.assertBlocking(`bar`) +} + +func TestBlocking_MethodCall_NonPointer(t *testing.T) { + // Test that calling a method on a non-pointer receiver. + bt := newBlockingTest(t, + `package test + + type Foo struct {} + + func (f Foo) blocking() { + ch := make(chan bool) + <-ch + } + + func (f Foo) notBlocking() { + println("hi") + } + + func blocking(f Foo) { + f.blocking() + } + + func notBlocking(f Foo) { + f.notBlocking() + }`) + bt.assertBlocking(`Foo.blocking`) + bt.assertNotBlocking(`Foo.notBlocking`) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_MethodCall_Pointer(t *testing.T) { + // Test that calling a method on a pointer receiver. + bt := newBlockingTest(t, + `package test + + type Foo struct {} + + func (f *Foo) blocking() { + ch := make(chan bool) + <-ch + } + + func (f *Foo) notBlocking() { + println("hi") + } + + func blocking(f *Foo) { + f.blocking() + } + + func notBlocking(f *Foo) { + f.notBlocking() + }`) + bt.assertBlocking(`Foo.blocking`) + bt.assertNotBlocking(`Foo.notBlocking`) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_InstantiationBlocking(t *testing.T) { + // This checks that the instantiation of a generic function is + // being used when checking for blocking not the type argument interface. + bt := newBlockingTest(t, + `package test + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + type Foo interface { Baz() } + func FooBaz[T Foo](foo T) { + foo.Baz() + } + + func blockingViaExplicit() { + FooBaz[BazBlocker](BazBlocker{c: make(chan bool)}) + } + + func notBlockingViaExplicit() { + FooBaz[BazNotBlocker](BazNotBlocker{}) + } + + func blockingViaImplicit() { + FooBaz(BazBlocker{c: make(chan bool)}) + } + + func notBlockingViaImplicit() { + FooBaz(BazNotBlocker{}) + }`) + bt.assertFuncInstCount(8) + // `FooBaz` as a generic function does not have FuncInfo for it, + // only non-generic or instantiations of a generic functions have FuncInfo. + + bt.assertBlocking(`BazBlocker.Baz`) + bt.assertBlocking(`blockingViaExplicit`) + bt.assertBlocking(`blockingViaImplicit`) + bt.assertBlockingInst(`test.FooBaz[pkg/test.BazBlocker]`) + + bt.assertNotBlocking(`BazNotBlocker.Baz`) + bt.assertNotBlocking(`notBlockingViaExplicit`) + bt.assertNotBlocking(`notBlockingViaImplicit`) + bt.assertNotBlockingInst(`test.FooBaz[pkg/test.BazNotBlocker]`) +} + +func TestBlocking_NestedInstantiations(t *testing.T) { + // Checking that the type parameters are being propagated down into calls. + bt := newBlockingTest(t, + `package test + + func Foo[T any](t T) { + println(t) + } + + func Bar[K comparable, V any, M ~map[K]V](m M) { + Foo(m) + } + + func Baz[T any, S ~[]T](s S) { + m:= map[int]T{} + for i, v := range s { + m[i] = v + } + Bar(m) + } + + func bazInt() { + Baz([]int{1, 2, 3}) + } + + func bazString() { + Baz([]string{"one", "two", "three"}) + }`) + bt.assertFuncInstCount(8) + bt.assertNotBlocking(`bazInt`) + bt.assertNotBlocking(`bazString`) + bt.assertNotBlockingInst(`test.Foo[map[int]int]`) + bt.assertNotBlockingInst(`test.Foo[map[int]string]`) + bt.assertNotBlockingInst(`test.Bar[int, int, map[int]int]`) + bt.assertNotBlockingInst(`test.Bar[int, string, map[int]string]`) + bt.assertNotBlockingInst(`test.Baz[int, []int]`) + bt.assertNotBlockingInst(`test.Baz[string, []string]`) +} + +func TestBlocking_MethodSelection(t *testing.T) { + // This tests method selection using method expression (receiver as the first + // argument) selecting on type and method call selecting on a variable. + // This tests in both generic (FooBaz[T]) and non-generic contexts. + bt := newBlockingTest(t, + `package test + + type Foo interface { Baz() } + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + type FooBaz[T Foo] struct {} + func (fb FooBaz[T]) ByMethodExpression() { + var foo T + T.Baz(foo) + } + func (fb FooBaz[T]) ByInstance() { + var foo T + foo.Baz() + } + + func blocking() { + fb := FooBaz[BazBlocker]{} + + FooBaz[BazBlocker].ByMethodExpression(fb) + FooBaz[BazBlocker].ByInstance(fb) + + fb.ByMethodExpression() + fb.ByInstance() + } + + func notBlocking() { + fb := FooBaz[BazNotBlocker]{} + + FooBaz[BazNotBlocker].ByMethodExpression(fb) + FooBaz[BazNotBlocker].ByInstance(fb) + + fb.ByMethodExpression() + fb.ByInstance() + }`) + bt.assertFuncInstCount(8) + + bt.assertBlocking(`BazBlocker.Baz`) + bt.assertBlockingInst(`test.ByMethodExpression[pkg/test.BazBlocker]`) + bt.assertBlockingInst(`test.ByInstance[pkg/test.BazBlocker]`) + bt.assertBlocking(`blocking`) + + bt.assertNotBlocking(`BazNotBlocker.Baz`) + bt.assertNotBlockingInst(`test.ByMethodExpression[pkg/test.BazNotBlocker]`) + bt.assertNotBlockingInst(`test.ByInstance[pkg/test.BazNotBlocker]`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_IsImportBlocking_Simple(t *testing.T) { + otherSrc := `package other + + func Blocking() { + ch := make(chan bool) + <-ch + } + + func NotBlocking() { + println("hi") + }` + + testSrc := `package test + + import "pkg/other" + + func blocking() { + other.Blocking() + } + + func notBlocking() { + other.NotBlocking() + }` + + bt := newBlockingTestWithOtherPackage(t, testSrc, otherSrc) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_IsImportBlocking_ForwardInstances(t *testing.T) { + otherSrc := `package other + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + }` + + testSrc := `package test + + import "pkg/other" + + type Foo interface { Baz() } + func FooBaz[T Foo](f T) { + f.Baz() + } + + func blocking() { + FooBaz(other.BazBlocker{}) + } + + func notBlocking() { + FooBaz(other.BazNotBlocker{}) + }` + + bt := newBlockingTestWithOtherPackage(t, testSrc, otherSrc) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +func TestBlocking_IsImportBlocking_BackwardInstances(t *testing.T) { + t.Skip(`isImportedBlocking doesn't fully handle instances yet`) + // TODO(grantnelson-wf): This test is currently failing because the info + // for the test package is need while creating the instances for FooBaz + // while analyzing the other package. However the other package is analyzed + // first since the test package is dependent on it. One possible fix is that + // we add some mechanism similar to the localInstCallees but for remote + // instances then perform the blocking propagation steps for all packages + // including the localInstCallees propagation at the same time. After all the + // propagation of the calls then the flow control statements can be marked. + + otherSrc := `package other + + type Foo interface { Baz() } + func FooBaz[T Foo](f T) { + f.Baz() + }` + + testSrc := `package test + + import "pkg/other" + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + func blocking() { + other.FooBaz(BazBlocker{}) + } + + func notBlocking() { + other.FooBaz(BazNotBlocker{}) + }` + + bt := newBlockingTestWithOtherPackage(t, testSrc, otherSrc) + bt.assertBlocking(`blocking`) + bt.assertNotBlocking(`notBlocking`) +} + +type blockingTest struct { + f *srctesting.Fixture + file *ast.File + pkgInfo *Info +} + +func newBlockingTest(t *testing.T, src string) *blockingTest { f := srctesting.New(t) - file := f.Parse("test.go", src) - typesInfo, typesPkg := f.Check("pkg/test", file) + tc := typeparams.Collector{ + TContext: types.NewContext(), + Info: f.Info, + Instances: &typeparams.PackageInstanceSets{}, + } - pkgInfo := AnalyzePkg([]*ast.File{file}, f.FileSet, typesInfo, typesPkg, func(f *types.Func) bool { - panic("isBlocking() should be never called for imported functions in this test.") - }) + file := f.Parse(`test.go`, src) + testInfo, testPkg := f.Check(`pkg/test`, file) + tc.Scan(testPkg, file) - assertBlocking(t, file, pkgInfo, "blocking") - assertBlocking(t, file, pkgInfo, "indirectlyBlocking") - assertBlocking(t, file, pkgInfo, "directlyBlocking") - assertNotBlocking(t, file, pkgInfo, "notBlocking") + isImportBlocking := func(i typeparams.Instance) bool { + t.Fatalf(`isImportBlocking should not be called in this test, called with %v`, i) + return true + } + pkgInfo := AnalyzePkg([]*ast.File{file}, f.FileSet, testInfo, types.NewContext(), testPkg, tc.Instances, isImportBlocking) + + return &blockingTest{ + f: f, + file: file, + pkgInfo: pkgInfo, + } } -func assertBlocking(t *testing.T, file *ast.File, pkgInfo *Info, funcName string) { - typesFunc := getTypesFunc(t, file, pkgInfo, funcName) - if !pkgInfo.IsBlocking(typesFunc) { - t.Errorf("Got: %q is not blocking. Want: %q is blocking.", typesFunc, typesFunc) +func newBlockingTestWithOtherPackage(t *testing.T, testSrc string, otherSrc string) *blockingTest { + f := srctesting.New(t) + tc := typeparams.Collector{ + TContext: types.NewContext(), + Info: f.Info, + Instances: &typeparams.PackageInstanceSets{}, + } + + pkgInfo := map[*types.Package]*Info{} + isImportBlocking := func(i typeparams.Instance) bool { + if info, ok := pkgInfo[i.Object.Pkg()]; ok { + return info.IsBlocking(i) + } + t.Fatalf(`unexpected package in isImportBlocking for %v`, i) + return true + } + + otherFile := f.Parse(`other.go`, otherSrc) + _, otherPkg := f.Check(`pkg/other`, otherFile) + tc.Scan(otherPkg, otherFile) + + testFile := f.Parse(`test.go`, testSrc) + _, testPkg := f.Check(`pkg/test`, testFile) + tc.Scan(testPkg, testFile) + + otherPkgInfo := AnalyzePkg([]*ast.File{otherFile}, f.FileSet, f.Info, types.NewContext(), otherPkg, tc.Instances, isImportBlocking) + pkgInfo[otherPkg] = otherPkgInfo + + testPkgInfo := AnalyzePkg([]*ast.File{testFile}, f.FileSet, f.Info, types.NewContext(), testPkg, tc.Instances, isImportBlocking) + pkgInfo[testPkg] = testPkgInfo + + return &blockingTest{ + f: f, + file: testFile, + pkgInfo: testPkgInfo, } } -func assertNotBlocking(t *testing.T, file *ast.File, pkgInfo *Info, funcName string) { - typesFunc := getTypesFunc(t, file, pkgInfo, funcName) - if pkgInfo.IsBlocking(typesFunc) { - t.Errorf("Got: %q is blocking. Want: %q is not blocking.", typesFunc, typesFunc) +func (bt *blockingTest) assertFuncInstCount(expCount int) { + if got := bt.pkgInfo.funcInstInfos.Len(); got != expCount { + bt.f.T.Errorf(`Got %d function infos but expected %d.`, got, expCount) + for i, inst := range bt.pkgInfo.funcInstInfos.Keys() { + bt.f.T.Logf(` %d. %q`, i+1, inst.TypeString()) + } } } -func getTypesFunc(t *testing.T, file *ast.File, pkgInfo *Info, funcName string) *types.Func { - obj := file.Scope.Lookup(funcName) - if obj == nil { - t.Fatalf("Declaration of %q is not found in the AST.", funcName) +func (bt *blockingTest) assertBlocking(funcName string) { + if !bt.isTypesFuncBlocking(funcName) { + bt.f.T.Errorf(`Got %q as not blocking but expected it to be blocking.`, funcName) } - decl, ok := obj.Decl.(*ast.FuncDecl) - if !ok { - t.Fatalf("Got: %q is %v. Want: a function declaration.", funcName, obj.Kind) +} + +func (bt *blockingTest) assertNotBlocking(funcName string) { + if bt.isTypesFuncBlocking(funcName) { + bt.f.T.Errorf(`Got %q as blocking but expected it to be not blocking.`, funcName) } - blockingType, ok := pkgInfo.Defs[decl.Name] +} + +func getFuncDeclName(fd *ast.FuncDecl) string { + name := fd.Name.Name + if fd.Recv != nil && len(fd.Recv.List) == 1 && fd.Recv.List[0].Type != nil { + typ := fd.Recv.List[0].Type + if p, ok := typ.(*ast.StarExpr); ok { + typ = p.X + } + if id, ok := typ.(*ast.Ident); ok { + name = id.Name + `.` + name + } + } + return name +} + +func (bt *blockingTest) isTypesFuncBlocking(funcName string) bool { + var decl *ast.FuncDecl + ast.Inspect(bt.file, func(n ast.Node) bool { + if f, ok := n.(*ast.FuncDecl); ok && getFuncDeclName(f) == funcName { + decl = f + return false + } + return decl == nil + }) + + if decl == nil { + bt.f.T.Fatalf(`Declaration of %q is not found in the AST.`, funcName) + } + + blockingType, ok := bt.pkgInfo.Defs[decl.Name] if !ok { - t.Fatalf("No type information is found for %v.", decl.Name) + bt.f.T.Fatalf(`No function declaration found for %q.`, decl.Name) + } + + inst := typeparams.Instance{Object: blockingType.(*types.Func)} + return bt.pkgInfo.IsBlocking(inst) +} + +func (bt *blockingTest) assertBlockingLit(lineNo int) { + if !bt.isFuncLitBlocking(lineNo) { + bt.f.T.Errorf(`Got FuncLit at line %d as not blocking but expected it to be blocking.`, lineNo) + } +} + +func (bt *blockingTest) assertNotBlockingLit(lineNo int) { + if bt.isFuncLitBlocking(lineNo) { + bt.f.T.Errorf(`Got FuncLit at line %d as blocking but expected it to be not blocking.`, lineNo) + } +} + +func (bt *blockingTest) getFuncLitLineNo(fl *ast.FuncLit) int { + return bt.f.FileSet.Position(fl.Pos()).Line +} + +func (bt *blockingTest) isFuncLitBlocking(lineNo int) bool { + var fnLit *ast.FuncLit + ast.Inspect(bt.file, func(n ast.Node) bool { + if fl, ok := n.(*ast.FuncLit); ok && bt.getFuncLitLineNo(fl) == lineNo { + fnLit = fl + return false + } + return fnLit == nil + }) + + if fnLit == nil { + bt.f.T.Fatalf(`FuncLit found on line %d not found in the AST.`, lineNo) + } + return bt.pkgInfo.FuncLitInfo(fnLit).HasBlocking() +} + +func (bt *blockingTest) assertBlockingInst(instanceStr string) { + if !bt.isFuncInstBlocking(instanceStr) { + bt.f.T.Errorf(`Got function instance of %q as not blocking but expected it to be blocking.`, instanceStr) + } +} + +func (bt *blockingTest) assertNotBlockingInst(instanceStr string) { + if bt.isFuncInstBlocking(instanceStr) { + bt.f.T.Errorf(`Got function instance of %q as blocking but expected it to be not blocking.`, instanceStr) + } +} + +func (bt *blockingTest) isFuncInstBlocking(instanceStr string) bool { + instances := bt.pkgInfo.funcInstInfos.Keys() + for _, inst := range instances { + if inst.TypeString() == instanceStr { + return bt.pkgInfo.FuncInfo(inst).HasBlocking() + } + } + bt.f.T.Logf(`Function instances found in package info:`) + for i, inst := range instances { + bt.f.T.Logf(` %d. %s`, i+1, inst.TypeString()) } - return blockingType.(*types.Func) + bt.f.T.Fatalf(`No function instance found for %q in package info.`, instanceStr) + return false } diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index f847a9825..763cd6428 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -147,6 +147,18 @@ func (iset *InstanceSet) ByObj() map[types.Object][]Instance { return result } +// ForObj returns instances for a given object type belong to. Order is not specified. +// This returns the same values as `ByObj()[obj]`. +func (iset *InstanceSet) ForObj(obj types.Object) []Instance { + result := []Instance{} + for _, inst := range iset.values { + if inst.Object == obj { + result = append(result, inst) + } + } + return result +} + // PackageInstanceSets stores an InstanceSet for each package in a program, keyed // by import path. type PackageInstanceSets map[string]*InstanceSet diff --git a/compiler/package.go b/compiler/package.go index 9fcf9d0b0..4cd800607 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -118,14 +118,14 @@ type funcContext struct { funcLitCounter int } -func newRootCtx(tContext *types.Context, srcs sources, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(*types.Func) bool, minify bool) *funcContext { +func newRootCtx(tContext *types.Context, srcs sources, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(typeparams.Instance) bool, minify bool) *funcContext { tc := typeparams.Collector{ TContext: tContext, Info: typesInfo, Instances: &typeparams.PackageInstanceSets{}, } tc.Scan(typesPkg, srcs.Files...) - pkgInfo := analysis.AnalyzePkg(srcs.Files, srcs.FileSet, typesInfo, typesPkg, isBlocking) + pkgInfo := analysis.AnalyzePkg(srcs.Files, srcs.FileSet, typesInfo, tContext, typesPkg, tc.Instances, isBlocking) funcCtx := &funcContext{ FuncInfo: pkgInfo.InitFuncInfo, pkgCtx: &pkgContext{ @@ -176,11 +176,20 @@ type ImportContext struct { // 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 { +func (ic *ImportContext) isBlocking(inst typeparams.Instance) bool { + f, ok := inst.Object.(*types.Func) + if !ok { + panic(bailout(fmt.Errorf("can't determine if instance %v is blocking: instance isn't for a function object", inst))) + } + archive, err := ic.Import(f.Pkg().Path()) if err != nil { panic(err) } + + // TODO(grantnelson-wf): f.FullName() does not differentiate between + // different instantiations of the same generic function. This needs to be + // fixed when the declaration names are updated to better support instances. fullName := f.FullName() for _, d := range archive.Declarations { if string(d.FullName) == fullName { diff --git a/internal/srctesting/srctesting.go b/internal/srctesting/srctesting.go index 961dffd0b..35d9e25cc 100644 --- a/internal/srctesting/srctesting.go +++ b/internal/srctesting/srctesting.go @@ -74,7 +74,7 @@ func (f *Fixture) Check(importPath string, files ...*ast.File) (*types.Info, *ty } pkg, err := config.Check(importPath, f.FileSet, files, info) if err != nil { - f.T.Fatalf("Filed to type check test source: %s", err) + f.T.Fatalf("Failed to type check test source: %s", err) } f.Packages[importPath] = pkg return info, pkg From 40b7fe49418b386d991ce377a3bbecdc09d30210 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Wed, 23 Oct 2024 10:50:02 -0600 Subject: [PATCH 20/58] Fixed requested changes --- compiler/functions.go | 10 ++-- compiler/internal/analysis/info.go | 64 +++++++++++++----------- compiler/internal/analysis/info_test.go | 4 +- compiler/internal/typeparams/instance.go | 4 +- 4 files changed, 44 insertions(+), 38 deletions(-) diff --git a/compiler/functions.go b/compiler/functions.go index 657bf9926..86d3916bf 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -237,7 +237,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, } bodyOutput := string(fc.CatchOutput(1, func() { - if fc.HasBlocking() { + if fc.IsBlocking() { fc.pkgCtx.Scopes[body] = fc.pkgCtx.Scopes[typ] fc.handleEscapingVars(body) } @@ -283,14 +283,14 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.HasDefer { fc.localVars = append(fc.localVars, "$deferred") suffix = " }" + suffix - if fc.HasBlocking() { + if fc.IsBlocking() { suffix = " }" + suffix } } localVarDefs := "" // Function-local var declaration at the top. - if fc.HasBlocking() { + if fc.IsBlocking() { 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. @@ -314,7 +314,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.HasDefer { prefix = prefix + " var $err = null; try {" deferSuffix := " } catch(err) { $err = err;" - if fc.HasBlocking() { + if fc.IsBlocking() { deferSuffix += " $s = -1;" } if fc.resultNames == nil && fc.sig.HasResults() { @@ -324,7 +324,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, if fc.resultNames != nil { deferSuffix += fmt.Sprintf(" if (!$curGoroutine.asleep) { return %s; }", fc.translateResults(fc.resultNames)) } - if fc.HasBlocking() { + if fc.IsBlocking() { deferSuffix += " if($curGoroutine.asleep) {" } suffix = deferSuffix + suffix diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go index f6edfd825..6ab8a2f26 100644 --- a/compiler/internal/analysis/info.go +++ b/compiler/internal/analysis/info.go @@ -123,7 +123,7 @@ func (info *Info) newFuncInfoInstances(fd *ast.FuncDecl) []*FuncInfo { // IsBlocking returns true if the function may contain blocking calls or operations. func (info *Info) IsBlocking(inst typeparams.Instance) bool { if funInfo := info.FuncInfo(inst); funInfo != nil { - return funInfo.HasBlocking() + return funInfo.IsBlocking() } panic(fmt.Errorf(`info did not have function declaration instance for %q`, inst)) } @@ -193,7 +193,7 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info for _, caller := range info.allInfos { // Check calls to named functions and function-typed variables. caller.localInstCallees.Iterate(func(callee typeparams.Instance, callSites []astPath) { - if info.FuncInfo(callee).HasBlocking() { + if info.FuncInfo(callee).IsBlocking() { for _, callSite := range callSites { caller.markBlocking(callSite) } @@ -204,7 +204,7 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info // Check direct calls to function literals. for callee, callSites := range caller.literalFuncCallees { - if info.FuncLitInfo(callee).HasBlocking() { + if info.FuncLitInfo(callee).IsBlocking() { for _, callSite := range callSites { caller.markBlocking(callSite) } @@ -267,11 +267,11 @@ type FuncInfo struct { visitorStack astPath } -// HasBlocking indicates if this function may block goroutine execution. +// IsBlocking indicates if this function may block goroutine execution. // // For example, a channel operation in a function or a call to another // possibly blocking function may block the function. -func (fi *FuncInfo) HasBlocking() bool { +func (fi *FuncInfo) IsBlocking() bool { return fi == nil || len(fi.Blocking) != 0 } @@ -397,19 +397,22 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { switch f := astutil.RemoveParens(n.Fun).(type) { case *ast.Ident: fi.callToNamedFunc(fi.instanceForIdent(f)) + return fi case *ast.SelectorExpr: if sel := fi.pkgInfo.Selections[f]; sel != nil { if typesutil.IsJsObject(sel.Recv()) { // js.Object methods are known to be non-blocking, but we still must // check its arguments. - } else { - // selection is a method call like `foo.Bar()`, where `foo` might - // be generic and needs to be substituted with the type argument. - fi.callToNamedFunc(fi.instanceFoSelection(sel)) + return fi } - } else { - fi.callToNamedFunc(fi.instanceForIdent(f.Sel)) + // selection is a method call like `foo.Bar()`, where `foo` might + // be generic and needs to be substituted with the type argument. + fi.callToNamedFunc(fi.instanceForSelection(sel)) + return fi } + + fi.callToNamedFunc(fi.instanceForIdent(f.Sel)) + return fi case *ast.FuncLit: // Collect info about the function literal itself. ast.Walk(fi, n.Fun) @@ -427,40 +430,43 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { // This is a type conversion to an instance of a generic type, // not a call. Type assertion itself is not blocking, but we will // visit the input expression. - } else if astutil.IsTypeExpr(f.Index, fi.pkgInfo.Info) { + return fi + } + if astutil.IsTypeExpr(f.Index, fi.pkgInfo.Info) { // This is a call of an instantiation of a generic function, // e.g. `foo[int]` in `func foo[T any]() { ... }; func main() { foo[int]() }` fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident))) - } else { - // The called function is gotten with an index or key from a map, array, or slice. - // e.g. `m := map[string]func(){}; m["key"]()`, `s := []func(); s[0]()`. - // Since we can't predict if the returned function will be blocking - // or not, we have to be conservative and assume that function might be blocking. - fi.markBlocking(fi.visitorStack) + return fi } + // The called function is gotten with an index or key from a map, array, or slice. + // e.g. `m := map[string]func(){}; m["key"]()`, `s := []func(); s[0]()`. + // Since we can't predict if the returned function will be blocking + // or not, we have to be conservative and assume that function might be blocking. + fi.markBlocking(fi.visitorStack) + return fi case *ast.IndexListExpr: // Collect info about the instantiated type or function. if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { // This is a type conversion to an instance of a generic type, // not a call. Type assertion itself is not blocking, but we will // visit the input expression. - } else { - // This is a call of an instantiation of a generic function, - // e.g. `foo[int, bool]` in `func foo[T1, T2 any]() { ... }; func main() { foo[int, bool]() }` - fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident))) + return fi } + // This is a call of an instantiation of a generic function, + // e.g. `foo[int, bool]` in `func foo[T1, T2 any]() { ... }; func main() { foo[int, bool]() }` + fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident))) + return fi default: if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { // This is a type conversion, not a call. Type assertion itself is not // blocking, but we will visit the input expression. - } else { - // The function is returned by a non-trivial expression. We have to be - // conservative and assume that function might be blocking. - fi.markBlocking(fi.visitorStack) + return fi } + // The function is returned by a non-trivial expression. We have to be + // conservative and assume that function might be blocking. + fi.markBlocking(fi.visitorStack) + return fi } - - return fi } func (fi *FuncInfo) instanceForIdent(fnId *ast.Ident) typeparams.Instance { @@ -471,7 +477,7 @@ func (fi *FuncInfo) instanceForIdent(fnId *ast.Ident) typeparams.Instance { } } -func (fi *FuncInfo) instanceFoSelection(sel *types.Selection) typeparams.Instance { +func (fi *FuncInfo) instanceForSelection(sel *types.Selection) typeparams.Instance { if _, ok := sel.Obj().Type().(*types.Signature); ok { // Substitute the selection to ensure that the receiver has the correct // type arguments propagated down from the caller. diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go index f4475cf0f..62238c377 100644 --- a/compiler/internal/analysis/info_test.go +++ b/compiler/internal/analysis/info_test.go @@ -1211,7 +1211,7 @@ func (bt *blockingTest) isFuncLitBlocking(lineNo int) bool { if fnLit == nil { bt.f.T.Fatalf(`FuncLit found on line %d not found in the AST.`, lineNo) } - return bt.pkgInfo.FuncLitInfo(fnLit).HasBlocking() + return bt.pkgInfo.FuncLitInfo(fnLit).IsBlocking() } func (bt *blockingTest) assertBlockingInst(instanceStr string) { @@ -1230,7 +1230,7 @@ func (bt *blockingTest) isFuncInstBlocking(instanceStr string) bool { instances := bt.pkgInfo.funcInstInfos.Keys() for _, inst := range instances { if inst.TypeString() == instanceStr { - return bt.pkgInfo.FuncInfo(inst).HasBlocking() + return bt.pkgInfo.FuncInfo(inst).IsBlocking() } } bt.f.T.Logf(`Function instances found in package info:`) diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index 763cd6428..3e4c04d2c 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -147,8 +147,8 @@ func (iset *InstanceSet) ByObj() map[types.Object][]Instance { return result } -// ForObj returns instances for a given object type belong to. Order is not specified. -// This returns the same values as `ByObj()[obj]`. +// ForObj returns the instances that belong to the given object type. +// Order is not specified. This returns the same values as `ByObj()[obj]`. func (iset *InstanceSet) ForObj(obj types.Object) []Instance { result := []Instance{} for _, inst := range iset.values { From ea51571547488528f2e2781fd2cf765a02f992ef Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 24 Oct 2024 09:26:16 -0600 Subject: [PATCH 21/58] Fixing GoDoc links and range checking on len --- compiler/internal/dce/filters.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/compiler/internal/dce/filters.go b/compiler/internal/dce/filters.go index 58768ff39..420fd4310 100644 --- a/compiler/internal/dce/filters.go +++ b/compiler/internal/dce/filters.go @@ -26,7 +26,7 @@ func getFilters(o types.Object, tArgs []types.Type) (objectFilter, methodFilter if ptrType, ok := typ.(*types.Pointer); ok { typ = ptrType.Elem() } - if len(tArgs) <= 0 { + if len(tArgs) == 0 { tArgs = getTypeArgs(typ) } if named, ok := typ.(*types.Named); ok { @@ -48,17 +48,21 @@ func getFilters(o types.Object, tArgs []types.Type) (objectFilter, methodFilter // getObjectFilter returns the object filter that functions as the primary // name when determining if a declaration is alive or not. -// See [naming design](./README.md#naming) for more information. +// See [naming design] for more information. +// +// [naming design]: https://github.com/gopherjs/gopherjs/compiler/internal/dce/README.md#naming func getObjectFilter(o types.Object, tArgs []types.Type) string { return (&filterGen{argTypeRemap: tArgs}).Object(o, tArgs) } // getMethodFilter returns the method filter that functions as the secondary // name when determining if a declaration is alive or not. -// See [naming design](./README.md#naming) for more information. +// See [naming design] for more information. +// +// [naming design]: https://github.com/gopherjs/gopherjs/compiler/internal/dce/README.md#naming func getMethodFilter(o types.Object, tArgs []types.Type) string { if sig, ok := o.Type().(*types.Signature); ok { - if len(tArgs) <= 0 { + if len(tArgs) == 0 { if recv := sig.Recv(); recv != nil { tArgs = getTypeArgs(recv.Type()) } @@ -167,7 +171,7 @@ func (gen *filterGen) Object(o types.Object, tArgs []types.Type) string { filter := objectName(o) // Add additional type information for generics and instances. - if len(tArgs) <= 0 { + if len(tArgs) == 0 { tArgs = getTypeArgs(o.Type()) } if len(tArgs) > 0 { @@ -298,10 +302,10 @@ func (gen *filterGen) Interface(inter *types.Interface) string { // e.g. `interface { a(); b() }` is the same as `interface { b(); a() }`. sort.Strings(parts) - if len(parts) <= 0 { + if len(parts) == 0 { return `any` } - if inter.NumMethods() <= 0 && len(parts) == 1 { + if inter.NumMethods() == 0 && len(parts) == 1 { return parts[0] // single constraint union, i.e. `bool|~int|string` } return `interface{ ` + strings.Join(parts, `; `) + ` }` @@ -309,7 +313,7 @@ func (gen *filterGen) Interface(inter *types.Interface) string { // Struct returns the filter part for a struct type. func (gen *filterGen) Struct(s *types.Struct) string { - if s.NumFields() <= 0 { + if s.NumFields() == 0 { return `struct{}` } parts := make([]string, s.NumFields()) From 3a364dcdde1cee70e6e2397a541932a416a60eb9 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Mon, 28 Oct 2024 13:42:17 -0600 Subject: [PATCH 22/58] Fixed issue in len with string concatenation in argument --- compiler/compiler_test.go | 45 +++++++++++++++++++++++++++++++++++++++ compiler/expressions.go | 4 ++++ 2 files changed, 49 insertions(+) diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 0fc4e75c7..65178e986 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -360,6 +360,51 @@ func TestDeclSelection_RemoveUnusedTypeConstraint(t *testing.T) { sel.InitCode.IsDead(`ghost = new Bar\[\d+ /\* int \*/\]\.ptr\(7\)`) } +func TestLengthParenthesizingIssue841(t *testing.T) { + // See issue https://github.com/gopherjs/gopherjs/issues/841 + // + // Summary: Given `len(a+b)` where a and b are strings being concatenated + // together, the result was `a + b.length` instead of `(a+b).length`. + // + // The fix was to check if the expression in `len` is a binary + // expression or not. If it is, then the expression is parenthesized. + // This will work for concatenations any combination of variables and + // literals but won't pick up `len(Foo(a+b))` or `len(a[0:i+3])`. + + src := ` + package main + + func main() { + a := "a" + b := "b" + ab := a + b + if len(a+b) != len(ab) { + panic("unreachable") + } + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + archives := compileProject(t, root, false) + mainPkg := archives[root.PkgPath] + + badRegex := regexp.MustCompile(`a\s*\+\s*b\.length`) + goodRegex := regexp.MustCompile(`\(a\s*\+\s*b\)\.length`) + goodFound := false + for i, decl := range mainPkg.Declarations { + if badRegex.Match(decl.DeclCode) { + t.Errorf("found length issue in decl #%d: %s", i, decl.FullName) + t.Logf("decl code:\n%s", string(decl.DeclCode)) + } + if goodRegex.Match(decl.DeclCode) { + goodFound = true + } + } + if !goodFound { + t.Error("parenthesized length not found") + } +} + func compareOrder(t *testing.T, sourceFiles []srctesting.Source, minify bool) { t.Helper() outputNormal := compile(t, sourceFiles, minify) diff --git a/compiler/expressions.go b/compiler/expressions.go index 0390290e6..f1c2e68f5 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -982,6 +982,10 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args case "len": switch argType := fc.typeOf(args[0]).Underlying().(type) { case *types.Basic: + // If the argument is a concatenation of strings, then add parentheses. + if _, ok := args[0].(*ast.BinaryExpr); ok { + return fc.formatExpr("(%e).length", args[0]) + } return fc.formatExpr("%e.length", args[0]) case *types.Slice: return fc.formatExpr("%e.$length", args[0]) From 54b13af62133620569af6e1bc64494ed7cde169e Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 31 Oct 2024 11:22:12 -0600 Subject: [PATCH 23/58] Improving return with defers blocking --- compiler/internal/analysis/defer.go | 108 ++++++ compiler/internal/analysis/info.go | 195 ++++++++--- compiler/internal/analysis/info_test.go | 448 ++++++++++++++++++++++-- compiler/internal/analysis/return.go | 21 ++ compiler/internal/dce/info.go | 1 - internal/srctesting/srctesting.go | 31 ++ 6 files changed, 729 insertions(+), 75 deletions(-) create mode 100644 compiler/internal/analysis/defer.go create mode 100644 compiler/internal/analysis/return.go diff --git a/compiler/internal/analysis/defer.go b/compiler/internal/analysis/defer.go new file mode 100644 index 000000000..63438ba23 --- /dev/null +++ b/compiler/internal/analysis/defer.go @@ -0,0 +1,108 @@ +package analysis + +import ( + "go/ast" + + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" +) + +// deferStmt represents a defer statement that is blocking or not. +// +// A blocking defer statement will cause a return statement to be blocking +// since the defer is called and potentially blocked while leaving the method. +// We try to determine which defers affect which returns so that we only +// mark returns as blocking if they are affected by a blocking defer. +// In general we know that a defer will affect all returns that have been +// declared after the defer statement. +// +// Since analysis doesn't create [CFG] basic blocks for full control +// flow analysis we can't easily determine several cases: +// +// - Terminating if-statements(i.e. does the body of the if-statement always +// return from the method) are difficult to determine. Any defer that is +// added whilst inside a terminating if-statement body can only affect the +// returns inside that if-statement body. +// Otherwise, the defer may affect returns after the if-statement block has +// rejoined the flow that it branched from. Since terminating if-statements +// are difficult to determine without [CFG] blocks, we treat all +// if-statements as if they are not terminating. +// That means there may be some false positives, since returns declared +// after a terminating branch will be marked as affected by a defer +// declared in that branch, when in reality they are not. +// +// - Same as above but for else blocks, switch cases, and any branching. +// +// - Loops (i.e. for-statements and for-range-statements) can cause return +// statements declared earlier in the loop to be affected by defers +// declared after it in the loop. We can't determine which branches in a +// loop may return to the start of the loop so we assume anywhere inside +// of a loop can return to the start of the loop. +// To handle this, all defers defined anywhere within a loop are assumed +// to affect any return also defined in that loop. +// We only need to track the top-level loop since nested loops will be +// superseded by the top-level loop. +// +// - Labels and goto's are similar to loops in [CFG] blocks but without those +// blocks it's harder to determine which defers will affect which returns. +// To be safe, for any function with any blocking defers, returns, and +// goto's, all the returns are defaulted to blocking. +// +// [CFG]: https://en.wikipedia.org/wiki/Control-flow_graph +type deferStmt struct { + isBlocking func(info *Info) (bool, bool) +} + +// newBlockingDefer creates a new defer statement that is blocking or not. +// If the defer is calling a js.Object method then the defer is non-blocking. +// If the defers calling an interface method or function pointer in a var +// then the defer is blocking. +func newDefer(blocking bool) *deferStmt { + return &deferStmt{ + isBlocking: func(info *Info) (bool, bool) { + return blocking, false + }, + } +} + +// newInstDefer creates a new defer statement for an instances of a method. +// The instance is used to look up the blocking information later. +func newInstDefer(inst typeparams.Instance) *deferStmt { + return &deferStmt{ + isBlocking: func(info *Info) (bool, bool) { + return info.FuncInfo(inst).IsBlocking(), true + }, + } +} + +// newLitDefer creates a new defer statement for a function literal. +// The literal is used to look up the blocking information later. +func newLitDefer(lit *ast.FuncLit) *deferStmt { + return &deferStmt{ + isBlocking: func(info *Info) (bool, bool) { + return info.FuncLitInfo(lit).IsBlocking(), true + }, + } +} + +// IsBlocking determines if the defer statement is blocking or not. +// +// The result will be cached for future calls since each return statement +// will check the same defers (plus any new defers) of prior return statements. +func (d *deferStmt) IsBlocking(info *Info) bool { + blocking, cacheResult := d.isBlocking(info) + if cacheResult { + d.isBlocking = func(info *Info) (bool, bool) { + return blocking, false + } + } + return blocking +} + +func isAnyDeferBlocking(deferStmts []*deferStmt, info *Info) bool { + for _, def := range deferStmts { + if def.IsBlocking(info) { + return true + } + } + return false +} diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go index 6ab8a2f26..fdd0ba906 100644 --- a/compiler/internal/analysis/info.go +++ b/compiler/internal/analysis/info.go @@ -68,8 +68,9 @@ func (info *Info) newFuncInfo(n ast.Node, inst *typeparams.Instance) *FuncInfo { Flattened: make(map[ast.Node]bool), Blocking: make(map[ast.Node]bool), GotoLabel: make(map[*types.Label]bool), + loopReturnIndex: -1, localInstCallees: new(typeparams.InstanceMap[[]astPath]), - literalFuncCallees: make(map[*ast.FuncLit][]astPath), + literalFuncCallees: make(map[*ast.FuncLit]astPath), } // Register the function in the appropriate map. @@ -169,21 +170,6 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info ast.Walk(info.InitFuncInfo, file) } - for _, funcInfo := range info.allInfos { - if !funcInfo.HasDefer { - continue - } - // Conservatively assume that if a function has a deferred call, it might be - // blocking, and therefore all return statements need to be treated as - // blocking. - // TODO(nevkontakte): This could be improved by detecting whether a deferred - // call is actually blocking. Doing so might reduce generated code size a - // bit. - for _, returnStmt := range funcInfo.returnStmts { - funcInfo.markBlocking(returnStmt) - } - } - // Propagate information about blocking calls to the caller functions. // For each function we check all other functions it may call and if any of // them are blocking, we mark the caller blocking as well. The process is @@ -203,11 +189,9 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info }) // Check direct calls to function literals. - for callee, callSites := range caller.literalFuncCallees { + for callee, callSite := range caller.literalFuncCallees { if info.FuncLitInfo(callee).IsBlocking() { - for _, callSite := range callSites { - caller.markBlocking(callSite) - } + caller.markBlocking(callSite) delete(caller.literalFuncCallees, callee) done = false } @@ -221,19 +205,15 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info // After all function blocking information was propagated, mark flow control // statements as blocking whenever they may lead to a blocking function call. for _, funcInfo := range info.allInfos { - for _, continueStmt := range funcInfo.continueStmts { - if funcInfo.Blocking[continueStmt.forStmt.Post] { - // If a for-loop post-expression is blocking, the continue statement - // that leads to it must be treated as blocking. - funcInfo.markBlocking(continueStmt.analyzeStack) - } - } + funcInfo.propagateReturnBlocking() + funcInfo.propagateContinueBlocking() } return info } type FuncInfo struct { + // HasDefer indicates if any defer statement exists in the function. HasDefer bool // Nodes are "flattened" into a switch-case statement when we need to be able // to jump into an arbitrary position in the code with a GOTO statement, or @@ -248,16 +228,27 @@ type FuncInfo struct { // List of continue statements in the function. continueStmts []continueStmt // List of return statements in the function. - returnStmts []astPath + returnStmts []returnStmt + // List of most of the deferred function calls in the function. + // This is built up as the function is analyzed so that we can mark all + // return statements with the defers that each return would need to call. + deferStmts []*deferStmt + // The index of the return statement that was analyzed prior to a top-level + // loop starting. This is used to determine which return statements + // were added within the loop so that they can be updated to reflect all + // the defers that were added anywhere inside the loop. This is because + // returns defined before any defers in a loop may still be affected by + // those defers because of the loop. See comment on [deferStmt]. + loopReturnIndex int // List of other named functions from the current package this function calls. // If any of them are blocking, this function will become blocking too. localInstCallees *typeparams.InstanceMap[[]astPath] // List of function literals directly called from this function (for example: // `func() { /* do stuff */ }()`). This is distinct from function literals // assigned to named variables (for example: `doStuff := func() {}; - // doStuff()`), which are handled by localNamedCallees. If any of them are + // doStuff()`), which are handled by localInstCallees. If any of them are // identified as blocking, this function will become blocking too. - literalFuncCallees map[*ast.FuncLit][]astPath + literalFuncCallees map[*ast.FuncLit]astPath // resolver is used by this function instance to resolve any type arguments // for internal function calls. // This may be nil if not an instance of a generic function. @@ -275,6 +266,49 @@ func (fi *FuncInfo) IsBlocking() bool { return fi == nil || len(fi.Blocking) != 0 } +// propagateReturnBlocking updates the blocking on the return statements. +// See comment on [deferStmt]. +// +// This should only be called once when finishing analysis and only after +// all functions have been analyzed and all blocking information has been +// propagated across functions. +func (fi *FuncInfo) propagateReturnBlocking() { + if len(fi.GotoLabel) > 0 { + // If there are any goto statements in the function then + // all the return statements are marked the same. + // If any defer is blocking, then all return statements are blocking. + if isAnyDeferBlocking(fi.deferStmts, fi.pkgInfo) { + for _, returnStmt := range fi.returnStmts { + fi.markBlocking(returnStmt.analyzeStack) + } + } + return + } + + for _, returnStmt := range fi.returnStmts { + // Check all the defer statements that affect the return statement, + // if any are blocking then the return statement is blocking. + if returnStmt.IsBlocking(fi) { + fi.markBlocking(returnStmt.analyzeStack) + } + } +} + +// propagateContinueBlocking updates the blocking on the continue statements. +// +// This should only be called once when finishing analysis and only after +// all functions have been analyzed and all blocking information has been +// propagated across functions. +func (fi *FuncInfo) propagateContinueBlocking() { + for _, continueStmt := range fi.continueStmts { + if fi.Blocking[continueStmt.forStmt.Post] { + // If a for-loop post-expression is blocking, the continue statement + // that leads to it must be treated as blocking. + fi.markBlocking(continueStmt.analyzeStack) + } + } +} + func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { if node == nil { if len(fi.visitorStack) != 0 { @@ -316,7 +350,7 @@ func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { } return fi case *ast.CallExpr: - return fi.visitCallExpr(n) + return fi.visitCallExpr(n, false) case *ast.SendStmt: // Sending into a channel is blocking. fi.markBlocking(fi.visitorStack) @@ -337,7 +371,41 @@ func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { // for-range loop over a channel is blocking. fi.markBlocking(fi.visitorStack) } - return fi + if fi.loopReturnIndex >= 0 { + // Already in a loop so just continue walking. + return fi + } + // Top-level for-loop, analyze it separately to be able to update + // returns with the defers that were added inside the loop. + // See comment on deferStmt. + fi.loopReturnIndex = len(fi.returnStmts) + // Analyze the for-loop's children. + ast.Walk(skipParentNode{then: fi}, n) + // After the for-loop is analyzed, update all return statements that + // were inside the loop with the resulting list of defer statements. + for i := fi.loopReturnIndex; i < len(fi.returnStmts); i++ { + fi.returnStmts[i].deferStmts = fi.deferStmts + } + fi.loopReturnIndex = -1 + return nil + case *ast.ForStmt: + if fi.loopReturnIndex >= 0 { + // Already in a loop so just continue walking. + return fi + } + // Top-level for-loop, analyze it separately to be able to update + // returns with the defers that were added inside the loop. + // See comment on deferStmt. + fi.loopReturnIndex = len(fi.returnStmts) + // Analyze the for-loop's children. + ast.Walk(skipParentNode{then: fi}, n) + // After the for-loop is analyzed, update all return statements that + // were inside the loop with the resulting list of defer statements. + for i := fi.loopReturnIndex; i < len(fi.returnStmts); i++ { + fi.returnStmts[i].deferStmts = fi.deferStmts + } + fi.loopReturnIndex = -1 + return nil case *ast.SelectStmt: for _, s := range n.Body.List { if s.(*ast.CommClause).Comm == nil { // default clause @@ -376,14 +444,12 @@ func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { return nil // The subtree was manually checked, no need to visit it again. case *ast.DeferStmt: fi.HasDefer = true - if funcLit, ok := n.Call.Fun.(*ast.FuncLit); ok { - ast.Walk(fi, funcLit.Body) - } - return fi + return fi.visitCallExpr(n.Call, true) case *ast.ReturnStmt: // Capture all return statements in the function. They could become blocking // if the function has a blocking deferred call. - fi.returnStmts = append(fi.returnStmts, fi.visitorStack.copy()) + rs := newReturnStmt(fi.visitorStack, fi.deferStmts) + fi.returnStmts = append(fi.returnStmts, rs) return fi default: return fi @@ -393,25 +459,27 @@ func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { // needs to be analyzed. } -func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { +func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr, forDefer bool) ast.Visitor { switch f := astutil.RemoveParens(n.Fun).(type) { case *ast.Ident: - fi.callToNamedFunc(fi.instanceForIdent(f)) + fi.callToNamedFunc(fi.instanceForIdent(f), forDefer) return fi case *ast.SelectorExpr: if sel := fi.pkgInfo.Selections[f]; sel != nil { if typesutil.IsJsObject(sel.Recv()) { - // js.Object methods are known to be non-blocking, but we still must - // check its arguments. + // js.Object methods are known to be non-blocking, + // but we still must check its arguments. + // We don't need to add a deferStmt when forDefer + // since that defer will always be non-blocking. return fi } // selection is a method call like `foo.Bar()`, where `foo` might // be generic and needs to be substituted with the type argument. - fi.callToNamedFunc(fi.instanceForSelection(sel)) + fi.callToNamedFunc(fi.instanceForSelection(sel), forDefer) return fi } - fi.callToNamedFunc(fi.instanceForIdent(f.Sel)) + fi.callToNamedFunc(fi.instanceForIdent(f.Sel), forDefer) return fi case *ast.FuncLit: // Collect info about the function literal itself. @@ -422,7 +490,10 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { ast.Walk(fi, arg) } // Register literal function call site in case it is identified as blocking. - fi.literalFuncCallees[f] = append(fi.literalFuncCallees[f], fi.visitorStack.copy()) + fi.literalFuncCallees[f] = fi.visitorStack.copy() + if forDefer { + fi.deferStmts = append(fi.deferStmts, newLitDefer(f)) + } return nil // No need to walk under this CallExpr, we already did it manually. case *ast.IndexExpr: // Collect info about the instantiated type or function, or index expression. @@ -435,7 +506,7 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { if astutil.IsTypeExpr(f.Index, fi.pkgInfo.Info) { // This is a call of an instantiation of a generic function, // e.g. `foo[int]` in `func foo[T any]() { ... }; func main() { foo[int]() }` - fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident))) + fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident)), forDefer) return fi } // The called function is gotten with an index or key from a map, array, or slice. @@ -443,6 +514,9 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { // Since we can't predict if the returned function will be blocking // or not, we have to be conservative and assume that function might be blocking. fi.markBlocking(fi.visitorStack) + if forDefer { + fi.deferStmts = append(fi.deferStmts, newDefer(true)) + } return fi case *ast.IndexListExpr: // Collect info about the instantiated type or function. @@ -454,7 +528,7 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { } // This is a call of an instantiation of a generic function, // e.g. `foo[int, bool]` in `func foo[T1, T2 any]() { ... }; func main() { foo[int, bool]() }` - fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident))) + fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident)), forDefer) return fi default: if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { @@ -465,6 +539,9 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { // The function is returned by a non-trivial expression. We have to be // conservative and assume that function might be blocking. fi.markBlocking(fi.visitorStack) + if forDefer { + fi.deferStmts = append(fi.deferStmts, newDefer(true)) + } return fi } } @@ -508,7 +585,7 @@ func (fi *FuncInfo) instanceForSelection(sel *types.Selection) typeparams.Instan return typeparams.Instance{Object: sel.Obj()} } -func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance) { +func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance, forDefer bool) { switch o := callee.Object.(type) { case *types.Func: o = o.Origin() @@ -516,9 +593,15 @@ func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance) { if _, ok := recv.Type().Underlying().(*types.Interface); ok { // Conservatively assume that an interface implementation may be blocking. fi.markBlocking(fi.visitorStack) + if forDefer { + fi.deferStmts = append(fi.deferStmts, newDefer(true)) + } return } } + if forDefer { + fi.deferStmts = append(fi.deferStmts, newInstDefer(callee)) + } if o.Pkg() != fi.pkgInfo.Pkg { if fi.pkgInfo.isImportedBlocking(callee) { fi.markBlocking(fi.visitorStack) @@ -533,6 +616,13 @@ func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance) { case *types.Var: // Conservatively assume that a function in a variable might be blocking. fi.markBlocking(fi.visitorStack) + if forDefer { + fi.deferStmts = append(fi.deferStmts, newDefer(true)) + } + default: + // No need to add defers for other call types, such as *types.Builtin, + // since those are considered non-blocking. + return } } @@ -548,3 +638,14 @@ func (fi *FuncInfo) markFlattened(stack astPath) { fi.Flattened[n] = true } } + +// skipParentNode is a visitor that skips the next node in the AST +// but will continue visiting the rest of the tree including the +// children of the skipped node. +type skipParentNode struct { + then ast.Visitor +} + +func (v skipParentNode) Visit(node ast.Node) ast.Visitor { + return v.then +} diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go index 62238c377..beaee0b40 100644 --- a/compiler/internal/analysis/info_test.go +++ b/compiler/internal/analysis/info_test.go @@ -3,6 +3,7 @@ package analysis import ( "go/ast" "go/types" + "sort" "testing" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" @@ -241,6 +242,9 @@ func TestBlocking_Defers_WithoutReturns_WithNamedFuncs(t *testing.T) { func notBlocking(c chan bool) { defer nonBlockingPrint(true) }`) + bt.assertFuncInstCount(5) + bt.assertFuncLitCount(0) + bt.assertBlocking(`blockingPrint`) bt.assertNotBlocking(`nonBlockingPrint`) @@ -279,9 +283,7 @@ func TestBlocking_Defers_WithReturns_WithFuncLiterals(t *testing.T) { bt.assertBlocking(`blockingArg`) bt.assertNotBlockingLit(11) - // TODO: The following is blocking because currently any defer with a return - // is assumed to be blocking. This limitation should be fixed in the future. - bt.assertBlocking(`notBlocking`) + bt.assertNotBlocking(`notBlocking`) bt.assertNotBlockingLit(18) } @@ -299,27 +301,370 @@ func TestBlocking_Defers_WithReturns_WithNamedFuncs(t *testing.T) { func blockingBody(c chan bool) int { defer blockingPrint(c) - return 42 + return 42 // line 13 } func blockingArg(c chan bool) int { defer nonBlockingPrint(<-c) - return 42 + return 42 // line 18 } func notBlocking(c chan bool) int { defer nonBlockingPrint(true) - return 42 + return 42 // line 23 }`) bt.assertBlocking(`blockingPrint`) bt.assertNotBlocking(`nonBlockingPrint`) bt.assertBlocking(`blockingBody`) + bt.assertBlockingReturn(13) + bt.assertBlocking(`blockingArg`) + // The defer is non-blocking so the return is not blocking + // even though the function is blocking. + bt.assertNotBlockingReturn(18) + + bt.assertNotBlocking(`notBlocking`) + bt.assertNotBlockingReturn(23) +} + +func TestBlocking_Defers_WithMultipleReturns(t *testing.T) { + bt := newBlockingTest(t, + `package test + + func foo(c chan int) bool { + defer func() { // line 4 + if r := recover(); r != nil { + println("Error", r) + } + }() + + if c == nil { + return false // line 11 + } + + defer func(v int) { // line 14 + println(v) + }(<-c) + + value := <-c + if value < 0 { + return false // line 20 + } + + if value > 0 { + defer func() { // line 24 + println(<-c) + }() + + return false // line 28 + } + + return true // line 31 + }`) + bt.assertBlocking(`foo`) + bt.assertNotBlockingLit(4) + // Early escape from function without blocking defers is not blocking. + bt.assertNotBlockingReturn(11) + bt.assertNotBlockingLit(14) + // Function has had blocking by this point but no blocking defers yet. + bt.assertNotBlockingReturn(20) + bt.assertBlockingLit(24) + // The return is blocking because of a blocking defer. + bt.assertBlockingReturn(28) + // Technically the return on line 31 is not blocking since the defer that + // is blocking can only exit through the return on line 28, but it would be + // difficult to determine which defers would only affect certain returns + // without doing full control flow analysis. + // + // TODO(grantnelson-wf): We could fix this at some point by keeping track + // of which flow control statements (e.g. if-statements) are terminating + // or not. Any defers added in a terminating control flow would not + // propagate to returns that are not in that block. + // + // For now we simply build up the list of defers as we go making + // the return on line 31 also blocking. + bt.assertBlockingReturn(31) +} + +func TestBlocking_Defers_WithReturnsAndDefaultBlocking(t *testing.T) { + bt := newBlockingTest(t, + `package test + + type foo struct {} + func (f foo) Bar() { + println("foo") + } + + type stringer interface { + Bar() + } + + var fb = foo{}.Bar - // TODO: The following is blocking because currently any defer with a return - // is assumed to be blocking. This limitation should be fixed in the future. - bt.assertBlocking(`notBlocking`) + func deferInterfaceCall() bool { + var s stringer = foo{} + defer s.Bar() + return true // line 17 + } + + func deferVarCall() bool { + defer fb() + return true // line 22 + } + + func deferLocalVarCall() bool { + fp := foo{}.Bar + defer fp() + return true // line 28 + } + + func deferMethodExpressionCall() bool { + fp := foo.Bar + defer fp(foo{}) + return true // line 34 + } + + func deferSlicedFuncCall() bool { + s := []func() { fb, foo{}.Bar } + defer s[0]() + return true // line 40 + } + + func deferMappedFuncCall() bool { + m := map[string]func() { + "fb": fb, + "fNew": foo{}.Bar, + } + defer m["fb"]() + return true // line 49 + }`) + + bt.assertFuncInstCount(7) + bt.assertNotBlocking(`foo.Bar`) + + // None of these are actually blocking but we treat them like they are + // because the defers invoke functions via interfaces and function pointers. + bt.assertBlocking(`deferInterfaceCall`) + bt.assertBlocking(`deferVarCall`) + bt.assertBlocking(`deferLocalVarCall`) + bt.assertBlocking(`deferMethodExpressionCall`) + bt.assertBlocking(`deferSlicedFuncCall`) + bt.assertBlocking(`deferMappedFuncCall`) + + // All of these returns are blocking because they have blocking defers. + bt.assertBlockingReturn(17) + bt.assertBlockingReturn(22) + bt.assertBlockingReturn(28) + bt.assertBlockingReturn(34) + bt.assertBlockingReturn(40) + bt.assertBlockingReturn(49) +} + +func TestBlocking_Defers_WithReturnsAndDeferBuiltin(t *testing.T) { + bt := newBlockingTest(t, + `package test + + type strSet map[string]bool + + func deferBuiltinCall() strSet { + m := strSet{ + "foo": true, + } + defer delete(m, "foo") + return m // line 10 + }`) + + bt.assertFuncInstCount(1) + bt.assertNotBlocking(`deferBuiltinCall`) + bt.assertNotBlockingReturn(10) +} + +func TestBlocking_Defers_WithReturnsInLoops(t *testing.T) { + // These are example of where a defer can affect the return that + // occurs prior to the defer in the function body. + bt := newBlockingTest(t, + `package test + + func blocking(c chan int) { + println(<-c) + } + + func deferInForLoop(c chan int) bool { + i := 1000 + for { + i-- + if i <= 0 { + return true // line 12 + } + defer blocking(c) + } + } + + func deferInForLoopReturnAfter(c chan int) bool { + for i := 1000; i > 0; i-- { + defer blocking(c) + } + return true // line 22 + } + + func deferInNamedForLoop(c chan int) bool { + i := 1000 + Start: + for { + i-- + if i <= 0 { + return true // line 31 + } + defer blocking(c) + continue Start + } + } + + func deferInNamedForLoopReturnAfter(c chan int) bool { + Start: + for i := 1000; i > 0; i-- { + defer blocking(c) + continue Start + } + return true // line 44 + } + + func deferInGotoLoop(c chan int) bool { + i := 1000 + Start: + i-- + if i <= 0 { + return true // line 52 + } + defer blocking(c) + goto Start + } + + func deferInGotoLoopReturnAfter(c chan int) bool { + i := 1000 + Start: + defer blocking(c) + i-- + if i > 0 { + goto Start + } + return true // line 66 + } + + func deferInRangeLoop(c chan int) bool { + s := []int{1, 2, 3} + for i := range s { + if i > 3 { + return true // line 73 + } + defer blocking(c) + } + return false // line 77 + }`) + + bt.assertFuncInstCount(8) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`deferInForLoop`) + bt.assertBlocking(`deferInForLoopReturnAfter`) + bt.assertBlocking(`deferInNamedForLoop`) + bt.assertBlocking(`deferInNamedForLoopReturnAfter`) + bt.assertBlocking(`deferInGotoLoop`) + bt.assertBlocking(`deferInGotoLoopReturnAfter`) + bt.assertBlocking(`deferInRangeLoop`) + // When the following 2 returns are defined there are no defers, however, + // because of the loop, the blocking defers defined after the return will + // block the returns. + bt.assertBlockingReturn(12) + bt.assertBlockingReturn(22) + bt.assertBlockingReturn(31) + bt.assertBlockingReturn(44) + bt.assertBlockingReturn(52) + bt.assertBlockingReturn(66) + bt.assertBlockingReturn(73) + bt.assertBlockingReturn(77) +} + +func TestBlocking_Defers_WithReturnsInLoopsInLoops(t *testing.T) { + // These are example of where a defer can affect the return that + // occurs prior to the defer in the function body. + bt := newBlockingTest(t, + `package test + + func blocking(c chan int) { + println(<-c) + } + + func forLoopTheLoop(c chan int) bool { + if c == nil { + return false // line 9 + } + for i := 0; i < 10; i++ { + if i > 3 { + return true // line 13 + } + for j := 0; j < 10; j++ { + if j > 3 { + return true // line 17 + } + defer blocking(c) + if j > 2 { + return false // line 21 + } + } + if i > 2 { + return false // line 25 + } + } + return false // line 28 + } + + func rangeLoopTheLoop(c chan int) bool { + data := []int{1, 2, 3} + for i := range data { + for j := range data { + if i + j > 3 { + return true // line 36 + } + } + defer blocking(c) + } + return false // line 41 + } + + func noopThenLoop(c chan int) bool { + data := []int{1, 2, 3} + for i := range data { + if i > 13 { + return true // line 48 + } + defer func() { println("hi") }() + } + for i := range data { + if i > 3 { + return true // line 54 + } + defer blocking(c) + } + return false // line 58 + }`) + + bt.assertFuncInstCount(4) + bt.assertBlocking(`blocking`) + bt.assertBlocking(`forLoopTheLoop`) + bt.assertNotBlockingReturn(9) + bt.assertBlockingReturn(13) + bt.assertBlockingReturn(17) + bt.assertBlockingReturn(21) + bt.assertBlockingReturn(25) + bt.assertBlockingReturn(28) + bt.assertBlocking(`rangeLoopTheLoop`) + bt.assertBlockingReturn(36) + bt.assertBlockingReturn(41) + bt.assertBlocking(`noopThenLoop`) + bt.assertNotBlockingReturn(48) + bt.assertBlockingReturn(54) + bt.assertBlockingReturn(58) } func TestBlocking_Returns_WithoutDefers(t *testing.T) { @@ -327,19 +672,40 @@ func TestBlocking_Returns_WithoutDefers(t *testing.T) { `package test func blocking(c chan bool) bool { - return <-c + return <-c // line 4 + } + + func blockingBeforeReturn(c chan bool) bool { + v := <-c + return v // line 9 } func indirectlyBlocking(c chan bool) bool { - return blocking(c) + return blocking(c) // line 13 + } + + func indirectlyBlockingBeforeReturn(c chan bool) bool { + v := blocking(c) + return v // line 18 } func notBlocking(c chan bool) bool { - return true + return true // line 22 }`) bt.assertBlocking(`blocking`) + bt.assertBlockingReturn(4) + + bt.assertBlocking(`blockingBeforeReturn`) + bt.assertNotBlockingReturn(9) + bt.assertBlocking(`indirectlyBlocking`) + bt.assertBlockingReturn(13) + + bt.assertBlocking(`indirectlyBlockingBeforeReturn`) + bt.assertNotBlockingReturn(18) + bt.assertNotBlocking(`notBlocking`) + bt.assertNotBlockingReturn(22) } func TestBlocking_FunctionLiteral(t *testing.T) { @@ -1126,13 +1492,27 @@ func newBlockingTestWithOtherPackage(t *testing.T, testSrc string, otherSrc stri func (bt *blockingTest) assertFuncInstCount(expCount int) { if got := bt.pkgInfo.funcInstInfos.Len(); got != expCount { - bt.f.T.Errorf(`Got %d function infos but expected %d.`, got, expCount) + bt.f.T.Errorf(`Got %d function instance infos but expected %d.`, got, expCount) for i, inst := range bt.pkgInfo.funcInstInfos.Keys() { bt.f.T.Logf(` %d. %q`, i+1, inst.TypeString()) } } } +func (bt *blockingTest) assertFuncLitCount(expCount int) { + if got := len(bt.pkgInfo.funcLitInfos); got != expCount { + bt.f.T.Errorf(`Got %d function literal infos but expected %d.`, got, expCount) + pos := make([]string, 0, len(bt.pkgInfo.funcLitInfos)) + for fl := range bt.pkgInfo.funcLitInfos { + pos = append(pos, bt.f.FileSet.Position(fl.Pos()).String()) + } + sort.Strings(pos) + for i := range pos { + bt.f.T.Logf(` %d. %q`, i+1, pos) + } + } +} + func (bt *blockingTest) assertBlocking(funcName string) { if !bt.isTypesFuncBlocking(funcName) { bt.f.T.Errorf(`Got %q as not blocking but expected it to be blocking.`, funcName) @@ -1194,22 +1574,10 @@ func (bt *blockingTest) assertNotBlockingLit(lineNo int) { } } -func (bt *blockingTest) getFuncLitLineNo(fl *ast.FuncLit) int { - return bt.f.FileSet.Position(fl.Pos()).Line -} - func (bt *blockingTest) isFuncLitBlocking(lineNo int) bool { - var fnLit *ast.FuncLit - ast.Inspect(bt.file, func(n ast.Node) bool { - if fl, ok := n.(*ast.FuncLit); ok && bt.getFuncLitLineNo(fl) == lineNo { - fnLit = fl - return false - } - return fnLit == nil - }) - + fnLit := srctesting.GetNodeAtLineNo[*ast.FuncLit](bt.file, bt.f.FileSet, lineNo) if fnLit == nil { - bt.f.T.Fatalf(`FuncLit found on line %d not found in the AST.`, lineNo) + bt.f.T.Fatalf(`FuncLit on line %d not found in the AST.`, lineNo) } return bt.pkgInfo.FuncLitInfo(fnLit).IsBlocking() } @@ -1240,3 +1608,29 @@ func (bt *blockingTest) isFuncInstBlocking(instanceStr string) bool { bt.f.T.Fatalf(`No function instance found for %q in package info.`, instanceStr) return false } + +func (bt *blockingTest) assertBlockingReturn(lineNo int) { + if !bt.isReturnBlocking(lineNo) { + bt.f.T.Errorf(`Got return at line %d as not blocking but expected it to be blocking.`, lineNo) + } +} + +func (bt *blockingTest) assertNotBlockingReturn(lineNo int) { + if bt.isReturnBlocking(lineNo) { + bt.f.T.Errorf(`Got return at line %d as blocking but expected it to be not blocking.`, lineNo) + } +} + +func (bt *blockingTest) isReturnBlocking(lineNo int) bool { + ret := srctesting.GetNodeAtLineNo[*ast.ReturnStmt](bt.file, bt.f.FileSet, lineNo) + if ret == nil { + bt.f.T.Fatalf(`ReturnStmt on line %d not found in the AST.`, lineNo) + } + for _, info := range bt.pkgInfo.allInfos { + if blocking, found := info.Blocking[ret]; found { + return blocking + } + } + // If not found in any info.Blocking, then it is not blocking. + return false +} diff --git a/compiler/internal/analysis/return.go b/compiler/internal/analysis/return.go new file mode 100644 index 000000000..3c83b3c1f --- /dev/null +++ b/compiler/internal/analysis/return.go @@ -0,0 +1,21 @@ +package analysis + +// returnStmt represents a return statement that is blocking or not. +type returnStmt struct { + analyzeStack astPath + deferStmts []*deferStmt +} + +func newReturnStmt(stack astPath, deferStmts []*deferStmt) returnStmt { + return returnStmt{ + analyzeStack: stack.copy(), + deferStmts: deferStmts, + } +} + +// IsBlocking determines if the return statement is blocking or not +// based on the defer statements that affect the return. +// The return may still be blocking if the function has labels and goto's. +func (r returnStmt) IsBlocking(info *FuncInfo) bool { + return isAnyDeferBlocking(r.deferStmts, info.pkgInfo) +} diff --git a/compiler/internal/dce/info.go b/compiler/internal/dce/info.go index 010fc2a21..07d818855 100644 --- a/compiler/internal/dce/info.go +++ b/compiler/internal/dce/info.go @@ -10,7 +10,6 @@ import ( // Info contains information used by the dead-code elimination (DCE) logic to // determine whether a declaration is alive or dead. type Info struct { - // alive indicates if the declaration is marked as alive // and will not be eliminated. alive bool diff --git a/internal/srctesting/srctesting.go b/internal/srctesting/srctesting.go index 35d9e25cc..83499c6dd 100644 --- a/internal/srctesting/srctesting.go +++ b/internal/srctesting/srctesting.go @@ -245,3 +245,34 @@ func ParseSources(t *testing.T, sourceFiles []Source, auxFiles []Source) *packag } return pkgs[0] } + +// GetNodeAtLineNo returns the first node of type N that starts on the given +// line in the given file. This helps lookup nodes that aren't named but +// are needed by a specific test. +func GetNodeAtLineNo[N ast.Node](file *ast.File, fSet *token.FileSet, lineNo int) N { + var node N + keepLooking := true + ast.Inspect(file, func(n ast.Node) bool { + if n == nil || !keepLooking { + return false + } + nodeLine := fSet.Position(n.Pos()).Line + switch { + case nodeLine < lineNo: + // We haven't reached the line yet, so check if we can skip over + // this whole node or if we should look inside it. + return fSet.Position(n.End()).Line >= lineNo + case nodeLine > lineNo: + // We went past it without finding it, so stop looking. + keepLooking = false + return false + default: // nodeLine == lineNo + if n, ok := n.(N); ok { + node = n + keepLooking = false + } + return keepLooking + } + }) + return node +} From fe49d93473b91809ed5da38015399e28a27838e7 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 14 Nov 2024 11:16:47 -0700 Subject: [PATCH 24/58] made defer more debuggable and some other fixes --- compiler/internal/analysis/defer.go | 44 +++++------- compiler/internal/analysis/info.go | 108 +++++++++++++++------------- 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/compiler/internal/analysis/defer.go b/compiler/internal/analysis/defer.go index 63438ba23..36b19ef2a 100644 --- a/compiler/internal/analysis/defer.go +++ b/compiler/internal/analysis/defer.go @@ -49,53 +49,43 @@ import ( // // [CFG]: https://en.wikipedia.org/wiki/Control-flow_graph type deferStmt struct { - isBlocking func(info *Info) (bool, bool) + inst *typeparams.Instance + lit *ast.FuncLit } -// newBlockingDefer creates a new defer statement that is blocking or not. +// newBlockingDefer creates a new defer statement that is blocking. +// // If the defer is calling a js.Object method then the defer is non-blocking. // If the defers calling an interface method or function pointer in a var // then the defer is blocking. -func newDefer(blocking bool) *deferStmt { - return &deferStmt{ - isBlocking: func(info *Info) (bool, bool) { - return blocking, false - }, - } +func newBlockingDefer() *deferStmt { + return &deferStmt{} } // newInstDefer creates a new defer statement for an instances of a method. // The instance is used to look up the blocking information later. func newInstDefer(inst typeparams.Instance) *deferStmt { - return &deferStmt{ - isBlocking: func(info *Info) (bool, bool) { - return info.FuncInfo(inst).IsBlocking(), true - }, - } + return &deferStmt{inst: &inst} } // newLitDefer creates a new defer statement for a function literal. // The literal is used to look up the blocking information later. func newLitDefer(lit *ast.FuncLit) *deferStmt { - return &deferStmt{ - isBlocking: func(info *Info) (bool, bool) { - return info.FuncLitInfo(lit).IsBlocking(), true - }, - } + return &deferStmt{lit: lit} } // IsBlocking determines if the defer statement is blocking or not. -// -// The result will be cached for future calls since each return statement -// will check the same defers (plus any new defers) of prior return statements. func (d *deferStmt) IsBlocking(info *Info) bool { - blocking, cacheResult := d.isBlocking(info) - if cacheResult { - d.isBlocking = func(info *Info) (bool, bool) { - return blocking, false - } + // If the instance or the literal is set then we can look up the blocking, + // otherwise assume blocking because otherwise the defer wouldn't + // have been recorded. + if d.inst != nil { + return info.FuncInfo(*d.inst).IsBlocking() + } + if d.lit != nil { + return info.FuncLitInfo(d.lit).IsBlocking() } - return blocking + return true } func isAnyDeferBlocking(deferStmts []*deferStmt, info *Info) bool { diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go index fdd0ba906..237697d1b 100644 --- a/compiler/internal/analysis/info.go +++ b/compiler/internal/analysis/info.go @@ -170,46 +170,56 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info ast.Walk(info.InitFuncInfo, file) } - // Propagate information about blocking calls to the caller functions. - // For each function we check all other functions it may call and if any of - // them are blocking, we mark the caller blocking as well. The process is - // repeated until no new blocking functions is detected. - for { - done := true - for _, caller := range info.allInfos { - // Check calls to named functions and function-typed variables. - caller.localInstCallees.Iterate(func(callee typeparams.Instance, callSites []astPath) { - if info.FuncInfo(callee).IsBlocking() { - for _, callSite := range callSites { - caller.markBlocking(callSite) - } - caller.localInstCallees.Delete(callee) - done = false - } - }) + done := false + for !done { + done = info.propagateFunctionBlocking() + } + + info.propagateControlStatementBlocking() + return info +} - // Check direct calls to function literals. - for callee, callSite := range caller.literalFuncCallees { - if info.FuncLitInfo(callee).IsBlocking() { +// propagateFunctionBlocking propagates information about blocking calls +// to the caller functions. Returns true if done, false if more iterations +// are needed. +// +// For each function we check all other functions it may call and if any of +// them are blocking, we mark the caller blocking as well. The process is +// repeated until no new blocking functions is detected. +func (info *Info) propagateFunctionBlocking() bool { + done := true + for _, caller := range info.allInfos { + // Check calls to named functions and function-typed variables. + caller.localInstCallees.Iterate(func(callee typeparams.Instance, callSites []astPath) { + if info.FuncInfo(callee).IsBlocking() { + for _, callSite := range callSites { caller.markBlocking(callSite) - delete(caller.literalFuncCallees, callee) - done = false } + caller.localInstCallees.Delete(callee) + done = false + } + }) + + // Check direct calls to function literals. + for callee, callSite := range caller.literalFuncCallees { + if info.FuncLitInfo(callee).IsBlocking() { + caller.markBlocking(callSite) + delete(caller.literalFuncCallees, callee) + done = false } - } - if done { - break } } + return done +} - // After all function blocking information was propagated, mark flow control - // statements as blocking whenever they may lead to a blocking function call. +// propagateControlStatementBlocking is called after all function blocking +// information was propagated, mark flow control statements as blocking +// whenever they may lead to a blocking function call. +func (info *Info) propagateControlStatementBlocking() { for _, funcInfo := range info.allInfos { funcInfo.propagateReturnBlocking() funcInfo.propagateContinueBlocking() } - - return info } type FuncInfo struct { @@ -229,7 +239,7 @@ type FuncInfo struct { continueStmts []continueStmt // List of return statements in the function. returnStmts []returnStmt - // List of most of the deferred function calls in the function. + // List of deferred function calls which could be blocking. // This is built up as the function is analyzed so that we can mark all // return statements with the defers that each return would need to call. deferStmts []*deferStmt @@ -459,27 +469,27 @@ func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { // needs to be analyzed. } -func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr, forDefer bool) ast.Visitor { +func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr, deferredCall bool) ast.Visitor { switch f := astutil.RemoveParens(n.Fun).(type) { case *ast.Ident: - fi.callToNamedFunc(fi.instanceForIdent(f), forDefer) + fi.callToNamedFunc(fi.instanceForIdent(f), deferredCall) return fi case *ast.SelectorExpr: if sel := fi.pkgInfo.Selections[f]; sel != nil { if typesutil.IsJsObject(sel.Recv()) { // js.Object methods are known to be non-blocking, // but we still must check its arguments. - // We don't need to add a deferStmt when forDefer - // since that defer will always be non-blocking. + // We don't need to add a deferStmt when `deferredCall` + // is true, since that defer will always be non-blocking. return fi } // selection is a method call like `foo.Bar()`, where `foo` might // be generic and needs to be substituted with the type argument. - fi.callToNamedFunc(fi.instanceForSelection(sel), forDefer) + fi.callToNamedFunc(fi.instanceForSelection(sel), deferredCall) return fi } - fi.callToNamedFunc(fi.instanceForIdent(f.Sel), forDefer) + fi.callToNamedFunc(fi.instanceForIdent(f.Sel), deferredCall) return fi case *ast.FuncLit: // Collect info about the function literal itself. @@ -491,7 +501,7 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr, forDefer bool) ast.Visitor { } // Register literal function call site in case it is identified as blocking. fi.literalFuncCallees[f] = fi.visitorStack.copy() - if forDefer { + if deferredCall { fi.deferStmts = append(fi.deferStmts, newLitDefer(f)) } return nil // No need to walk under this CallExpr, we already did it manually. @@ -506,7 +516,7 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr, forDefer bool) ast.Visitor { if astutil.IsTypeExpr(f.Index, fi.pkgInfo.Info) { // This is a call of an instantiation of a generic function, // e.g. `foo[int]` in `func foo[T any]() { ... }; func main() { foo[int]() }` - fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident)), forDefer) + fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident)), deferredCall) return fi } // The called function is gotten with an index or key from a map, array, or slice. @@ -514,8 +524,8 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr, forDefer bool) ast.Visitor { // Since we can't predict if the returned function will be blocking // or not, we have to be conservative and assume that function might be blocking. fi.markBlocking(fi.visitorStack) - if forDefer { - fi.deferStmts = append(fi.deferStmts, newDefer(true)) + if deferredCall { + fi.deferStmts = append(fi.deferStmts, newBlockingDefer()) } return fi case *ast.IndexListExpr: @@ -528,7 +538,7 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr, forDefer bool) ast.Visitor { } // This is a call of an instantiation of a generic function, // e.g. `foo[int, bool]` in `func foo[T1, T2 any]() { ... }; func main() { foo[int, bool]() }` - fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident)), forDefer) + fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident)), deferredCall) return fi default: if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { @@ -539,8 +549,8 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr, forDefer bool) ast.Visitor { // The function is returned by a non-trivial expression. We have to be // conservative and assume that function might be blocking. fi.markBlocking(fi.visitorStack) - if forDefer { - fi.deferStmts = append(fi.deferStmts, newDefer(true)) + if deferredCall { + fi.deferStmts = append(fi.deferStmts, newBlockingDefer()) } return fi } @@ -585,7 +595,7 @@ func (fi *FuncInfo) instanceForSelection(sel *types.Selection) typeparams.Instan return typeparams.Instance{Object: sel.Obj()} } -func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance, forDefer bool) { +func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance, deferredCall bool) { switch o := callee.Object.(type) { case *types.Func: o = o.Origin() @@ -593,13 +603,13 @@ func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance, forDefer bool) { if _, ok := recv.Type().Underlying().(*types.Interface); ok { // Conservatively assume that an interface implementation may be blocking. fi.markBlocking(fi.visitorStack) - if forDefer { - fi.deferStmts = append(fi.deferStmts, newDefer(true)) + if deferredCall { + fi.deferStmts = append(fi.deferStmts, newBlockingDefer()) } return } } - if forDefer { + if deferredCall { fi.deferStmts = append(fi.deferStmts, newInstDefer(callee)) } if o.Pkg() != fi.pkgInfo.Pkg { @@ -616,8 +626,8 @@ func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance, forDefer bool) { case *types.Var: // Conservatively assume that a function in a variable might be blocking. fi.markBlocking(fi.visitorStack) - if forDefer { - fi.deferStmts = append(fi.deferStmts, newDefer(true)) + if deferredCall { + fi.deferStmts = append(fi.deferStmts, newBlockingDefer()) } default: // No need to add defers for other call types, such as *types.Builtin, From 6e01eab47177db80b1686e867fac20f0487c4e47 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 14 Nov 2024 11:32:19 -0700 Subject: [PATCH 25/58] fixed calling a defer across packages --- compiler/internal/analysis/info.go | 11 +++++--- compiler/internal/analysis/info_test.go | 35 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go index 237697d1b..42918258e 100644 --- a/compiler/internal/analysis/info.go +++ b/compiler/internal/analysis/info.go @@ -609,20 +609,25 @@ func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance, deferredCall boo return } } - if deferredCall { - fi.deferStmts = append(fi.deferStmts, newInstDefer(callee)) - } + if o.Pkg() != fi.pkgInfo.Pkg { if fi.pkgInfo.isImportedBlocking(callee) { fi.markBlocking(fi.visitorStack) + if deferredCall { + fi.deferStmts = append(fi.deferStmts, newBlockingDefer()) + } } return } + // We probably don't know yet whether the callee function is blocking. // Record the calls site for the later stage. paths := fi.localInstCallees.Get(callee) paths = append(paths, fi.visitorStack.copy()) fi.localInstCallees.Set(callee, paths) + if deferredCall { + fi.deferStmts = append(fi.deferStmts, newInstDefer(callee)) + } case *types.Var: // Conservatively assume that a function in a variable might be blocking. fi.markBlocking(fi.visitorStack) diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go index beaee0b40..d4e21b501 100644 --- a/compiler/internal/analysis/info_test.go +++ b/compiler/internal/analysis/info_test.go @@ -708,6 +708,41 @@ func TestBlocking_Returns_WithoutDefers(t *testing.T) { bt.assertNotBlockingReturn(22) } +func TestBlocking_Defers_WithReturnsAndOtherPackages(t *testing.T) { + otherSrc := `package other + + func Blocking() { + c := make(chan int) + println(<-c) + } + + func NotBlocking() { + println("Hello") + }` + + testSrc := `package test + + import "pkg/other" + + func deferOtherBlocking() bool { + defer other.Blocking() + return true // line 7 + } + + func deferOtherNotBlocking() bool { + defer other.NotBlocking() + return true // line 12 + }` + + bt := newBlockingTestWithOtherPackage(t, testSrc, otherSrc) + + bt.assertBlocking(`deferOtherBlocking`) + bt.assertBlockingReturn(7) + + bt.assertNotBlocking(`deferOtherNotBlocking`) + bt.assertNotBlockingReturn(12) +} + func TestBlocking_FunctionLiteral(t *testing.T) { // See: https://github.com/gopherjs/gopherjs/issues/955. bt := newBlockingTest(t, From 010220ae3d82e34f84b3d15690f219996df26c3f Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 21 Nov 2024 09:44:49 -0700 Subject: [PATCH 26/58] fixing some issues in analysis around generics --- compiler/functions.go | 2 +- compiler/internal/analysis/defer.go | 19 +- compiler/internal/analysis/info.go | 94 ++++-- compiler/internal/analysis/info_test.go | 406 +++++++++++++++++------ compiler/internal/typeparams/map.go | 18 +- compiler/internal/typeparams/map_test.go | 2 +- compiler/typesutil/typelist.go | 13 + 7 files changed, 393 insertions(+), 161 deletions(-) diff --git a/compiler/functions.go b/compiler/functions.go index 86d3916bf..592992efc 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -82,7 +82,7 @@ func (fc *funcContext) namedFuncContext(inst typeparams.Instance) *funcContext { // 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.FuncLitInfo(fun) + info := fc.pkgCtx.FuncLitInfo(fun, fc.TypeArgs()) sig := fc.pkgCtx.TypeOf(fun).(*types.Signature) o := types.NewFunc(fun.Pos(), fc.pkgCtx.Pkg, fc.newLitFuncName(), sig) inst := typeparams.Instance{Object: o} diff --git a/compiler/internal/analysis/defer.go b/compiler/internal/analysis/defer.go index 36b19ef2a..65d153d6e 100644 --- a/compiler/internal/analysis/defer.go +++ b/compiler/internal/analysis/defer.go @@ -2,8 +2,10 @@ package analysis import ( "go/ast" + "go/types" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/typesutil" ) // deferStmt represents a defer statement that is blocking or not. @@ -49,8 +51,9 @@ import ( // // [CFG]: https://en.wikipedia.org/wiki/Control-flow_graph type deferStmt struct { - inst *typeparams.Instance - lit *ast.FuncLit + obj types.Object + lit *ast.FuncLit + typeArgs typesutil.TypeList } // newBlockingDefer creates a new defer statement that is blocking. @@ -65,13 +68,13 @@ func newBlockingDefer() *deferStmt { // newInstDefer creates a new defer statement for an instances of a method. // The instance is used to look up the blocking information later. func newInstDefer(inst typeparams.Instance) *deferStmt { - return &deferStmt{inst: &inst} + return &deferStmt{obj: inst.Object, typeArgs: inst.TArgs} } // newLitDefer creates a new defer statement for a function literal. // The literal is used to look up the blocking information later. -func newLitDefer(lit *ast.FuncLit) *deferStmt { - return &deferStmt{lit: lit} +func newLitDefer(lit *ast.FuncLit, typeArgs typesutil.TypeList) *deferStmt { + return &deferStmt{lit: lit, typeArgs: typeArgs} } // IsBlocking determines if the defer statement is blocking or not. @@ -79,11 +82,11 @@ func (d *deferStmt) IsBlocking(info *Info) bool { // If the instance or the literal is set then we can look up the blocking, // otherwise assume blocking because otherwise the defer wouldn't // have been recorded. - if d.inst != nil { - return info.FuncInfo(*d.inst).IsBlocking() + if d.obj != nil { + return info.IsBlocking(typeparams.Instance{Object: d.obj, TArgs: d.typeArgs}) } if d.lit != nil { - return info.FuncLitInfo(d.lit).IsBlocking() + return info.FuncLitInfo(d.lit, d.typeArgs).IsBlocking() } return true } diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go index 42918258e..803952b24 100644 --- a/compiler/internal/analysis/info.go +++ b/compiler/internal/analysis/info.go @@ -55,22 +55,24 @@ type Info struct { instanceSets *typeparams.PackageInstanceSets HasPointer map[*types.Var]bool funcInstInfos *typeparams.InstanceMap[*FuncInfo] - funcLitInfos map[*ast.FuncLit]*FuncInfo + funcLitInfos map[*ast.FuncLit][]*FuncInfo InitFuncInfo *FuncInfo // Context for package variable initialization. isImportedBlocking func(typeparams.Instance) bool // For functions from other packages. allInfos []*FuncInfo } -func (info *Info) newFuncInfo(n ast.Node, inst *typeparams.Instance) *FuncInfo { +func (info *Info) newFuncInfo(n ast.Node, obj types.Object, typeArgs typesutil.TypeList, resolver *typeparams.Resolver) *FuncInfo { funcInfo := &FuncInfo{ pkgInfo: info, Flattened: make(map[ast.Node]bool), Blocking: make(map[ast.Node]bool), GotoLabel: make(map[*types.Label]bool), loopReturnIndex: -1, - localInstCallees: new(typeparams.InstanceMap[[]astPath]), + instCallees: new(typeparams.InstanceMap[[]astPath]), literalFuncCallees: make(map[*ast.FuncLit]astPath), + typeArgs: typeArgs, + resolver: resolver, } // Register the function in the appropriate map. @@ -86,13 +88,14 @@ func (info *Info) newFuncInfo(n ast.Node, inst *typeparams.Instance) *FuncInfo { funcInfo.Blocking[n] = true } - if inst == nil { - inst = &typeparams.Instance{Object: info.Defs[n.Name]} + if obj == nil { + obj = info.Defs[n.Name] } - info.funcInstInfos.Set(*inst, funcInfo) + inst := typeparams.Instance{Object: obj, TArgs: typeArgs} + info.funcInstInfos.Set(inst, funcInfo) case *ast.FuncLit: - info.funcLitInfos[n] = funcInfo + info.funcLitInfos[n] = append(info.funcLitInfos[n], funcInfo) } // And add it to the list of all functions. @@ -105,28 +108,40 @@ func (info *Info) newFuncInfoInstances(fd *ast.FuncDecl) []*FuncInfo { obj := info.Defs[fd.Name] instances := info.instanceSets.Pkg(info.Pkg).ForObj(obj) if len(instances) == 0 { - // No instances found, this is a non-generic function. - return []*FuncInfo{info.newFuncInfo(fd, nil)} + if typeparams.HasTypeParams(obj.Type()) { + // This is a generic function, but no instances were found, + // this is an unused function, so skip over it. + return []*FuncInfo{} + } + + // No instances found and this is a non-generic function. + return []*FuncInfo{info.newFuncInfo(fd, nil, nil, nil)} } funcInfos := make([]*FuncInfo, 0, len(instances)) for _, inst := range instances { - fi := info.newFuncInfo(fd, &inst) + var resolver *typeparams.Resolver if sig, ok := obj.Type().(*types.Signature); ok { tp := typeparams.ToSlice(typeparams.SignatureTypeParams(sig)) - fi.resolver = typeparams.NewResolver(info.typeCtx, tp, inst.TArgs) + resolver = typeparams.NewResolver(info.typeCtx, tp, inst.TArgs) } + fi := info.newFuncInfo(fd, inst.Object, inst.TArgs, resolver) funcInfos = append(funcInfos, fi) } return funcInfos } // IsBlocking returns true if the function may contain blocking calls or operations. +// If inst is from a different package, this will use the isImportedBlocking +// to lookup the information from the other package. func (info *Info) IsBlocking(inst typeparams.Instance) bool { + if inst.Object.Pkg() != info.Pkg { + return info.isImportedBlocking(inst) + } if funInfo := info.FuncInfo(inst); funInfo != nil { return funInfo.IsBlocking() } - panic(fmt.Errorf(`info did not have function declaration instance for %q`, inst)) + panic(fmt.Errorf(`info did not have function declaration instance for %q`, inst.TypeString())) } // FuncInfo returns information about the given function declaration instance, or nil if not found. @@ -135,8 +150,16 @@ func (info *Info) FuncInfo(inst typeparams.Instance) *FuncInfo { } // FuncLitInfo returns information about the given function literal, or nil if not found. -func (info *Info) FuncLitInfo(fun *ast.FuncLit) *FuncInfo { - return info.funcLitInfos[fun] +// The given type arguments are used to identify the correct instance of the +// function literal in the case the literal was defined inside a generic function. +func (info *Info) FuncLitInfo(fun *ast.FuncLit, typeArgs typesutil.TypeList) *FuncInfo { + lits := info.funcLitInfos[fun] + for _, fi := range lits { + if fi.typeArgs.Equal(typeArgs) { + return fi + } + } + return nil } // VarsWithInitializers returns a set of package-level variables that have @@ -160,9 +183,9 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info HasPointer: make(map[*types.Var]bool), isImportedBlocking: isBlocking, funcInstInfos: new(typeparams.InstanceMap[*FuncInfo]), - funcLitInfos: make(map[*ast.FuncLit]*FuncInfo), + funcLitInfos: make(map[*ast.FuncLit][]*FuncInfo), } - info.InitFuncInfo = info.newFuncInfo(nil, nil) + info.InitFuncInfo = info.newFuncInfo(nil, nil, nil, nil) // Traverse the full AST of the package and collect information about existing // functions. @@ -190,19 +213,19 @@ func (info *Info) propagateFunctionBlocking() bool { done := true for _, caller := range info.allInfos { // Check calls to named functions and function-typed variables. - caller.localInstCallees.Iterate(func(callee typeparams.Instance, callSites []astPath) { - if info.FuncInfo(callee).IsBlocking() { + caller.instCallees.Iterate(func(callee typeparams.Instance, callSites []astPath) { + if info.IsBlocking(callee) { for _, callSite := range callSites { caller.markBlocking(callSite) } - caller.localInstCallees.Delete(callee) + caller.instCallees.Delete(callee) done = false } }) // Check direct calls to function literals. for callee, callSite := range caller.literalFuncCallees { - if info.FuncLitInfo(callee).IsBlocking() { + if info.FuncLitInfo(callee, caller.typeArgs).IsBlocking() { caller.markBlocking(callSite) delete(caller.literalFuncCallees, callee) done = false @@ -250,15 +273,18 @@ type FuncInfo struct { // returns defined before any defers in a loop may still be affected by // those defers because of the loop. See comment on [deferStmt]. loopReturnIndex int - // List of other named functions from the current package this function calls. + // List of other named functions in the current package or another package + // that this function calls. // If any of them are blocking, this function will become blocking too. - localInstCallees *typeparams.InstanceMap[[]astPath] + instCallees *typeparams.InstanceMap[[]astPath] // List of function literals directly called from this function (for example: // `func() { /* do stuff */ }()`). This is distinct from function literals // assigned to named variables (for example: `doStuff := func() {}; // doStuff()`), which are handled by localInstCallees. If any of them are // identified as blocking, this function will become blocking too. literalFuncCallees map[*ast.FuncLit]astPath + // typeArgs are the type arguments for the function instance. + typeArgs typesutil.TypeList // resolver is used by this function instance to resolve any type arguments // for internal function calls. // This may be nil if not an instance of a generic function. @@ -276,6 +302,12 @@ func (fi *FuncInfo) IsBlocking() bool { return fi == nil || len(fi.Blocking) != 0 } +// TypeArgs gets the type arguments of this inside of a function instance +// or empty if not in a function instance. +func (fi *FuncInfo) TypeArgs() typesutil.TypeList { + return fi.typeArgs +} + // propagateReturnBlocking updates the blocking on the return statements. // See comment on [deferStmt]. // @@ -341,7 +373,7 @@ func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { return nil case *ast.FuncLit: // Analyze the function literal in its own context. - return fi.pkgInfo.newFuncInfo(n, nil) + return fi.pkgInfo.newFuncInfo(n, nil, fi.typeArgs, fi.resolver) case *ast.BranchStmt: switch n.Tok { case token.GOTO: @@ -502,7 +534,7 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr, deferredCall bool) ast.Visito // Register literal function call site in case it is identified as blocking. fi.literalFuncCallees[f] = fi.visitorStack.copy() if deferredCall { - fi.deferStmts = append(fi.deferStmts, newLitDefer(f)) + fi.deferStmts = append(fi.deferStmts, newLitDefer(f, fi.typeArgs)) } return nil // No need to walk under this CallExpr, we already did it manually. case *ast.IndexExpr: @@ -610,21 +642,11 @@ func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance, deferredCall boo } } - if o.Pkg() != fi.pkgInfo.Pkg { - if fi.pkgInfo.isImportedBlocking(callee) { - fi.markBlocking(fi.visitorStack) - if deferredCall { - fi.deferStmts = append(fi.deferStmts, newBlockingDefer()) - } - } - return - } - // We probably don't know yet whether the callee function is blocking. // Record the calls site for the later stage. - paths := fi.localInstCallees.Get(callee) + paths := fi.instCallees.Get(callee) paths = append(paths, fi.visitorStack.copy()) - fi.localInstCallees.Set(callee, paths) + fi.instCallees.Set(callee, paths) if deferredCall { fi.deferStmts = append(fi.deferStmts, newInstDefer(callee)) } diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go index d4e21b501..00c1d1623 100644 --- a/compiler/internal/analysis/info_test.go +++ b/compiler/internal/analysis/info_test.go @@ -156,10 +156,10 @@ func TestBlocking_GoRoutines_WithFuncLiterals(t *testing.T) { }(<-c) }`) bt.assertNotBlocking(`notBlocking`) - bt.assertBlockingLit(4) + bt.assertBlockingLit(4, ``) bt.assertBlocking(`blocking`) - bt.assertNotBlockingLit(10) + bt.assertNotBlockingLit(10, ``) } func TestBlocking_GoRoutines_WithNamedFuncs(t *testing.T) { @@ -210,13 +210,13 @@ func TestBlocking_Defers_WithoutReturns_WithFuncLiterals(t *testing.T) { }(true) }`) bt.assertBlocking(`blockingBody`) - bt.assertBlockingLit(4) + bt.assertBlockingLit(4, ``) bt.assertBlocking(`blockingArg`) - bt.assertNotBlockingLit(10) + bt.assertNotBlockingLit(10, ``) bt.assertNotBlocking(`notBlocking`) - bt.assertNotBlockingLit(16) + bt.assertNotBlockingLit(16, ``) } func TestBlocking_Defers_WithoutReturns_WithNamedFuncs(t *testing.T) { @@ -278,13 +278,13 @@ func TestBlocking_Defers_WithReturns_WithFuncLiterals(t *testing.T) { return 42 }`) bt.assertBlocking(`blockingBody`) - bt.assertBlockingLit(4) + bt.assertBlockingLit(4, ``) bt.assertBlocking(`blockingArg`) - bt.assertNotBlockingLit(11) + bt.assertNotBlockingLit(11, ``) bt.assertNotBlocking(`notBlocking`) - bt.assertNotBlockingLit(18) + bt.assertNotBlockingLit(18, ``) } func TestBlocking_Defers_WithReturns_WithNamedFuncs(t *testing.T) { @@ -317,15 +317,15 @@ func TestBlocking_Defers_WithReturns_WithNamedFuncs(t *testing.T) { bt.assertNotBlocking(`nonBlockingPrint`) bt.assertBlocking(`blockingBody`) - bt.assertBlockingReturn(13) + bt.assertBlockingReturn(13, ``) bt.assertBlocking(`blockingArg`) // The defer is non-blocking so the return is not blocking // even though the function is blocking. - bt.assertNotBlockingReturn(18) + bt.assertNotBlockingReturn(18, ``) bt.assertNotBlocking(`notBlocking`) - bt.assertNotBlockingReturn(23) + bt.assertNotBlockingReturn(23, ``) } func TestBlocking_Defers_WithMultipleReturns(t *testing.T) { @@ -363,15 +363,15 @@ func TestBlocking_Defers_WithMultipleReturns(t *testing.T) { return true // line 31 }`) bt.assertBlocking(`foo`) - bt.assertNotBlockingLit(4) + bt.assertNotBlockingLit(4, ``) // Early escape from function without blocking defers is not blocking. - bt.assertNotBlockingReturn(11) - bt.assertNotBlockingLit(14) + bt.assertNotBlockingReturn(11, ``) + bt.assertNotBlockingLit(14, ``) // Function has had blocking by this point but no blocking defers yet. - bt.assertNotBlockingReturn(20) - bt.assertBlockingLit(24) + bt.assertNotBlockingReturn(20, ``) + bt.assertBlockingLit(24, ``) // The return is blocking because of a blocking defer. - bt.assertBlockingReturn(28) + bt.assertBlockingReturn(28, ``) // Technically the return on line 31 is not blocking since the defer that // is blocking can only exit through the return on line 28, but it would be // difficult to determine which defers would only affect certain returns @@ -384,7 +384,7 @@ func TestBlocking_Defers_WithMultipleReturns(t *testing.T) { // // For now we simply build up the list of defers as we go making // the return on line 31 also blocking. - bt.assertBlockingReturn(31) + bt.assertBlockingReturn(31, ``) } func TestBlocking_Defers_WithReturnsAndDefaultBlocking(t *testing.T) { @@ -453,12 +453,12 @@ func TestBlocking_Defers_WithReturnsAndDefaultBlocking(t *testing.T) { bt.assertBlocking(`deferMappedFuncCall`) // All of these returns are blocking because they have blocking defers. - bt.assertBlockingReturn(17) - bt.assertBlockingReturn(22) - bt.assertBlockingReturn(28) - bt.assertBlockingReturn(34) - bt.assertBlockingReturn(40) - bt.assertBlockingReturn(49) + bt.assertBlockingReturn(17, ``) + bt.assertBlockingReturn(22, ``) + bt.assertBlockingReturn(28, ``) + bt.assertBlockingReturn(34, ``) + bt.assertBlockingReturn(40, ``) + bt.assertBlockingReturn(49, ``) } func TestBlocking_Defers_WithReturnsAndDeferBuiltin(t *testing.T) { @@ -477,7 +477,7 @@ func TestBlocking_Defers_WithReturnsAndDeferBuiltin(t *testing.T) { bt.assertFuncInstCount(1) bt.assertNotBlocking(`deferBuiltinCall`) - bt.assertNotBlockingReturn(10) + bt.assertNotBlockingReturn(10, ``) } func TestBlocking_Defers_WithReturnsInLoops(t *testing.T) { @@ -575,14 +575,14 @@ func TestBlocking_Defers_WithReturnsInLoops(t *testing.T) { // When the following 2 returns are defined there are no defers, however, // because of the loop, the blocking defers defined after the return will // block the returns. - bt.assertBlockingReturn(12) - bt.assertBlockingReturn(22) - bt.assertBlockingReturn(31) - bt.assertBlockingReturn(44) - bt.assertBlockingReturn(52) - bt.assertBlockingReturn(66) - bt.assertBlockingReturn(73) - bt.assertBlockingReturn(77) + bt.assertBlockingReturn(12, ``) + bt.assertBlockingReturn(22, ``) + bt.assertBlockingReturn(31, ``) + bt.assertBlockingReturn(44, ``) + bt.assertBlockingReturn(52, ``) + bt.assertBlockingReturn(66, ``) + bt.assertBlockingReturn(73, ``) + bt.assertBlockingReturn(77, ``) } func TestBlocking_Defers_WithReturnsInLoopsInLoops(t *testing.T) { @@ -652,19 +652,19 @@ func TestBlocking_Defers_WithReturnsInLoopsInLoops(t *testing.T) { bt.assertFuncInstCount(4) bt.assertBlocking(`blocking`) bt.assertBlocking(`forLoopTheLoop`) - bt.assertNotBlockingReturn(9) - bt.assertBlockingReturn(13) - bt.assertBlockingReturn(17) - bt.assertBlockingReturn(21) - bt.assertBlockingReturn(25) - bt.assertBlockingReturn(28) + bt.assertNotBlockingReturn(9, ``) + bt.assertBlockingReturn(13, ``) + bt.assertBlockingReturn(17, ``) + bt.assertBlockingReturn(21, ``) + bt.assertBlockingReturn(25, ``) + bt.assertBlockingReturn(28, ``) bt.assertBlocking(`rangeLoopTheLoop`) - bt.assertBlockingReturn(36) - bt.assertBlockingReturn(41) + bt.assertBlockingReturn(36, ``) + bt.assertBlockingReturn(41, ``) bt.assertBlocking(`noopThenLoop`) - bt.assertNotBlockingReturn(48) - bt.assertBlockingReturn(54) - bt.assertBlockingReturn(58) + bt.assertNotBlockingReturn(48, ``) + bt.assertBlockingReturn(54, ``) + bt.assertBlockingReturn(58, ``) } func TestBlocking_Returns_WithoutDefers(t *testing.T) { @@ -693,19 +693,67 @@ func TestBlocking_Returns_WithoutDefers(t *testing.T) { return true // line 22 }`) bt.assertBlocking(`blocking`) - bt.assertBlockingReturn(4) + bt.assertBlockingReturn(4, ``) bt.assertBlocking(`blockingBeforeReturn`) - bt.assertNotBlockingReturn(9) + bt.assertNotBlockingReturn(9, ``) bt.assertBlocking(`indirectlyBlocking`) - bt.assertBlockingReturn(13) + bt.assertBlockingReturn(13, ``) bt.assertBlocking(`indirectlyBlockingBeforeReturn`) - bt.assertNotBlockingReturn(18) + bt.assertNotBlockingReturn(18, ``) bt.assertNotBlocking(`notBlocking`) - bt.assertNotBlockingReturn(22) + bt.assertNotBlockingReturn(22, ``) +} + +func TestBlocking_Defers_WithReturnsInInstances(t *testing.T) { + // This is an example of a deferred function literal inside of + // an instance of a generic function affecting the return + // differently based on the type arguments of the instance. + bt := newBlockingTest(t, + `package test + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + type Foo interface { Baz() } + func FooBaz[T Foo]() bool { + defer func() { // line 17 + var foo T + foo.Baz() + }() + return true // line 21 + } + + func main() { + FooBaz[BazBlocker]() + FooBaz[BazNotBlocker]() + }`) + + bt.assertFuncInstCount(5) + bt.assertBlocking(`BazBlocker.Baz`) + bt.assertNotBlocking(`BazNotBlocker.Baz`) + bt.assertBlockingInst(`pkg/test.FooBaz`) + bt.assertNotBlockingInst(`pkg/test.FooBaz`) + bt.assertBlocking(`main`) + + bt.assertFuncLitCount(2) + bt.assertBlockingLit(17, `pkg/test.BazBlocker`) + bt.assertNotBlockingLit(17, `pkg/test.BazNotBlocker`) + + bt.assertBlockingReturn(21, `pkg/test.BazBlocker`) + bt.assertNotBlockingReturn(21, `pkg/test.BazNotBlocker`) } func TestBlocking_Defers_WithReturnsAndOtherPackages(t *testing.T) { @@ -737,10 +785,10 @@ func TestBlocking_Defers_WithReturnsAndOtherPackages(t *testing.T) { bt := newBlockingTestWithOtherPackage(t, testSrc, otherSrc) bt.assertBlocking(`deferOtherBlocking`) - bt.assertBlockingReturn(7) + bt.assertBlockingReturn(7, ``) bt.assertNotBlocking(`deferOtherNotBlocking`) - bt.assertNotBlockingReturn(12) + bt.assertNotBlockingReturn(12, ``) } func TestBlocking_FunctionLiteral(t *testing.T) { @@ -770,13 +818,13 @@ func TestBlocking_FunctionLiteral(t *testing.T) { bt.assertBlocking(`blocking`) bt.assertBlocking(`indirectlyBlocking`) - bt.assertBlockingLit(9) + bt.assertBlockingLit(9, ``) bt.assertBlocking(`directlyBlocking`) - bt.assertBlockingLit(13) + bt.assertBlockingLit(13, ``) bt.assertNotBlocking(`notBlocking`) - bt.assertNotBlockingLit(20) + bt.assertNotBlockingLit(20, ``) } func TestBlocking_LinkedFunction(t *testing.T) { @@ -818,9 +866,9 @@ func TestBlocking_Instances_WithSingleTypeArg(t *testing.T) { // blocking and notBlocking as generics do not have FuncInfo, // only non-generic and instances have FuncInfo. - bt.assertBlockingInst(`test.blocking[int]`) + bt.assertBlockingInst(`pkg/test.blocking`) bt.assertBlocking(`bInt`) - bt.assertNotBlockingInst(`test.notBlocking[uint]`) + bt.assertNotBlockingInst(`pkg/test.notBlocking`) bt.assertNotBlocking(`nbUint`) } @@ -849,9 +897,9 @@ func TestBlocking_Instances_WithMultipleTypeArgs(t *testing.T) { // blocking and notBlocking as generics do not have FuncInfo, // only non-generic and instances have FuncInfo. - bt.assertBlockingInst(`test.blocking[string, int, map[string]int]`) + bt.assertBlockingInst(`pkg/test.blocking`) bt.assertBlocking(`bInt`) - bt.assertNotBlockingInst(`test.notBlocking[string, uint, map[string]uint]`) + bt.assertNotBlockingInst(`pkg/test.notBlocking`) bt.assertNotBlocking(`nbUint`) } @@ -1075,7 +1123,7 @@ func TestBlocking_VarFunctionCall(t *testing.T) { func bar() { foo() }`) - bt.assertNotBlockingLit(3) + bt.assertNotBlockingLit(3, ``) bt.assertBlocking(`bar`) } @@ -1233,12 +1281,12 @@ func TestBlocking_InstantiationBlocking(t *testing.T) { bt.assertBlocking(`BazBlocker.Baz`) bt.assertBlocking(`blockingViaExplicit`) bt.assertBlocking(`blockingViaImplicit`) - bt.assertBlockingInst(`test.FooBaz[pkg/test.BazBlocker]`) + bt.assertBlockingInst(`pkg/test.FooBaz`) bt.assertNotBlocking(`BazNotBlocker.Baz`) bt.assertNotBlocking(`notBlockingViaExplicit`) bt.assertNotBlocking(`notBlockingViaImplicit`) - bt.assertNotBlockingInst(`test.FooBaz[pkg/test.BazNotBlocker]`) + bt.assertNotBlockingInst(`pkg/test.FooBaz`) } func TestBlocking_NestedInstantiations(t *testing.T) { @@ -1272,12 +1320,129 @@ func TestBlocking_NestedInstantiations(t *testing.T) { bt.assertFuncInstCount(8) bt.assertNotBlocking(`bazInt`) bt.assertNotBlocking(`bazString`) - bt.assertNotBlockingInst(`test.Foo[map[int]int]`) - bt.assertNotBlockingInst(`test.Foo[map[int]string]`) - bt.assertNotBlockingInst(`test.Bar[int, int, map[int]int]`) - bt.assertNotBlockingInst(`test.Bar[int, string, map[int]string]`) - bt.assertNotBlockingInst(`test.Baz[int, []int]`) - bt.assertNotBlockingInst(`test.Baz[string, []string]`) + bt.assertNotBlockingInst(`pkg/test.Foo`) + bt.assertNotBlockingInst(`pkg/test.Foo`) + bt.assertNotBlockingInst(`pkg/test.Bar`) + bt.assertNotBlockingInst(`pkg/test.Bar`) + bt.assertNotBlockingInst(`pkg/test.Baz`) + bt.assertNotBlockingInst(`pkg/test.Baz`) +} + +func TestBlocking_UnusedGenericFunctions(t *testing.T) { + // Checking that the type parameters are being propagated down into callee. + // This is based off of go1.19.13/test/typeparam/orderedmap.go + bt := newBlockingTest(t, + `package test + + type node[K, V any] struct { + key K + val V + left, right *node[K, V] + } + + type Tree[K, V any] struct { + root *node[K, V] + eq func(K, K) bool + } + + func New[K, V any](eq func(K, K) bool) *Tree[K, V] { + return &Tree[K, V]{eq: eq} + } + + func NewStrKey[K ~string, V any]() *Tree[K, V] { // unused + return New[K, V](func(k1, k2 K) bool { + return string(k1) == string(k2) + }) + } + + func NewStrStr[V any]() *Tree[string, V] { // unused + return NewStrKey[string, V]() + } + + func main() { + t := New[int, string](func(k1, k2 int) bool { + return k1 == k2 + }) + println(t) + }`) + bt.assertFuncInstCount(2) + // Notice that `NewStrKey` and `NewStrStr` are not called so doesn't have + // any known instances and therefore they don't have any FuncInfos. + bt.assertNotBlockingInst(`pkg/test.New`) + bt.assertNotBlocking(`main`) +} + +func TestBlocking_LitInstanceCalls(t *testing.T) { + // Literals defined inside a generic function must inherit the + // type arguments (resolver) of the enclosing instance it is defined in + // so that things like calls to other generic functions create the + // call to the correct concrete instance. + bt := newBlockingTest(t, + `package test + + func foo[T any](x T) { + println(x) + } + + func bar[T any](x T) { + f := func(v T) { // line 8 + foo[T](v) + } + f(x) + } + + func main() { + bar[int](42) + bar[float64](3.14) + }`) + bt.assertFuncInstCount(5) + + bt.assertNotBlockingInst(`pkg/test.foo`) + bt.assertNotBlockingInst(`pkg/test.foo`) + bt.assertNotBlockingLit(8, `int`) + bt.assertNotBlockingLit(8, `float64`) + // The following are blocking because the function literal call. + bt.assertBlockingInst(`pkg/test.bar`) + bt.assertBlockingInst(`pkg/test.bar`) +} + +func TestBlocking_BlockingLitInstance(t *testing.T) { + bt := newBlockingTest(t, + `package test + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + type Foo interface { Baz() } + func FooBaz[T Foo](foo T) func() { + return func() { // line 17 + foo.Baz() + } + } + + func main() { + _ = FooBaz(BazBlocker{}) + _ = FooBaz(BazNotBlocker{}) + }`) + bt.assertFuncInstCount(5) + + bt.assertBlocking(`BazBlocker.Baz`) + // THe following is not blocking because the function literal is not called. + bt.assertNotBlockingInst(`pkg/test.FooBaz`) + bt.assertBlockingLit(17, `pkg/test.BazBlocker`) + + bt.assertNotBlocking(`BazNotBlocker.Baz`) + bt.assertNotBlockingInst(`pkg/test.FooBaz`) + bt.assertNotBlockingLit(17, `pkg/test.BazNotBlocker`) } func TestBlocking_MethodSelection(t *testing.T) { @@ -1333,13 +1498,13 @@ func TestBlocking_MethodSelection(t *testing.T) { bt.assertFuncInstCount(8) bt.assertBlocking(`BazBlocker.Baz`) - bt.assertBlockingInst(`test.ByMethodExpression[pkg/test.BazBlocker]`) - bt.assertBlockingInst(`test.ByInstance[pkg/test.BazBlocker]`) + bt.assertBlockingInst(`pkg/test.FooBaz.ByMethodExpression`) + bt.assertBlockingInst(`pkg/test.FooBaz.ByInstance`) bt.assertBlocking(`blocking`) bt.assertNotBlocking(`BazNotBlocker.Baz`) - bt.assertNotBlockingInst(`test.ByMethodExpression[pkg/test.BazNotBlocker]`) - bt.assertNotBlockingInst(`test.ByInstance[pkg/test.BazNotBlocker]`) + bt.assertNotBlockingInst(`pkg/test.FooBaz.ByMethodExpression`) + bt.assertNotBlockingInst(`pkg/test.FooBaz.ByInstance`) bt.assertNotBlocking(`notBlocking`) } @@ -1529,21 +1694,29 @@ func (bt *blockingTest) assertFuncInstCount(expCount int) { if got := bt.pkgInfo.funcInstInfos.Len(); got != expCount { bt.f.T.Errorf(`Got %d function instance infos but expected %d.`, got, expCount) for i, inst := range bt.pkgInfo.funcInstInfos.Keys() { - bt.f.T.Logf(` %d. %q`, i+1, inst.TypeString()) + bt.f.T.Logf(` %d. %q`, i+1, inst.String()) } } } func (bt *blockingTest) assertFuncLitCount(expCount int) { - if got := len(bt.pkgInfo.funcLitInfos); got != expCount { + got := 0 + for _, fis := range bt.pkgInfo.funcLitInfos { + got += len(fis) + } + if got != expCount { bt.f.T.Errorf(`Got %d function literal infos but expected %d.`, got, expCount) - pos := make([]string, 0, len(bt.pkgInfo.funcLitInfos)) - for fl := range bt.pkgInfo.funcLitInfos { - pos = append(pos, bt.f.FileSet.Position(fl.Pos()).String()) + + lits := make([]string, 0, len(bt.pkgInfo.funcLitInfos)) + for fl, fis := range bt.pkgInfo.funcLitInfos { + pos := bt.f.FileSet.Position(fl.Pos()).String() + for _, fi := range fis { + lits = append(lits, pos+`<`+fi.typeArgs.String()+`>`) + } } - sort.Strings(pos) - for i := range pos { - bt.f.T.Logf(` %d. %q`, i+1, pos) + sort.Strings(lits) + for i := range lits { + bt.f.T.Logf(` %d. %q`, i+1, lits[i]) } } } @@ -1597,24 +1770,41 @@ func (bt *blockingTest) isTypesFuncBlocking(funcName string) bool { return bt.pkgInfo.IsBlocking(inst) } -func (bt *blockingTest) assertBlockingLit(lineNo int) { - if !bt.isFuncLitBlocking(lineNo) { - bt.f.T.Errorf(`Got FuncLit at line %d as not blocking but expected it to be blocking.`, lineNo) +func (bt *blockingTest) assertBlockingLit(lineNo int, typeArgsStr string) { + if !bt.isFuncLitBlocking(lineNo, typeArgsStr) { + bt.f.T.Errorf(`Got FuncLit at line %d with type args %q as not blocking but expected it to be blocking.`, lineNo, typeArgsStr) } } -func (bt *blockingTest) assertNotBlockingLit(lineNo int) { - if bt.isFuncLitBlocking(lineNo) { - bt.f.T.Errorf(`Got FuncLit at line %d as blocking but expected it to be not blocking.`, lineNo) +func (bt *blockingTest) assertNotBlockingLit(lineNo int, typeArgsStr string) { + if bt.isFuncLitBlocking(lineNo, typeArgsStr) { + bt.f.T.Errorf(`Got FuncLit at line %d with type args %q as blocking but expected it to be not blocking.`, lineNo, typeArgsStr) } } -func (bt *blockingTest) isFuncLitBlocking(lineNo int) bool { +func (bt *blockingTest) isFuncLitBlocking(lineNo int, typeArgsStr string) bool { fnLit := srctesting.GetNodeAtLineNo[*ast.FuncLit](bt.file, bt.f.FileSet, lineNo) if fnLit == nil { bt.f.T.Fatalf(`FuncLit on line %d not found in the AST.`, lineNo) } - return bt.pkgInfo.FuncLitInfo(fnLit).IsBlocking() + + fis, ok := bt.pkgInfo.funcLitInfos[fnLit] + if !ok { + bt.f.T.Fatalf(`No FuncInfo found for FuncLit at line %d.`, lineNo) + } + + for _, fi := range fis { + if fi.typeArgs.String() == typeArgsStr { + return fi.IsBlocking() + } + } + + bt.f.T.Logf("FuncList instances:") + for i, fi := range fis { + bt.f.T.Logf("\t%d. %q\n", i+1, fi.typeArgs.String()) + } + bt.f.T.Fatalf(`No FuncInfo found for FuncLit at line %d with type args %q.`, lineNo, typeArgsStr) + return false } func (bt *blockingTest) assertBlockingInst(instanceStr string) { @@ -1632,40 +1822,58 @@ func (bt *blockingTest) assertNotBlockingInst(instanceStr string) { func (bt *blockingTest) isFuncInstBlocking(instanceStr string) bool { instances := bt.pkgInfo.funcInstInfos.Keys() for _, inst := range instances { - if inst.TypeString() == instanceStr { + if inst.String() == instanceStr { return bt.pkgInfo.FuncInfo(inst).IsBlocking() } } bt.f.T.Logf(`Function instances found in package info:`) for i, inst := range instances { - bt.f.T.Logf(` %d. %s`, i+1, inst.TypeString()) + bt.f.T.Logf(` %d. %s`, i+1, inst.String()) } bt.f.T.Fatalf(`No function instance found for %q in package info.`, instanceStr) return false } -func (bt *blockingTest) assertBlockingReturn(lineNo int) { - if !bt.isReturnBlocking(lineNo) { - bt.f.T.Errorf(`Got return at line %d as not blocking but expected it to be blocking.`, lineNo) +func (bt *blockingTest) assertBlockingReturn(lineNo int, typeArgsStr string) { + if !bt.isReturnBlocking(lineNo, typeArgsStr) { + bt.f.T.Errorf(`Got return at line %d (%q) as not blocking but expected it to be blocking.`, lineNo, typeArgsStr) } } -func (bt *blockingTest) assertNotBlockingReturn(lineNo int) { - if bt.isReturnBlocking(lineNo) { - bt.f.T.Errorf(`Got return at line %d as blocking but expected it to be not blocking.`, lineNo) +func (bt *blockingTest) assertNotBlockingReturn(lineNo int, typeArgsStr string) { + if bt.isReturnBlocking(lineNo, typeArgsStr) { + bt.f.T.Errorf(`Got return at line %d (%q) as blocking but expected it to be not blocking.`, lineNo, typeArgsStr) } } -func (bt *blockingTest) isReturnBlocking(lineNo int) bool { +func (bt *blockingTest) isReturnBlocking(lineNo int, typeArgsStr string) bool { ret := srctesting.GetNodeAtLineNo[*ast.ReturnStmt](bt.file, bt.f.FileSet, lineNo) if ret == nil { bt.f.T.Fatalf(`ReturnStmt on line %d not found in the AST.`, lineNo) } + + foundInfo := []*FuncInfo{} for _, info := range bt.pkgInfo.allInfos { - if blocking, found := info.Blocking[ret]; found { - return blocking + for _, rs := range info.returnStmts { + if rs.analyzeStack[len(rs.analyzeStack)-1] == ret { + if info.typeArgs.String() == typeArgsStr { + // Found info that matches the type args and + // has the return statement so return the blocking value. + return info.Blocking[ret] + } + + // Wrong instance, record for error message in the case + // that the correct one instance is not found. + foundInfo = append(foundInfo, info) + break + } } } - // If not found in any info.Blocking, then it is not blocking. + + bt.f.T.Logf("FuncInfo instances with ReturnStmt at line %d:", lineNo) + for i, info := range foundInfo { + bt.f.T.Logf("\t%d. %q\n", i+1, info.typeArgs.String()) + } + bt.f.T.Fatalf(`No FuncInfo found for ReturnStmt at line %d with type args %q.`, lineNo, typeArgsStr) return false } diff --git a/compiler/internal/typeparams/map.go b/compiler/internal/typeparams/map.go index 4f6645421..dbe07a54e 100644 --- a/compiler/internal/typeparams/map.go +++ b/compiler/internal/typeparams/map.go @@ -40,7 +40,7 @@ func (im *InstanceMap[V]) findIndex(key Instance) (mapBucket[V], int) { if im != nil && im.data != nil { bucket := im.data[key.Object][typeHash(im.hasher, key.TArgs...)] for i, candidate := range bucket { - if candidate != nil && typeArgsEq(candidate.key.TArgs, key.TArgs) { + if candidate != nil && candidate.key.TArgs.Equal(key.TArgs) { return bucket, i } } @@ -90,7 +90,7 @@ func (im *InstanceMap[V]) Set(key Instance, value V) V { for i, candidate := range bucket { if candidate == nil { hole = i - } else if typeArgsEq(candidate.key.TArgs, key.TArgs) { + } else if candidate.key.TArgs.Equal(key.TArgs) { old := candidate.value candidate.value = value return old @@ -192,17 +192,3 @@ func typeHash(hasher typeutil.Hasher, types ...types.Type) uint32 { } return hash } - -// typeArgsEq returns if both lists of type arguments are identical. -func typeArgsEq(a, b []types.Type) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if !types.Identical(a[i], b[i]) { - return false - } - } - - return true -} diff --git a/compiler/internal/typeparams/map_test.go b/compiler/internal/typeparams/map_test.go index ed65587c7..baa31f64a 100644 --- a/compiler/internal/typeparams/map_test.go +++ b/compiler/internal/typeparams/map_test.go @@ -317,7 +317,7 @@ func keysMatch(a, b []Instance) bool { func keyAt(keys []Instance, target Instance) int { for i, v := range keys { - if v.Object == target.Object && typeArgsEq(v.TArgs, target.TArgs) { + if v.Object == target.Object && v.TArgs.Equal(target.TArgs) { return i } } diff --git a/compiler/typesutil/typelist.go b/compiler/typesutil/typelist.go index 04d0d6869..768677365 100644 --- a/compiler/typesutil/typelist.go +++ b/compiler/typesutil/typelist.go @@ -18,3 +18,16 @@ func (tl TypeList) String() string { } return buf.String() } + +// Equal returns true if both lists of type arguments are identical. +func (tl TypeList) Equal(other TypeList) bool { + if len(tl) != len(other) { + return false + } + for i := range tl { + if !types.Identical(tl[i], other[i]) { + return false + } + } + return true +} From 65c5c0bc4162abd81990cc317d570eec75bd274b Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Tue, 19 Nov 2024 12:14:44 -0700 Subject: [PATCH 27/58] updating decl naming --- compiler/compiler_test.go | 426 ++++++++++++++++------- compiler/declNames.go | 70 ++++ compiler/decls.go | 40 ++- compiler/functions.go | 2 +- compiler/internal/analysis/defer.go | 21 +- compiler/internal/analysis/info.go | 94 +++-- compiler/internal/analysis/info_test.go | 218 +++++++++--- compiler/internal/typeparams/map.go | 18 +- compiler/internal/typeparams/map_test.go | 2 +- compiler/package.go | 7 +- compiler/typesutil/typelist.go | 12 + 11 files changed, 670 insertions(+), 240 deletions(-) create mode 100644 compiler/declNames.go diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 65178e986..a79e7d4c0 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -67,9 +67,9 @@ func TestDeclSelection_KeepUnusedExportedMethods(t *testing.T) { srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) - sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) - sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) - sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Baz`) + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) + sel.IsAlive(`func:command-line-arguments.Foo.Baz`) } func TestDeclSelection_RemoveUnusedUnexportedMethods(t *testing.T) { @@ -89,10 +89,10 @@ func TestDeclSelection_RemoveUnusedUnexportedMethods(t *testing.T) { srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) - sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) - sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) - sel.DeclCode.IsDead(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) + sel.IsDead(`func:command-line-arguments.Foo.baz`) } func TestDeclSelection_KeepUnusedUnexportedMethodForInterface(t *testing.T) { @@ -109,7 +109,7 @@ func TestDeclSelection_KeepUnusedUnexportedMethodForInterface(t *testing.T) { println("foo2") } - type IFoo interface { + type IFoo interface { Bar() baz() } @@ -125,13 +125,13 @@ func TestDeclSelection_KeepUnusedUnexportedMethodForInterface(t *testing.T) { srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) - sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) - sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) // `baz` signature metadata is used to check a type assertion against IFoo, // but the method itself is never called, so it can be removed. - sel.DeclCode.IsDead(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) - sel.MethodListCode.IsAlive(`^\s*Foo.methods = .* \{prop: "baz", name: "baz"`) + // The method is kept in Foo's MethodList for type checking. + sel.IsDead(`func:command-line-arguments.Foo.baz`) } func TestDeclSelection_KeepUnexportedMethodUsedViaInterfaceLit(t *testing.T) { @@ -155,9 +155,9 @@ func TestDeclSelection_KeepUnexportedMethodUsedViaInterfaceLit(t *testing.T) { srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) - sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) - sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) - sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) + sel.IsAlive(`func:command-line-arguments.Foo.baz`) } func TestDeclSelection_KeepAliveUnexportedMethodsUsedInMethodExpressions(t *testing.T) { @@ -175,8 +175,8 @@ func TestDeclSelection_KeepAliveUnexportedMethodsUsedInMethodExpressions(t *test srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) - sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) - sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.baz`) } func TestDeclSelection_RemoveUnusedFuncInstance(t *testing.T) { @@ -199,12 +199,12 @@ func TestDeclSelection_RemoveUnusedFuncInstance(t *testing.T) { srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) - sel.DeclCode.IsAlive(`^\s*Sum\[\d+ /\* float64 \*/\]`) - sel.DeclCode.IsAlive(`^\s*sliceType(\$\d+)? = \$sliceType\(\$Float64\)`) + sel.IsAlive(`func:command-line-arguments.Sum`) + sel.IsAlive(`anonType:command-line-arguments.sliceType$1`) // []float64 - sel.DeclCode.IsDead(`^\s*Foo = function`) - sel.DeclCode.IsDead(`^\s*sliceType(\$\d+)? = \$sliceType\(\$Int\)`) - sel.DeclCode.IsDead(`^\s*Sum\[\d+ /\* int \*/\]`) + sel.IsDead(`func:command-line-arguments.Foo`) + sel.IsDead(`anonType:command-line-arguments.sliceType`) // []int + sel.IsDead(`func:command-line-arguments.Sum`) } func TestDeclSelection_RemoveUnusedStructTypeInstances(t *testing.T) { @@ -224,11 +224,11 @@ func TestDeclSelection_RemoveUnusedStructTypeInstances(t *testing.T) { srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) - sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) - sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\[\d+ /\* int \*/\]\)\.prototype\.Bar`) + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) - sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* float64 \*/\] = \$newType`) - sel.DeclCode.IsDead(`^\s*\$ptrType\(Foo\[\d+ /\* float64 \*/\]\)\.prototype\.Bar`) + sel.IsDead(`type:command-line-arguments.Foo`) + sel.IsDead(`func:command-line-arguments.Foo.Bar`) } func TestDeclSelection_RemoveUnusedInterfaceTypeInstances(t *testing.T) { @@ -254,18 +254,18 @@ func TestDeclSelection_RemoveUnusedInterfaceTypeInstances(t *testing.T) { srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) - sel.DeclCode.IsAlive(`^\s*Baz = \$newType`) - sel.DeclCode.IsAlive(`^\s*Baz\.prototype\.Bar`) - sel.InitCode.IsDead(`\$pkg\.F64 = FooBar\[\d+ /\* float64 \*/\]`) + sel.IsAlive(`type:command-line-arguments.Baz`) + sel.IsAlive(`func:command-line-arguments.Baz.Bar`) + sel.IsDead(`var:command-line-arguments.F64`) - sel.DeclCode.IsAlive(`^\s*FooBar\[\d+ /\* int \*/\]`) + sel.IsAlive(`func:command-line-arguments.FooBar`) // The Foo[int] instance is defined as a parameter in FooBar[int] that is alive. // However, Foo[int] isn't used directly in the code so it can be removed. // JS will simply duck-type the Baz object to Foo[int] without Foo[int] specifically defined. - sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) + sel.IsDead(`type:command-line-arguments.Foo`) - sel.DeclCode.IsDead(`^\s*FooBar\[\d+ /\* float64 \*/\]`) - sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* float64 \*/\] = \$newType`) + sel.IsDead(`func:command-line-arguments.FooBar`) + sel.IsDead(`type:command-line-arguments.Foo`) } func TestDeclSelection_RemoveUnusedMethodWithDifferentSignature(t *testing.T) { @@ -291,13 +291,13 @@ func TestDeclSelection_RemoveUnusedMethodWithDifferentSignature(t *testing.T) { srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) - sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) - sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\)\.prototype\.Bar`) - sel.DeclCode.IsDead(`\s*\$ptrType\(Foo\)\.prototype\.baz`) + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) + sel.IsDead(`func:command-line-arguments.Foo.baz`) - sel.DeclCode.IsAlive(`^\s*Foo2 = \$newType`) - sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo2\)\.prototype\.Bar`) - sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo2\)\.prototype\.baz`) + sel.IsAlive(`type:command-line-arguments.Foo2`) + sel.IsAlive(`func:command-line-arguments.Foo2.Bar`) + sel.IsAlive(`func:command-line-arguments.Foo2.baz`) } func TestDeclSelection_RemoveUnusedUnexportedMethodInstance(t *testing.T) { @@ -322,19 +322,19 @@ func TestDeclSelection_RemoveUnusedUnexportedMethodInstance(t *testing.T) { srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) - sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) - sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* int \*/\]\)\.prototype\.Bar`) - sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* int \*/\]\)\.prototype\.baz`) - sel.DeclCode.IsAlive(`^\s*Baz\[\d+ /\* int \*/\] = \$newType`) - sel.DeclCode.IsAlive(`\s*\$ptrType\(Baz\[\d+ /\* int \*/\]\)\.prototype\.Bar`) + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) + sel.IsAlive(`func:command-line-arguments.Foo.baz`) + sel.IsAlive(`type:command-line-arguments.Baz`) + sel.IsAlive(`func:command-line-arguments.Baz.Bar`) - sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* uint \*/\] = \$newType`) - sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* uint \*/\]\)\.prototype\.Bar`) + sel.IsAlive(`type:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo.Bar`) // All three below are dead because Foo[uint].baz is unused. - sel.DeclCode.IsDead(`\s*\$ptrType\(Foo\[\d+ /\* uint \*/\]\)\.prototype\.baz`) - sel.DeclCode.IsDead(`^\s*Baz\[\d+ /\* uint \*/\] = \$newType`) - sel.DeclCode.IsDead(`\s*\$ptrType\(Baz\[\d+ /\* uint \*/\]\)\.prototype\.Bar`) + sel.IsDead(`func:command-line-arguments.Foo.baz`) + sel.IsDead(`type:command-line-arguments.Baz`) + sel.IsDead(`func:command-line-arguments.Baz.Bar`) } func TestDeclSelection_RemoveUnusedTypeConstraint(t *testing.T) { @@ -354,10 +354,10 @@ func TestDeclSelection_RemoveUnusedTypeConstraint(t *testing.T) { srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) - sel.DeclCode.IsDead(`^\s*Foo = \$newType`) - sel.DeclCode.IsDead(`^\s*Bar\[\d+ /\* int \*/\] = \$newType`) - sel.DeclCode.IsDead(`^\s*\$ptrType\(Bar\[\d+ /\* int \*/\]\)\.prototype\.Baz`) - sel.InitCode.IsDead(`ghost = new Bar\[\d+ /\* int \*/\]\.ptr\(7\)`) + sel.IsDead(`type:command-line-arguments.Foo`) + sel.IsDead(`type:command-line-arguments.Bar`) + sel.IsDead(`func:command-line-arguments.Bar.Baz`) + sel.IsDead(`var:command-line-arguments.ghost`) } func TestLengthParenthesizingIssue841(t *testing.T) { @@ -405,6 +405,211 @@ func TestLengthParenthesizingIssue841(t *testing.T) { } } +func TestDeclNaming_Import(t *testing.T) { + src1 := ` + package main + + import ( + newt "github.com/gopherjs/gopherjs/compiler/jorden" + "github.com/gopherjs/gopherjs/compiler/burke" + "github.com/gopherjs/gopherjs/compiler/hudson" + ) + + func main() { + newt.Quote() + burke.Quote() + hudson.Quote() + }` + src2 := `package jorden + func Quote() { println("They mostly come at night... mostly") }` + src3 := `package burke + func Quote() { println("Busy little creatures, huh?") }` + src4 := `package hudson + func Quote() { println("Game over, man! Game over!") }` + + root := srctesting.ParseSources(t, + []srctesting.Source{ + {Name: `main.go`, Contents: []byte(src1)}, + }, + []srctesting.Source{ + {Name: `jorden/rebecca.go`, Contents: []byte(src2)}, + {Name: `burke/carter.go`, Contents: []byte(src3)}, + {Name: `hudson/william.go`, Contents: []byte(src4)}, + }) + + archives := compileProject(t, root, false) + checkForDeclFullNames(t, archives, + `import:github.com/gopherjs/gopherjs/compiler/burke`, + `import:github.com/gopherjs/gopherjs/compiler/hudson`, + `import:github.com/gopherjs/gopherjs/compiler/jorden`, + ) +} + +func TestDeclNaming_FuncAndFuncVar(t *testing.T) { + src := ` + package main + + func Avasarala(value int) { println("Chrisjen", value) } + + func Draper[T any](value T) { println("Bobbie", value) } + + type Nagata struct{ value int } + func (n Nagata) Print() { println("Naomi", n.value) } + + type Burton[T any] struct{ value T } + func (b Burton[T]) Print() { println("Amos", b.value) } + + func main() { + Avasarala(10) + Draper(11) + Draper("Babs") + Nagata{value: 12}.Print() + Burton[int]{value: 13}.Print() + Burton[string]{value: "Timothy"}.Print() + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + archives := compileProject(t, root, false) + checkForDeclFullNames(t, archives, + `funcVar:command-line-arguments.Avasarala`, + `func:command-line-arguments.Avasarala`, + + `funcVar:command-line-arguments.Draper`, + `func:command-line-arguments.Draper`, + `func:command-line-arguments.Draper`, + + `func:command-line-arguments.Nagata.Print`, + + `typeVar:command-line-arguments.Burton`, + `type:command-line-arguments.Burton`, + `type:command-line-arguments.Burton`, + `func:command-line-arguments.Burton.Print`, + `func:command-line-arguments.Burton.Print`, + + `funcVar:command-line-arguments.main`, + `func:command-line-arguments.main`, + `init:main`, + ) +} + +func TestDeclNaming_InitsAndVars(t *testing.T) { + src1 := ` + package main + + import ( + _ "github.com/gopherjs/gopherjs/compiler/spengler" + _ "github.com/gopherjs/gopherjs/compiler/barrett" + _ "github.com/gopherjs/gopherjs/compiler/tully" + ) + + var peck = "Walter" + func init() { println(peck) } + + func main() { + println("Janosz Poha") + }` + src2 := `package spengler + func init() { println("Egon") } + var egie = func() { println("Dirt Farmer") } + func init() { egie() }` + src3 := `package barrett + func init() { println("Dana") }` + src4 := `package barrett + func init() { println("Zuul") }` + src5 := `package barrett + func init() { println("Gatekeeper") }` + src6 := `package tully + func init() { println("Louis") }` + src7 := `package tully + var keymaster = "Vinz Clortho" + func init() { println(keymaster) }` + + root := srctesting.ParseSources(t, + []srctesting.Source{ + {Name: `main.go`, Contents: []byte(src1)}, + }, + []srctesting.Source{ + {Name: `spengler/a.go`, Contents: []byte(src2)}, + {Name: `barrett/a.go`, Contents: []byte(src3)}, + {Name: `barrett/b.go`, Contents: []byte(src4)}, + {Name: `barrett/c.go`, Contents: []byte(src5)}, + {Name: `tully/a.go`, Contents: []byte(src6)}, + {Name: `tully/b.go`, Contents: []byte(src7)}, + }) + + archives := compileProject(t, root, false) + checkForDeclFullNames(t, archives, + // tully + `var:github.com/gopherjs/gopherjs/compiler/tully.keymaster`, + `funcVar:github.com/gopherjs/gopherjs/compiler/tully.init`, + `funcVar:github.com/gopherjs/gopherjs/compiler/tully.init`, + `func:github.com/gopherjs/gopherjs/compiler/tully.init`, + `func:github.com/gopherjs/gopherjs/compiler/tully.init`, + + // spangler + `var:github.com/gopherjs/gopherjs/compiler/spengler.egie`, + `funcVar:github.com/gopherjs/gopherjs/compiler/spengler.init`, + `funcVar:github.com/gopherjs/gopherjs/compiler/spengler.init`, + `func:github.com/gopherjs/gopherjs/compiler/spengler.init`, + `func:github.com/gopherjs/gopherjs/compiler/spengler.init`, + + // barrett + `funcVar:github.com/gopherjs/gopherjs/compiler/barrett.init`, + `funcVar:github.com/gopherjs/gopherjs/compiler/barrett.init`, + `funcVar:github.com/gopherjs/gopherjs/compiler/barrett.init`, + `func:github.com/gopherjs/gopherjs/compiler/barrett.init`, + `func:github.com/gopherjs/gopherjs/compiler/barrett.init`, + `func:github.com/gopherjs/gopherjs/compiler/barrett.init`, + + // main + `var:command-line-arguments.peck`, + `funcVar:command-line-arguments.init`, + `func:command-line-arguments.init`, + `funcVar:command-line-arguments.main`, + `func:command-line-arguments.main`, + `init:main`, + ) +} + +func TestDeclNaming_VarsAndTypes(t *testing.T) { + src := ` + package main + + var _, shawn, _ = func() (int, string, float64) { + return 1, "Vizzini", 3.14 + }() + + var _ = func() string { + return "Inigo Montoya" + }() + + var fezzik = struct{ value int }{value: 7} + var inigo = struct{ value string }{value: "Montoya"} + + type westley struct{ value string } + + func main() {}` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + + archives := compileProject(t, root, false) + checkForDeclFullNames(t, archives, + `var:command-line-arguments.shawn`, + `var:blank`, + + `var:command-line-arguments.fezzik`, + `anonType:command-line-arguments.structType`, + + `var:command-line-arguments.inigo`, + `anonType:command-line-arguments.structType$1`, + + `typeVar:command-line-arguments.westley`, + `type:command-line-arguments.westley`, + ) +} + func compareOrder(t *testing.T, sourceFiles []srctesting.Source, minify bool) { t.Helper() outputNormal := compile(t, sourceFiles, minify) @@ -503,10 +708,6 @@ type selectionTester struct { archives map[string]*Archive packages []*Archive dceSelection map[*Decl]struct{} - - DeclCode *selectionCodeTester - InitCode *selectionCodeTester - MethodListCode *selectionCodeTester } func declSelection(t *testing.T, sourceFiles []srctesting.Source, auxFiles []srctesting.Source) *selectionTester { @@ -539,27 +740,6 @@ func declSelection(t *testing.T, sourceFiles []srctesting.Source, auxFiles []src archives: archives, packages: packages, dceSelection: dceSelection, - DeclCode: &selectionCodeTester{ - t: t, - packages: packages, - dceSelection: dceSelection, - codeName: `DeclCode`, - getCode: func(d *Decl) []byte { return d.DeclCode }, - }, - InitCode: &selectionCodeTester{ - t: t, - packages: packages, - dceSelection: dceSelection, - codeName: `InitCode`, - getCode: func(d *Decl) []byte { return d.InitCode }, - }, - MethodListCode: &selectionCodeTester{ - t: t, - packages: packages, - dceSelection: dceSelection, - codeName: `MethodListCode`, - getCode: func(d *Decl) []byte { return d.MethodListCode }, - }, } } @@ -573,65 +753,77 @@ func (st *selectionTester) PrintDeclStatus() { } else { st.t.Logf(` [Dead] %q`, decl.FullName) } - if len(decl.DeclCode) > 0 { - st.t.Logf(` DeclCode: %q`, string(decl.DeclCode)) - } - if len(decl.InitCode) > 0 { - st.t.Logf(` InitCode: %q`, string(decl.InitCode)) - } - if len(decl.MethodListCode) > 0 { - st.t.Logf(` MethodListCode: %q`, string(decl.MethodListCode)) - } - if len(decl.TypeInitCode) > 0 { - st.t.Logf(` TypeInitCode: %q`, string(decl.TypeInitCode)) - } - if len(decl.Vars) > 0 { - st.t.Logf(` Vars: %v`, decl.Vars) - } } } } -type selectionCodeTester struct { - t *testing.T - packages []*Archive - dceSelection map[*Decl]struct{} - codeName string - getCode func(*Decl) []byte -} - -func (ct *selectionCodeTester) IsAlive(pattern string) { - ct.t.Helper() - decl := ct.FindDeclMatch(pattern) - if _, ok := ct.dceSelection[decl]; !ok { - ct.t.Error(`expected the`, ct.codeName, `code to be alive:`, pattern) +func (st *selectionTester) IsAlive(declFullName string) { + st.t.Helper() + decl := st.FindDecl(declFullName) + if _, ok := st.dceSelection[decl]; !ok { + st.t.Error(`expected the decl to be alive:`, declFullName) } } -func (ct *selectionCodeTester) IsDead(pattern string) { - ct.t.Helper() - decl := ct.FindDeclMatch(pattern) - if _, ok := ct.dceSelection[decl]; ok { - ct.t.Error(`expected the`, ct.codeName, `code to be dead:`, pattern) +func (st *selectionTester) IsDead(declFullName string) { + st.t.Helper() + decl := st.FindDecl(declFullName) + if _, ok := st.dceSelection[decl]; ok { + st.t.Error(`expected the decl to be dead:`, declFullName) } } -func (ct *selectionCodeTester) FindDeclMatch(pattern string) *Decl { - ct.t.Helper() - regex := regexp.MustCompile(pattern) +func (st *selectionTester) FindDecl(declFullName string) *Decl { + st.t.Helper() var found *Decl - for _, pkg := range ct.packages { + for _, pkg := range st.packages { for _, d := range pkg.Declarations { - if regex.Match(ct.getCode(d)) { + if d.FullName == declFullName { if found != nil { - ct.t.Fatal(`multiple`, ct.codeName, `found containing pattern:`, pattern) + st.t.Fatal(`multiple decls found with the name`, declFullName) } found = d } } } if found == nil { - ct.t.Fatal(ct.codeName, `not found with pattern:`, pattern) + st.t.Fatal(`no decl found by the name`, declFullName) } return found } + +func checkForDeclFullNames(t *testing.T, archives map[string]*Archive, expectedFullNames ...string) { + t.Helper() + + expected := map[string]int{} + counts := map[string]int{} + for _, name := range expectedFullNames { + expected[name]++ + counts[name]++ + } + for _, pkg := range archives { + for _, decl := range pkg.Declarations { + if found, has := expected[decl.FullName]; has { + if found <= 0 { + t.Errorf(`decl name existed more than %d time(s): %q`, counts[decl.FullName], decl.FullName) + } else { + expected[decl.FullName]-- + } + } + } + } + for imp, found := range expected { + if found > 0 { + t.Errorf(`missing %d decl name(s): %q`, found, imp) + } + } + if t.Failed() { + t.Log("Declarations:") + for pkgName, pkg := range archives { + t.Logf("\t%q", pkgName) + for i, decl := range pkg.Declarations { + t.Logf("\t\t%d:\t%q", i, decl.FullName) + } + } + } +} diff --git a/compiler/declNames.go b/compiler/declNames.go new file mode 100644 index 000000000..ea4528d9e --- /dev/null +++ b/compiler/declNames.go @@ -0,0 +1,70 @@ +package compiler + +import ( + "go/types" + + "github.com/gopherjs/gopherjs/compiler/internal/symbol" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" +) + +// importDeclFullName returns a unique name for an import declaration. +// This import name may be duplicated in different packages if they both +// import the same package, they are only unique per package. +func importDeclFullName(importedPkg *types.Package) string { + return `import:` + importedPkg.Path() +} + +// varDeclFullName returns a name for a package-level variable declaration. +// This var name only references the first named variable in an assignment. +// If no variables are named, the name is `var:blank` and not unique. +func varDeclFullName(init *types.Initializer) string { + for _, lhs := range init.Lhs { + if lhs.Name() != `_` { + return `var:` + symbol.New(lhs).String() + } + } + return `var:blank` +} + +// funcVarDeclFullName returns a name for a package-level variable +// that is used for a function (without a receiver) declaration. +// The name is unique unless the function is an `init` function. +// If the function is generic, this declaration name is also for the list +// of instantiations of the function. +func funcVarDeclFullName(o *types.Func) string { + return `funcVar:` + symbol.New(o).String() +} + +// mainFuncFullName returns the name for the declaration used to invoke the +// main function of the program. There should only be one decl with this name. +func mainFuncDeclFullName() string { + return `init:main` +} + +// funcDeclFullName returns a name for a package-level function +// declaration for the given instance of a function. +// The name is unique except unless the function is an `init` function. +func funcDeclFullName(inst typeparams.Instance) string { + return `func:` + inst.String() +} + +// typeVarDeclFullName returns a unique name for a package-level variable +// that is used for a named type declaration. +// If the type is generic, this declaration name is also for the list +// of instantiations of the type. +func typeVarDeclFullName(o *types.TypeName) string { + return `typeVar:` + symbol.New(o).String() +} + +// typeDeclFullName returns a unique name for a package-level type declaration +// for the given instance of a type. Names are only unique per package. +func typeDeclFullName(inst typeparams.Instance) string { + return `type:` + inst.String() +} + +// anonTypeDeclFullName returns a unique name for a package-level type +// declaration for an anonymous type. Names are only unique per package. +// These names are generated for types that are not named in the source code. +func anonTypeDeclFullName(o types.Object) string { + return `anonType:` + symbol.New(o).String() +} diff --git a/compiler/decls.go b/compiler/decls.go index 5419c4c7d..9216f5359 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -160,6 +160,7 @@ func (fc *funcContext) importDecls() (importedPaths []string, importDecls []*Dec func (fc *funcContext) newImportDecl(importedPkg *types.Package) *Decl { pkgVar := fc.importedPkgVar(importedPkg) d := &Decl{ + FullName: importDeclFullName(importedPkg), 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) }), @@ -224,7 +225,9 @@ func (fc *funcContext) varDecls(vars []*types.Var) []*Decl { // newVarDecl creates a new Decl describing a variable, given an explicit // initializer. func (fc *funcContext) newVarDecl(init *types.Initializer) *Decl { - var d Decl + d := &Decl{ + FullName: varDeclFullName(init), + } assignLHS := []ast.Expr{} for _, o := range init.Lhs { @@ -241,7 +244,7 @@ func (fc *funcContext) newVarDecl(init *types.Initializer) *Decl { } } - fc.pkgCtx.CollectDCEDeps(&d, func() { + fc.pkgCtx.CollectDCEDeps(d, func() { fc.localVars = nil d.InitCode = fc.CatchOutput(1, func() { fc.translateStmt(&ast.AssignStmt{ @@ -261,7 +264,7 @@ func (fc *funcContext) newVarDecl(init *types.Initializer) *Decl { if len(init.Lhs) != 1 || analysis.HasSideEffect(init.Rhs, fc.pkgCtx.Info.Info) { d.Dce().SetAsAlive() } - return &d + return d } // funcDecls translates all package-level function and methods. @@ -279,15 +282,18 @@ func (fc *funcContext) funcDecls(functions []*ast.FuncDecl) ([]*Decl, error) { if fun.Recv == nil { // Auxiliary decl shared by all instances of the function that defines // package-level variable by which they all are referenced. - varDecl := Decl{} + objName := fc.objectName(o) + varDecl := &Decl{ + FullName: funcVarDeclFullName(o), + Vars: []string{objName}, + } varDecl.Dce().SetName(o) - varDecl.Vars = []string{fc.objectName(o)} if o.Type().(*types.Signature).TypeParams().Len() != 0 { varDecl.DeclCode = fc.CatchOutput(0, func() { - fc.Printf("%s = {};", fc.objectName(o)) + fc.Printf("%s = {};", objName) }) } - funcDecls = append(funcDecls, &varDecl) + funcDecls = append(funcDecls, varDecl) } for _, inst := range fc.knownInstances(o) { @@ -306,6 +312,7 @@ func (fc *funcContext) funcDecls(functions []*ast.FuncDecl) ([]*Decl, error) { // been initialized. It must come after all other functions, especially all // init() functions, otherwise main() will be invoked too early. funcDecls = append(funcDecls, &Decl{ + FullName: mainFuncDeclFullName(), InitCode: fc.CatchOutput(1, func() { fc.translateStmt(fc.callMainFunc(mainFunc), nil) }), }) } @@ -316,7 +323,7 @@ func (fc *funcContext) funcDecls(functions []*ast.FuncDecl) ([]*Decl, error) { func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance) *Decl { o := fc.pkgCtx.Defs[fun.Name].(*types.Func) d := &Decl{ - FullName: o.FullName(), + FullName: funcDeclFullName(inst), Blocking: fc.pkgCtx.IsBlocking(inst), LinkingName: symbol.New(o), } @@ -417,15 +424,19 @@ func (fc *funcContext) namedTypeDecls(typeNames typesutil.TypeNames) ([]*Decl, e // of the type, keyed by the type argument combination. Otherwise it contains // the type definition directly. func (fc *funcContext) newNamedTypeVarDecl(obj *types.TypeName) *Decl { - varDecl := &Decl{Vars: []string{fc.objectName(obj)}} + name := fc.objectName(obj) + varDecl := &Decl{ + FullName: typeVarDeclFullName(obj), + Vars: []string{name}, + } if typeparams.HasTypeParams(obj.Type()) { varDecl.DeclCode = fc.CatchOutput(0, func() { - fc.Printf("%s = {};", fc.objectName(obj)) + fc.Printf("%s = {};", name) }) } if isPkgLevel(obj) { varDecl.TypeInitCode = fc.CatchOutput(0, func() { - fc.Printf("$pkg.%s = %s;", encodeIdent(obj.Name()), fc.objectName(obj)) + fc.Printf("$pkg.%s = %s;", encodeIdent(obj.Name()), name) }) } return varDecl @@ -449,7 +460,9 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er } underlying := instanceType.Underlying() - d := &Decl{} + d := &Decl{ + FullName: typeDeclFullName(inst), + } d.Dce().SetName(inst.Object, inst.TArgs...) fc.pkgCtx.CollectDCEDeps(d, func() { // Code that declares a JS type (i.e. prototype) for each Go type. @@ -571,7 +584,8 @@ func (fc *funcContext) anonTypeDecls(anonTypes []*types.TypeName) []*Decl { decls := []*Decl{} for _, t := range anonTypes { d := &Decl{ - Vars: []string{t.Name()}, + FullName: anonTypeDeclFullName(t), + Vars: []string{t.Name()}, } d.Dce().SetName(t) fc.pkgCtx.CollectDCEDeps(d, func() { diff --git a/compiler/functions.go b/compiler/functions.go index 86d3916bf..592992efc 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -82,7 +82,7 @@ func (fc *funcContext) namedFuncContext(inst typeparams.Instance) *funcContext { // 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.FuncLitInfo(fun) + info := fc.pkgCtx.FuncLitInfo(fun, fc.TypeArgs()) sig := fc.pkgCtx.TypeOf(fun).(*types.Signature) o := types.NewFunc(fun.Pos(), fc.pkgCtx.Pkg, fc.newLitFuncName(), sig) inst := typeparams.Instance{Object: o} diff --git a/compiler/internal/analysis/defer.go b/compiler/internal/analysis/defer.go index 36b19ef2a..5d4f151a3 100644 --- a/compiler/internal/analysis/defer.go +++ b/compiler/internal/analysis/defer.go @@ -2,8 +2,10 @@ package analysis import ( "go/ast" + "go/types" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/typesutil" ) // deferStmt represents a defer statement that is blocking or not. @@ -49,8 +51,9 @@ import ( // // [CFG]: https://en.wikipedia.org/wiki/Control-flow_graph type deferStmt struct { - inst *typeparams.Instance - lit *ast.FuncLit + obj types.Object + lit *ast.FuncLit + typeArgs typesutil.TypeList } // newBlockingDefer creates a new defer statement that is blocking. @@ -65,25 +68,25 @@ func newBlockingDefer() *deferStmt { // newInstDefer creates a new defer statement for an instances of a method. // The instance is used to look up the blocking information later. func newInstDefer(inst typeparams.Instance) *deferStmt { - return &deferStmt{inst: &inst} + return &deferStmt{obj: inst.Object, typeArgs: inst.TArgs} } // newLitDefer creates a new defer statement for a function literal. // The literal is used to look up the blocking information later. -func newLitDefer(lit *ast.FuncLit) *deferStmt { - return &deferStmt{lit: lit} +func newLitDefer(lit *ast.FuncLit, typeArgs typesutil.TypeList) *deferStmt { + return &deferStmt{lit: lit, typeArgs: typeArgs} } // IsBlocking determines if the defer statement is blocking or not. func (d *deferStmt) IsBlocking(info *Info) bool { - // If the instance or the literal is set then we can look up the blocking, + // If the object or the literal is set then we can look up the blocking, // otherwise assume blocking because otherwise the defer wouldn't // have been recorded. - if d.inst != nil { - return info.FuncInfo(*d.inst).IsBlocking() + if d.obj != nil { + return info.IsBlocking(typeparams.Instance{Object: d.obj, TArgs: d.typeArgs}) } if d.lit != nil { - return info.FuncLitInfo(d.lit).IsBlocking() + return info.FuncLitInfo(d.lit, d.typeArgs).IsBlocking() } return true } diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go index 42918258e..803952b24 100644 --- a/compiler/internal/analysis/info.go +++ b/compiler/internal/analysis/info.go @@ -55,22 +55,24 @@ type Info struct { instanceSets *typeparams.PackageInstanceSets HasPointer map[*types.Var]bool funcInstInfos *typeparams.InstanceMap[*FuncInfo] - funcLitInfos map[*ast.FuncLit]*FuncInfo + funcLitInfos map[*ast.FuncLit][]*FuncInfo InitFuncInfo *FuncInfo // Context for package variable initialization. isImportedBlocking func(typeparams.Instance) bool // For functions from other packages. allInfos []*FuncInfo } -func (info *Info) newFuncInfo(n ast.Node, inst *typeparams.Instance) *FuncInfo { +func (info *Info) newFuncInfo(n ast.Node, obj types.Object, typeArgs typesutil.TypeList, resolver *typeparams.Resolver) *FuncInfo { funcInfo := &FuncInfo{ pkgInfo: info, Flattened: make(map[ast.Node]bool), Blocking: make(map[ast.Node]bool), GotoLabel: make(map[*types.Label]bool), loopReturnIndex: -1, - localInstCallees: new(typeparams.InstanceMap[[]astPath]), + instCallees: new(typeparams.InstanceMap[[]astPath]), literalFuncCallees: make(map[*ast.FuncLit]astPath), + typeArgs: typeArgs, + resolver: resolver, } // Register the function in the appropriate map. @@ -86,13 +88,14 @@ func (info *Info) newFuncInfo(n ast.Node, inst *typeparams.Instance) *FuncInfo { funcInfo.Blocking[n] = true } - if inst == nil { - inst = &typeparams.Instance{Object: info.Defs[n.Name]} + if obj == nil { + obj = info.Defs[n.Name] } - info.funcInstInfos.Set(*inst, funcInfo) + inst := typeparams.Instance{Object: obj, TArgs: typeArgs} + info.funcInstInfos.Set(inst, funcInfo) case *ast.FuncLit: - info.funcLitInfos[n] = funcInfo + info.funcLitInfos[n] = append(info.funcLitInfos[n], funcInfo) } // And add it to the list of all functions. @@ -105,28 +108,40 @@ func (info *Info) newFuncInfoInstances(fd *ast.FuncDecl) []*FuncInfo { obj := info.Defs[fd.Name] instances := info.instanceSets.Pkg(info.Pkg).ForObj(obj) if len(instances) == 0 { - // No instances found, this is a non-generic function. - return []*FuncInfo{info.newFuncInfo(fd, nil)} + if typeparams.HasTypeParams(obj.Type()) { + // This is a generic function, but no instances were found, + // this is an unused function, so skip over it. + return []*FuncInfo{} + } + + // No instances found and this is a non-generic function. + return []*FuncInfo{info.newFuncInfo(fd, nil, nil, nil)} } funcInfos := make([]*FuncInfo, 0, len(instances)) for _, inst := range instances { - fi := info.newFuncInfo(fd, &inst) + var resolver *typeparams.Resolver if sig, ok := obj.Type().(*types.Signature); ok { tp := typeparams.ToSlice(typeparams.SignatureTypeParams(sig)) - fi.resolver = typeparams.NewResolver(info.typeCtx, tp, inst.TArgs) + resolver = typeparams.NewResolver(info.typeCtx, tp, inst.TArgs) } + fi := info.newFuncInfo(fd, inst.Object, inst.TArgs, resolver) funcInfos = append(funcInfos, fi) } return funcInfos } // IsBlocking returns true if the function may contain blocking calls or operations. +// If inst is from a different package, this will use the isImportedBlocking +// to lookup the information from the other package. func (info *Info) IsBlocking(inst typeparams.Instance) bool { + if inst.Object.Pkg() != info.Pkg { + return info.isImportedBlocking(inst) + } if funInfo := info.FuncInfo(inst); funInfo != nil { return funInfo.IsBlocking() } - panic(fmt.Errorf(`info did not have function declaration instance for %q`, inst)) + panic(fmt.Errorf(`info did not have function declaration instance for %q`, inst.TypeString())) } // FuncInfo returns information about the given function declaration instance, or nil if not found. @@ -135,8 +150,16 @@ func (info *Info) FuncInfo(inst typeparams.Instance) *FuncInfo { } // FuncLitInfo returns information about the given function literal, or nil if not found. -func (info *Info) FuncLitInfo(fun *ast.FuncLit) *FuncInfo { - return info.funcLitInfos[fun] +// The given type arguments are used to identify the correct instance of the +// function literal in the case the literal was defined inside a generic function. +func (info *Info) FuncLitInfo(fun *ast.FuncLit, typeArgs typesutil.TypeList) *FuncInfo { + lits := info.funcLitInfos[fun] + for _, fi := range lits { + if fi.typeArgs.Equal(typeArgs) { + return fi + } + } + return nil } // VarsWithInitializers returns a set of package-level variables that have @@ -160,9 +183,9 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info HasPointer: make(map[*types.Var]bool), isImportedBlocking: isBlocking, funcInstInfos: new(typeparams.InstanceMap[*FuncInfo]), - funcLitInfos: make(map[*ast.FuncLit]*FuncInfo), + funcLitInfos: make(map[*ast.FuncLit][]*FuncInfo), } - info.InitFuncInfo = info.newFuncInfo(nil, nil) + info.InitFuncInfo = info.newFuncInfo(nil, nil, nil, nil) // Traverse the full AST of the package and collect information about existing // functions. @@ -190,19 +213,19 @@ func (info *Info) propagateFunctionBlocking() bool { done := true for _, caller := range info.allInfos { // Check calls to named functions and function-typed variables. - caller.localInstCallees.Iterate(func(callee typeparams.Instance, callSites []astPath) { - if info.FuncInfo(callee).IsBlocking() { + caller.instCallees.Iterate(func(callee typeparams.Instance, callSites []astPath) { + if info.IsBlocking(callee) { for _, callSite := range callSites { caller.markBlocking(callSite) } - caller.localInstCallees.Delete(callee) + caller.instCallees.Delete(callee) done = false } }) // Check direct calls to function literals. for callee, callSite := range caller.literalFuncCallees { - if info.FuncLitInfo(callee).IsBlocking() { + if info.FuncLitInfo(callee, caller.typeArgs).IsBlocking() { caller.markBlocking(callSite) delete(caller.literalFuncCallees, callee) done = false @@ -250,15 +273,18 @@ type FuncInfo struct { // returns defined before any defers in a loop may still be affected by // those defers because of the loop. See comment on [deferStmt]. loopReturnIndex int - // List of other named functions from the current package this function calls. + // List of other named functions in the current package or another package + // that this function calls. // If any of them are blocking, this function will become blocking too. - localInstCallees *typeparams.InstanceMap[[]astPath] + instCallees *typeparams.InstanceMap[[]astPath] // List of function literals directly called from this function (for example: // `func() { /* do stuff */ }()`). This is distinct from function literals // assigned to named variables (for example: `doStuff := func() {}; // doStuff()`), which are handled by localInstCallees. If any of them are // identified as blocking, this function will become blocking too. literalFuncCallees map[*ast.FuncLit]astPath + // typeArgs are the type arguments for the function instance. + typeArgs typesutil.TypeList // resolver is used by this function instance to resolve any type arguments // for internal function calls. // This may be nil if not an instance of a generic function. @@ -276,6 +302,12 @@ func (fi *FuncInfo) IsBlocking() bool { return fi == nil || len(fi.Blocking) != 0 } +// TypeArgs gets the type arguments of this inside of a function instance +// or empty if not in a function instance. +func (fi *FuncInfo) TypeArgs() typesutil.TypeList { + return fi.typeArgs +} + // propagateReturnBlocking updates the blocking on the return statements. // See comment on [deferStmt]. // @@ -341,7 +373,7 @@ func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor { return nil case *ast.FuncLit: // Analyze the function literal in its own context. - return fi.pkgInfo.newFuncInfo(n, nil) + return fi.pkgInfo.newFuncInfo(n, nil, fi.typeArgs, fi.resolver) case *ast.BranchStmt: switch n.Tok { case token.GOTO: @@ -502,7 +534,7 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr, deferredCall bool) ast.Visito // Register literal function call site in case it is identified as blocking. fi.literalFuncCallees[f] = fi.visitorStack.copy() if deferredCall { - fi.deferStmts = append(fi.deferStmts, newLitDefer(f)) + fi.deferStmts = append(fi.deferStmts, newLitDefer(f, fi.typeArgs)) } return nil // No need to walk under this CallExpr, we already did it manually. case *ast.IndexExpr: @@ -610,21 +642,11 @@ func (fi *FuncInfo) callToNamedFunc(callee typeparams.Instance, deferredCall boo } } - if o.Pkg() != fi.pkgInfo.Pkg { - if fi.pkgInfo.isImportedBlocking(callee) { - fi.markBlocking(fi.visitorStack) - if deferredCall { - fi.deferStmts = append(fi.deferStmts, newBlockingDefer()) - } - } - return - } - // We probably don't know yet whether the callee function is blocking. // Record the calls site for the later stage. - paths := fi.localInstCallees.Get(callee) + paths := fi.instCallees.Get(callee) paths = append(paths, fi.visitorStack.copy()) - fi.localInstCallees.Set(callee, paths) + fi.instCallees.Set(callee, paths) if deferredCall { fi.deferStmts = append(fi.deferStmts, newInstDefer(callee)) } diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go index d4e21b501..176620d82 100644 --- a/compiler/internal/analysis/info_test.go +++ b/compiler/internal/analysis/info_test.go @@ -156,10 +156,10 @@ func TestBlocking_GoRoutines_WithFuncLiterals(t *testing.T) { }(<-c) }`) bt.assertNotBlocking(`notBlocking`) - bt.assertBlockingLit(4) + bt.assertBlockingLit(4, ``) bt.assertBlocking(`blocking`) - bt.assertNotBlockingLit(10) + bt.assertNotBlockingLit(10, ``) } func TestBlocking_GoRoutines_WithNamedFuncs(t *testing.T) { @@ -210,13 +210,13 @@ func TestBlocking_Defers_WithoutReturns_WithFuncLiterals(t *testing.T) { }(true) }`) bt.assertBlocking(`blockingBody`) - bt.assertBlockingLit(4) + bt.assertBlockingLit(4, ``) bt.assertBlocking(`blockingArg`) - bt.assertNotBlockingLit(10) + bt.assertNotBlockingLit(10, ``) bt.assertNotBlocking(`notBlocking`) - bt.assertNotBlockingLit(16) + bt.assertNotBlockingLit(16, ``) } func TestBlocking_Defers_WithoutReturns_WithNamedFuncs(t *testing.T) { @@ -278,13 +278,13 @@ func TestBlocking_Defers_WithReturns_WithFuncLiterals(t *testing.T) { return 42 }`) bt.assertBlocking(`blockingBody`) - bt.assertBlockingLit(4) + bt.assertBlockingLit(4, ``) bt.assertBlocking(`blockingArg`) - bt.assertNotBlockingLit(11) + bt.assertNotBlockingLit(11, ``) bt.assertNotBlocking(`notBlocking`) - bt.assertNotBlockingLit(18) + bt.assertNotBlockingLit(18, ``) } func TestBlocking_Defers_WithReturns_WithNamedFuncs(t *testing.T) { @@ -363,13 +363,13 @@ func TestBlocking_Defers_WithMultipleReturns(t *testing.T) { return true // line 31 }`) bt.assertBlocking(`foo`) - bt.assertNotBlockingLit(4) + bt.assertNotBlockingLit(4, ``) // Early escape from function without blocking defers is not blocking. bt.assertNotBlockingReturn(11) - bt.assertNotBlockingLit(14) + bt.assertNotBlockingLit(14, ``) // Function has had blocking by this point but no blocking defers yet. bt.assertNotBlockingReturn(20) - bt.assertBlockingLit(24) + bt.assertBlockingLit(24, ``) // The return is blocking because of a blocking defer. bt.assertBlockingReturn(28) // Technically the return on line 31 is not blocking since the defer that @@ -770,13 +770,13 @@ func TestBlocking_FunctionLiteral(t *testing.T) { bt.assertBlocking(`blocking`) bt.assertBlocking(`indirectlyBlocking`) - bt.assertBlockingLit(9) + bt.assertBlockingLit(9, ``) bt.assertBlocking(`directlyBlocking`) - bt.assertBlockingLit(13) + bt.assertBlockingLit(13, ``) bt.assertNotBlocking(`notBlocking`) - bt.assertNotBlockingLit(20) + bt.assertNotBlockingLit(20, ``) } func TestBlocking_LinkedFunction(t *testing.T) { @@ -818,9 +818,9 @@ func TestBlocking_Instances_WithSingleTypeArg(t *testing.T) { // blocking and notBlocking as generics do not have FuncInfo, // only non-generic and instances have FuncInfo. - bt.assertBlockingInst(`test.blocking[int]`) + bt.assertBlockingInst(`pkg/test.blocking`) bt.assertBlocking(`bInt`) - bt.assertNotBlockingInst(`test.notBlocking[uint]`) + bt.assertNotBlockingInst(`pkg/test.notBlocking`) bt.assertNotBlocking(`nbUint`) } @@ -849,9 +849,9 @@ func TestBlocking_Instances_WithMultipleTypeArgs(t *testing.T) { // blocking and notBlocking as generics do not have FuncInfo, // only non-generic and instances have FuncInfo. - bt.assertBlockingInst(`test.blocking[string, int, map[string]int]`) + bt.assertBlockingInst(`pkg/test.blocking`) bt.assertBlocking(`bInt`) - bt.assertNotBlockingInst(`test.notBlocking[string, uint, map[string]uint]`) + bt.assertNotBlockingInst(`pkg/test.notBlocking`) bt.assertNotBlocking(`nbUint`) } @@ -1075,7 +1075,7 @@ func TestBlocking_VarFunctionCall(t *testing.T) { func bar() { foo() }`) - bt.assertNotBlockingLit(3) + bt.assertNotBlockingLit(3, ``) bt.assertBlocking(`bar`) } @@ -1233,12 +1233,12 @@ func TestBlocking_InstantiationBlocking(t *testing.T) { bt.assertBlocking(`BazBlocker.Baz`) bt.assertBlocking(`blockingViaExplicit`) bt.assertBlocking(`blockingViaImplicit`) - bt.assertBlockingInst(`test.FooBaz[pkg/test.BazBlocker]`) + bt.assertBlockingInst(`pkg/test.FooBaz`) bt.assertNotBlocking(`BazNotBlocker.Baz`) bt.assertNotBlocking(`notBlockingViaExplicit`) bt.assertNotBlocking(`notBlockingViaImplicit`) - bt.assertNotBlockingInst(`test.FooBaz[pkg/test.BazNotBlocker]`) + bt.assertNotBlockingInst(`pkg/test.FooBaz`) } func TestBlocking_NestedInstantiations(t *testing.T) { @@ -1272,12 +1272,129 @@ func TestBlocking_NestedInstantiations(t *testing.T) { bt.assertFuncInstCount(8) bt.assertNotBlocking(`bazInt`) bt.assertNotBlocking(`bazString`) - bt.assertNotBlockingInst(`test.Foo[map[int]int]`) - bt.assertNotBlockingInst(`test.Foo[map[int]string]`) - bt.assertNotBlockingInst(`test.Bar[int, int, map[int]int]`) - bt.assertNotBlockingInst(`test.Bar[int, string, map[int]string]`) - bt.assertNotBlockingInst(`test.Baz[int, []int]`) - bt.assertNotBlockingInst(`test.Baz[string, []string]`) + bt.assertNotBlockingInst(`pkg/test.Foo`) + bt.assertNotBlockingInst(`pkg/test.Foo`) + bt.assertNotBlockingInst(`pkg/test.Bar`) + bt.assertNotBlockingInst(`pkg/test.Bar`) + bt.assertNotBlockingInst(`pkg/test.Baz`) + bt.assertNotBlockingInst(`pkg/test.Baz`) +} + +func TestBlocking_UnusedGenericFunctions(t *testing.T) { + // Checking that the type parameters are being propagated down into callee. + // This is based off of go1.19.13/test/typeparam/orderedmap.go + bt := newBlockingTest(t, + `package test + + type node[K, V any] struct { + key K + val V + left, right *node[K, V] + } + + type Tree[K, V any] struct { + root *node[K, V] + eq func(K, K) bool + } + + func New[K, V any](eq func(K, K) bool) *Tree[K, V] { + return &Tree[K, V]{eq: eq} + } + + func NewStrKey[K ~string, V any]() *Tree[K, V] { // unused + return New[K, V](func(k1, k2 K) bool { + return string(k1) == string(k2) + }) + } + + func NewStrStr[V any]() *Tree[string, V] { // unused + return NewStrKey[string, V]() + } + + func main() { + t := New[int, string](func(k1, k2 int) bool { + return k1 == k2 + }) + println(t) + }`) + bt.assertFuncInstCount(2) + // Notice that `NewStrKey` and `NewStrStr` are not called so doesn't have + // any known instances and therefore they don't have any FuncInfos. + bt.assertNotBlockingInst(`pkg/test.New`) + bt.assertNotBlocking(`main`) +} + +func TestBlocking_LitInstanceCalls(t *testing.T) { + // Literals defined inside a generic function must inherit the + // type arguments (resolver) of the enclosing instance it is defined in + // so that things like calls to other generic functions create the + // call to the correct concrete instance. + bt := newBlockingTest(t, + `package test + + func foo[T any](x T) { + println(x) + } + + func bar[T any](x T) { + f := func(v T) { // line 8 + foo[T](v) + } + f(x) + } + + func main() { + bar[int](42) + bar[float64](3.14) + }`) + bt.assertFuncInstCount(5) + + bt.assertNotBlockingInst(`pkg/test.foo`) + bt.assertNotBlockingInst(`pkg/test.foo`) + bt.assertNotBlockingLit(8, `int`) + bt.assertNotBlockingLit(8, `float64`) + // The following are blocking because the function literal call. + bt.assertBlockingInst(`pkg/test.bar`) + bt.assertBlockingInst(`pkg/test.bar`) +} + +func TestBlocking_BlockingLitInstance(t *testing.T) { + bt := newBlockingTest(t, + `package test + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + type Foo interface { Baz() } + func FooBaz[T Foo](foo T) func() { + return func() { // line 17 + foo.Baz() + } + } + + func main() { + _ = FooBaz(BazBlocker{}) + _ = FooBaz(BazNotBlocker{}) + }`) + bt.assertFuncInstCount(5) + + bt.assertBlocking(`BazBlocker.Baz`) + // THe following is not blocking because the function literal is not called. + bt.assertNotBlockingInst(`pkg/test.FooBaz`) + bt.assertBlockingLit(17, `pkg/test.BazBlocker`) + + bt.assertNotBlocking(`BazNotBlocker.Baz`) + bt.assertNotBlockingInst(`pkg/test.FooBaz`) + bt.assertNotBlockingLit(17, `pkg/test.BazNotBlocker`) } func TestBlocking_MethodSelection(t *testing.T) { @@ -1333,13 +1450,13 @@ func TestBlocking_MethodSelection(t *testing.T) { bt.assertFuncInstCount(8) bt.assertBlocking(`BazBlocker.Baz`) - bt.assertBlockingInst(`test.ByMethodExpression[pkg/test.BazBlocker]`) - bt.assertBlockingInst(`test.ByInstance[pkg/test.BazBlocker]`) + bt.assertBlockingInst(`pkg/test.FooBaz.ByMethodExpression`) + bt.assertBlockingInst(`pkg/test.FooBaz.ByInstance`) bt.assertBlocking(`blocking`) bt.assertNotBlocking(`BazNotBlocker.Baz`) - bt.assertNotBlockingInst(`test.ByMethodExpression[pkg/test.BazNotBlocker]`) - bt.assertNotBlockingInst(`test.ByInstance[pkg/test.BazNotBlocker]`) + bt.assertNotBlockingInst(`pkg/test.FooBaz.ByMethodExpression`) + bt.assertNotBlockingInst(`pkg/test.FooBaz.ByInstance`) bt.assertNotBlocking(`notBlocking`) } @@ -1529,7 +1646,7 @@ func (bt *blockingTest) assertFuncInstCount(expCount int) { if got := bt.pkgInfo.funcInstInfos.Len(); got != expCount { bt.f.T.Errorf(`Got %d function instance infos but expected %d.`, got, expCount) for i, inst := range bt.pkgInfo.funcInstInfos.Keys() { - bt.f.T.Logf(` %d. %q`, i+1, inst.TypeString()) + bt.f.T.Logf(` %d. %q`, i+1, inst.String()) } } } @@ -1597,24 +1714,41 @@ func (bt *blockingTest) isTypesFuncBlocking(funcName string) bool { return bt.pkgInfo.IsBlocking(inst) } -func (bt *blockingTest) assertBlockingLit(lineNo int) { - if !bt.isFuncLitBlocking(lineNo) { - bt.f.T.Errorf(`Got FuncLit at line %d as not blocking but expected it to be blocking.`, lineNo) +func (bt *blockingTest) assertBlockingLit(lineNo int, typeArgsStr string) { + if !bt.isFuncLitBlocking(lineNo, typeArgsStr) { + bt.f.T.Errorf(`Got FuncLit at line %d with type args %q as not blocking but expected it to be blocking.`, lineNo, typeArgsStr) } } -func (bt *blockingTest) assertNotBlockingLit(lineNo int) { - if bt.isFuncLitBlocking(lineNo) { - bt.f.T.Errorf(`Got FuncLit at line %d as blocking but expected it to be not blocking.`, lineNo) +func (bt *blockingTest) assertNotBlockingLit(lineNo int, typeArgsStr string) { + if bt.isFuncLitBlocking(lineNo, typeArgsStr) { + bt.f.T.Errorf(`Got FuncLit at line %d with type args %q as blocking but expected it to be not blocking.`, lineNo, typeArgsStr) } } -func (bt *blockingTest) isFuncLitBlocking(lineNo int) bool { +func (bt *blockingTest) isFuncLitBlocking(lineNo int, typeArgsStr string) bool { fnLit := srctesting.GetNodeAtLineNo[*ast.FuncLit](bt.file, bt.f.FileSet, lineNo) if fnLit == nil { bt.f.T.Fatalf(`FuncLit on line %d not found in the AST.`, lineNo) } - return bt.pkgInfo.FuncLitInfo(fnLit).IsBlocking() + + fis, ok := bt.pkgInfo.funcLitInfos[fnLit] + if !ok { + bt.f.T.Fatalf(`No FuncInfo found for FuncLit at line %d.`, lineNo) + } + + for _, fi := range fis { + if fi.typeArgs.String() == typeArgsStr { + return fi.IsBlocking() + } + } + + bt.f.T.Logf("FuncList instances:") + for i, fi := range fis { + bt.f.T.Logf("\t%d. %q\n", i, fi.typeArgs.String()) + } + bt.f.T.Fatalf(`No FuncInfo found for FuncLit at line %d with type args %q.`, lineNo, typeArgsStr) + return false } func (bt *blockingTest) assertBlockingInst(instanceStr string) { @@ -1632,13 +1766,13 @@ func (bt *blockingTest) assertNotBlockingInst(instanceStr string) { func (bt *blockingTest) isFuncInstBlocking(instanceStr string) bool { instances := bt.pkgInfo.funcInstInfos.Keys() for _, inst := range instances { - if inst.TypeString() == instanceStr { + if inst.String() == instanceStr { return bt.pkgInfo.FuncInfo(inst).IsBlocking() } } bt.f.T.Logf(`Function instances found in package info:`) for i, inst := range instances { - bt.f.T.Logf(` %d. %s`, i+1, inst.TypeString()) + bt.f.T.Logf(` %d. %s`, i+1, inst.String()) } bt.f.T.Fatalf(`No function instance found for %q in package info.`, instanceStr) return false diff --git a/compiler/internal/typeparams/map.go b/compiler/internal/typeparams/map.go index 4f6645421..dbe07a54e 100644 --- a/compiler/internal/typeparams/map.go +++ b/compiler/internal/typeparams/map.go @@ -40,7 +40,7 @@ func (im *InstanceMap[V]) findIndex(key Instance) (mapBucket[V], int) { if im != nil && im.data != nil { bucket := im.data[key.Object][typeHash(im.hasher, key.TArgs...)] for i, candidate := range bucket { - if candidate != nil && typeArgsEq(candidate.key.TArgs, key.TArgs) { + if candidate != nil && candidate.key.TArgs.Equal(key.TArgs) { return bucket, i } } @@ -90,7 +90,7 @@ func (im *InstanceMap[V]) Set(key Instance, value V) V { for i, candidate := range bucket { if candidate == nil { hole = i - } else if typeArgsEq(candidate.key.TArgs, key.TArgs) { + } else if candidate.key.TArgs.Equal(key.TArgs) { old := candidate.value candidate.value = value return old @@ -192,17 +192,3 @@ func typeHash(hasher typeutil.Hasher, types ...types.Type) uint32 { } return hash } - -// typeArgsEq returns if both lists of type arguments are identical. -func typeArgsEq(a, b []types.Type) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if !types.Identical(a[i], b[i]) { - return false - } - } - - return true -} diff --git a/compiler/internal/typeparams/map_test.go b/compiler/internal/typeparams/map_test.go index ed65587c7..baa31f64a 100644 --- a/compiler/internal/typeparams/map_test.go +++ b/compiler/internal/typeparams/map_test.go @@ -317,7 +317,7 @@ func keysMatch(a, b []Instance) bool { func keyAt(keys []Instance, target Instance) int { for i, v := range keys { - if v.Object == target.Object && typeArgsEq(v.TArgs, target.TArgs) { + if v.Object == target.Object && v.TArgs.Equal(target.TArgs) { return i } } diff --git a/compiler/package.go b/compiler/package.go index 4cd800607..95b3f8b9e 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -187,12 +187,9 @@ func (ic *ImportContext) isBlocking(inst typeparams.Instance) bool { panic(err) } - // TODO(grantnelson-wf): f.FullName() does not differentiate between - // different instantiations of the same generic function. This needs to be - // fixed when the declaration names are updated to better support instances. - fullName := f.FullName() + fullName := funcDeclFullName(inst) for _, d := range archive.Declarations { - if string(d.FullName) == fullName { + if d.FullName == fullName { return d.Blocking } } diff --git a/compiler/typesutil/typelist.go b/compiler/typesutil/typelist.go index 04d0d6869..8072b6f44 100644 --- a/compiler/typesutil/typelist.go +++ b/compiler/typesutil/typelist.go @@ -18,3 +18,15 @@ func (tl TypeList) String() string { } return buf.String() } + +func (tl TypeList) Equal(other TypeList) bool { + if len(tl) != len(other) { + return false + } + for i := range tl { + if !types.Identical(tl[i], other[i]) { + return false + } + } + return true +} From ea5dd714a2c7b9e100e00941a619f3258b84f780 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 21 Nov 2024 12:40:44 -0700 Subject: [PATCH 28/58] merged #1351 --- compiler/internal/analysis/info_test.go | 196 ++++++++++++++++-------- compiler/typesutil/typelist.go | 1 + 2 files changed, 136 insertions(+), 61 deletions(-) diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go index 176620d82..3556324cf 100644 --- a/compiler/internal/analysis/info_test.go +++ b/compiler/internal/analysis/info_test.go @@ -317,15 +317,15 @@ func TestBlocking_Defers_WithReturns_WithNamedFuncs(t *testing.T) { bt.assertNotBlocking(`nonBlockingPrint`) bt.assertBlocking(`blockingBody`) - bt.assertBlockingReturn(13) + bt.assertBlockingReturn(13, ``) bt.assertBlocking(`blockingArg`) // The defer is non-blocking so the return is not blocking // even though the function is blocking. - bt.assertNotBlockingReturn(18) + bt.assertNotBlockingReturn(18, ``) bt.assertNotBlocking(`notBlocking`) - bt.assertNotBlockingReturn(23) + bt.assertNotBlockingReturn(23, ``) } func TestBlocking_Defers_WithMultipleReturns(t *testing.T) { @@ -365,13 +365,13 @@ func TestBlocking_Defers_WithMultipleReturns(t *testing.T) { bt.assertBlocking(`foo`) bt.assertNotBlockingLit(4, ``) // Early escape from function without blocking defers is not blocking. - bt.assertNotBlockingReturn(11) + bt.assertNotBlockingReturn(11, ``) bt.assertNotBlockingLit(14, ``) // Function has had blocking by this point but no blocking defers yet. - bt.assertNotBlockingReturn(20) + bt.assertNotBlockingReturn(20, ``) bt.assertBlockingLit(24, ``) // The return is blocking because of a blocking defer. - bt.assertBlockingReturn(28) + bt.assertBlockingReturn(28, ``) // Technically the return on line 31 is not blocking since the defer that // is blocking can only exit through the return on line 28, but it would be // difficult to determine which defers would only affect certain returns @@ -384,7 +384,7 @@ func TestBlocking_Defers_WithMultipleReturns(t *testing.T) { // // For now we simply build up the list of defers as we go making // the return on line 31 also blocking. - bt.assertBlockingReturn(31) + bt.assertBlockingReturn(31, ``) } func TestBlocking_Defers_WithReturnsAndDefaultBlocking(t *testing.T) { @@ -453,12 +453,12 @@ func TestBlocking_Defers_WithReturnsAndDefaultBlocking(t *testing.T) { bt.assertBlocking(`deferMappedFuncCall`) // All of these returns are blocking because they have blocking defers. - bt.assertBlockingReturn(17) - bt.assertBlockingReturn(22) - bt.assertBlockingReturn(28) - bt.assertBlockingReturn(34) - bt.assertBlockingReturn(40) - bt.assertBlockingReturn(49) + bt.assertBlockingReturn(17, ``) + bt.assertBlockingReturn(22, ``) + bt.assertBlockingReturn(28, ``) + bt.assertBlockingReturn(34, ``) + bt.assertBlockingReturn(40, ``) + bt.assertBlockingReturn(49, ``) } func TestBlocking_Defers_WithReturnsAndDeferBuiltin(t *testing.T) { @@ -477,7 +477,7 @@ func TestBlocking_Defers_WithReturnsAndDeferBuiltin(t *testing.T) { bt.assertFuncInstCount(1) bt.assertNotBlocking(`deferBuiltinCall`) - bt.assertNotBlockingReturn(10) + bt.assertNotBlockingReturn(10, ``) } func TestBlocking_Defers_WithReturnsInLoops(t *testing.T) { @@ -575,14 +575,14 @@ func TestBlocking_Defers_WithReturnsInLoops(t *testing.T) { // When the following 2 returns are defined there are no defers, however, // because of the loop, the blocking defers defined after the return will // block the returns. - bt.assertBlockingReturn(12) - bt.assertBlockingReturn(22) - bt.assertBlockingReturn(31) - bt.assertBlockingReturn(44) - bt.assertBlockingReturn(52) - bt.assertBlockingReturn(66) - bt.assertBlockingReturn(73) - bt.assertBlockingReturn(77) + bt.assertBlockingReturn(12, ``) + bt.assertBlockingReturn(22, ``) + bt.assertBlockingReturn(31, ``) + bt.assertBlockingReturn(44, ``) + bt.assertBlockingReturn(52, ``) + bt.assertBlockingReturn(66, ``) + bt.assertBlockingReturn(73, ``) + bt.assertBlockingReturn(77, ``) } func TestBlocking_Defers_WithReturnsInLoopsInLoops(t *testing.T) { @@ -652,19 +652,19 @@ func TestBlocking_Defers_WithReturnsInLoopsInLoops(t *testing.T) { bt.assertFuncInstCount(4) bt.assertBlocking(`blocking`) bt.assertBlocking(`forLoopTheLoop`) - bt.assertNotBlockingReturn(9) - bt.assertBlockingReturn(13) - bt.assertBlockingReturn(17) - bt.assertBlockingReturn(21) - bt.assertBlockingReturn(25) - bt.assertBlockingReturn(28) + bt.assertNotBlockingReturn(9, ``) + bt.assertBlockingReturn(13, ``) + bt.assertBlockingReturn(17, ``) + bt.assertBlockingReturn(21, ``) + bt.assertBlockingReturn(25, ``) + bt.assertBlockingReturn(28, ``) bt.assertBlocking(`rangeLoopTheLoop`) - bt.assertBlockingReturn(36) - bt.assertBlockingReturn(41) + bt.assertBlockingReturn(36, ``) + bt.assertBlockingReturn(41, ``) bt.assertBlocking(`noopThenLoop`) - bt.assertNotBlockingReturn(48) - bt.assertBlockingReturn(54) - bt.assertBlockingReturn(58) + bt.assertNotBlockingReturn(48, ``) + bt.assertBlockingReturn(54, ``) + bt.assertBlockingReturn(58, ``) } func TestBlocking_Returns_WithoutDefers(t *testing.T) { @@ -693,19 +693,67 @@ func TestBlocking_Returns_WithoutDefers(t *testing.T) { return true // line 22 }`) bt.assertBlocking(`blocking`) - bt.assertBlockingReturn(4) + bt.assertBlockingReturn(4, ``) bt.assertBlocking(`blockingBeforeReturn`) - bt.assertNotBlockingReturn(9) + bt.assertNotBlockingReturn(9, ``) bt.assertBlocking(`indirectlyBlocking`) - bt.assertBlockingReturn(13) + bt.assertBlockingReturn(13, ``) bt.assertBlocking(`indirectlyBlockingBeforeReturn`) - bt.assertNotBlockingReturn(18) + bt.assertNotBlockingReturn(18, ``) bt.assertNotBlocking(`notBlocking`) - bt.assertNotBlockingReturn(22) + bt.assertNotBlockingReturn(22, ``) +} + +func TestBlocking_Defers_WithReturnsInInstances(t *testing.T) { + // This is an example of a deferred function literal inside of + // an instance of a generic function affecting the return + // differently based on the type arguments of the instance. + bt := newBlockingTest(t, + `package test + + type BazBlocker struct { + c chan bool + } + func (bb BazBlocker) Baz() { + println(<-bb.c) + } + + type BazNotBlocker struct {} + func (bnb BazNotBlocker) Baz() { + println("hi") + } + + type Foo interface { Baz() } + func FooBaz[T Foo]() bool { + defer func() { // line 17 + var foo T + foo.Baz() + }() + return true // line 21 + } + + func main() { + FooBaz[BazBlocker]() + FooBaz[BazNotBlocker]() + }`) + + bt.assertFuncInstCount(5) + bt.assertBlocking(`BazBlocker.Baz`) + bt.assertNotBlocking(`BazNotBlocker.Baz`) + bt.assertBlockingInst(`pkg/test.FooBaz`) + bt.assertNotBlockingInst(`pkg/test.FooBaz`) + bt.assertBlocking(`main`) + + bt.assertFuncLitCount(2) + bt.assertBlockingLit(17, `pkg/test.BazBlocker`) + bt.assertNotBlockingLit(17, `pkg/test.BazNotBlocker`) + + bt.assertBlockingReturn(21, `pkg/test.BazBlocker`) + bt.assertNotBlockingReturn(21, `pkg/test.BazNotBlocker`) } func TestBlocking_Defers_WithReturnsAndOtherPackages(t *testing.T) { @@ -737,10 +785,10 @@ func TestBlocking_Defers_WithReturnsAndOtherPackages(t *testing.T) { bt := newBlockingTestWithOtherPackage(t, testSrc, otherSrc) bt.assertBlocking(`deferOtherBlocking`) - bt.assertBlockingReturn(7) + bt.assertBlockingReturn(7, ``) bt.assertNotBlocking(`deferOtherNotBlocking`) - bt.assertNotBlockingReturn(12) + bt.assertNotBlockingReturn(12, ``) } func TestBlocking_FunctionLiteral(t *testing.T) { @@ -1652,15 +1700,23 @@ func (bt *blockingTest) assertFuncInstCount(expCount int) { } func (bt *blockingTest) assertFuncLitCount(expCount int) { - if got := len(bt.pkgInfo.funcLitInfos); got != expCount { + got := 0 + for _, fis := range bt.pkgInfo.funcLitInfos { + got += len(fis) + } + if got != expCount { bt.f.T.Errorf(`Got %d function literal infos but expected %d.`, got, expCount) - pos := make([]string, 0, len(bt.pkgInfo.funcLitInfos)) - for fl := range bt.pkgInfo.funcLitInfos { - pos = append(pos, bt.f.FileSet.Position(fl.Pos()).String()) + + lits := make([]string, 0, len(bt.pkgInfo.funcLitInfos)) + for fl, fis := range bt.pkgInfo.funcLitInfos { + pos := bt.f.FileSet.Position(fl.Pos()).String() + for _, fi := range fis { + lits = append(lits, pos+`<`+fi.typeArgs.String()+`>`) + } } - sort.Strings(pos) - for i := range pos { - bt.f.T.Logf(` %d. %q`, i+1, pos) + sort.Strings(lits) + for i := range lits { + bt.f.T.Logf(` %d. %q`, i+1, lits[i]) } } } @@ -1716,13 +1772,13 @@ func (bt *blockingTest) isTypesFuncBlocking(funcName string) bool { func (bt *blockingTest) assertBlockingLit(lineNo int, typeArgsStr string) { if !bt.isFuncLitBlocking(lineNo, typeArgsStr) { - bt.f.T.Errorf(`Got FuncLit at line %d with type args %q as not blocking but expected it to be blocking.`, lineNo, typeArgsStr) + bt.f.T.Errorf(`Got FuncLit at line %d with type args %q as not blocking but expected it to be blocking.`, lineNo, typeArgsStr) } } func (bt *blockingTest) assertNotBlockingLit(lineNo int, typeArgsStr string) { if bt.isFuncLitBlocking(lineNo, typeArgsStr) { - bt.f.T.Errorf(`Got FuncLit at line %d with type args %q as blocking but expected it to be not blocking.`, lineNo, typeArgsStr) + bt.f.T.Errorf(`Got FuncLit at line %d with type args %q as blocking but expected it to be not blocking.`, lineNo, typeArgsStr) } } @@ -1745,7 +1801,7 @@ func (bt *blockingTest) isFuncLitBlocking(lineNo int, typeArgsStr string) bool { bt.f.T.Logf("FuncList instances:") for i, fi := range fis { - bt.f.T.Logf("\t%d. %q\n", i, fi.typeArgs.String()) + bt.f.T.Logf("\t%d. %q\n", i+1, fi.typeArgs.String()) } bt.f.T.Fatalf(`No FuncInfo found for FuncLit at line %d with type args %q.`, lineNo, typeArgsStr) return false @@ -1772,34 +1828,52 @@ func (bt *blockingTest) isFuncInstBlocking(instanceStr string) bool { } bt.f.T.Logf(`Function instances found in package info:`) for i, inst := range instances { - bt.f.T.Logf(` %d. %s`, i+1, inst.String()) + bt.f.T.Logf("\t%d. %s", i+1, inst.String()) } bt.f.T.Fatalf(`No function instance found for %q in package info.`, instanceStr) return false } -func (bt *blockingTest) assertBlockingReturn(lineNo int) { - if !bt.isReturnBlocking(lineNo) { - bt.f.T.Errorf(`Got return at line %d as not blocking but expected it to be blocking.`, lineNo) +func (bt *blockingTest) assertBlockingReturn(lineNo int, typeArgsStr string) { + if !bt.isReturnBlocking(lineNo, typeArgsStr) { + bt.f.T.Errorf(`Got return at line %d (%q) as not blocking but expected it to be blocking.`, lineNo, typeArgsStr) } } -func (bt *blockingTest) assertNotBlockingReturn(lineNo int) { - if bt.isReturnBlocking(lineNo) { - bt.f.T.Errorf(`Got return at line %d as blocking but expected it to be not blocking.`, lineNo) +func (bt *blockingTest) assertNotBlockingReturn(lineNo int, typeArgsStr string) { + if bt.isReturnBlocking(lineNo, typeArgsStr) { + bt.f.T.Errorf(`Got return at line %d (%q) as blocking but expected it to be not blocking.`, lineNo, typeArgsStr) } } -func (bt *blockingTest) isReturnBlocking(lineNo int) bool { +func (bt *blockingTest) isReturnBlocking(lineNo int, typeArgsStr string) bool { ret := srctesting.GetNodeAtLineNo[*ast.ReturnStmt](bt.file, bt.f.FileSet, lineNo) if ret == nil { bt.f.T.Fatalf(`ReturnStmt on line %d not found in the AST.`, lineNo) } + + foundInfo := []*FuncInfo{} for _, info := range bt.pkgInfo.allInfos { - if blocking, found := info.Blocking[ret]; found { - return blocking + for _, rs := range info.returnStmts { + if rs.analyzeStack[len(rs.analyzeStack)-1] == ret { + if info.typeArgs.String() == typeArgsStr { + // Found info that matches the type args and + // has the return statement so return the blocking value. + return info.Blocking[ret] + } + + // Wrong instance, record for error message in the case + // that the correct one instance is not found. + foundInfo = append(foundInfo, info) + break + } } } - // If not found in any info.Blocking, then it is not blocking. + + bt.f.T.Logf("FuncInfo instances with ReturnStmt at line %d:", lineNo) + for i, info := range foundInfo { + bt.f.T.Logf("\t%d. %q\n", i+1, info.typeArgs.String()) + } + bt.f.T.Fatalf(`No FuncInfo found for ReturnStmt at line %d with type args %q.`, lineNo, typeArgsStr) return false } diff --git a/compiler/typesutil/typelist.go b/compiler/typesutil/typelist.go index 8072b6f44..768677365 100644 --- a/compiler/typesutil/typelist.go +++ b/compiler/typesutil/typelist.go @@ -19,6 +19,7 @@ func (tl TypeList) String() string { return buf.String() } +// Equal returns true if both lists of type arguments are identical. func (tl TypeList) Equal(other TypeList) bool { if len(tl) != len(other) { return false From 96b85d90ab94d18d23ba6766435530def7900699 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Tue, 3 Dec 2024 16:00:02 -0700 Subject: [PATCH 29/58] Serialization updates --- build/build.go | 56 +++++++++------ build/cache/cache.go | 19 +++-- build/cache/cache_test.go | 53 ++++++++++++-- compiler/compiler.go | 127 ++++++++++++++++++++++++---------- compiler/compiler_test.go | 90 +++++++++++++++++++++++- compiler/decls.go | 9 ++- compiler/internal/dce/info.go | 38 ++++++++++ compiler/package.go | 18 +---- 8 files changed, 320 insertions(+), 90 deletions(-) diff --git a/build/build.go b/build/build.go index 62b360c61..2ec712fc7 100644 --- a/build/build.go +++ b/build/build.go @@ -21,6 +21,7 @@ import ( "sort" "strconv" "strings" + "sync" "time" "github.com/fsnotify/fsnotify" @@ -938,23 +939,40 @@ func (s *Session) buildImportPathWithSrcDir(path string, srcDir string) (*Packag return pkg, archive, nil } +// getExeModTime will determine the mod time of the GopherJS binary +// the first time this is called and cache the result for subsequent calls. +var getExeModTime = func() func() time.Time { + var ( + once sync.Once + result time.Time + ) + getTime := func() { + gopherjsBinary, err := os.Executable() + if err == nil { + var fileInfo os.FileInfo + fileInfo, err = os.Stat(gopherjsBinary) + if err == nil { + result = fileInfo.ModTime() + return + } + } + os.Stderr.WriteString("Could not get GopherJS binary's modification timestamp. Please report issue.\n") + result = time.Now() + } + return func() time.Time { + once.Do(getTime) + return result + } +}() + // BuildPackage compiles an already loaded package. func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) { if archive, ok := s.UpToDateArchives[pkg.ImportPath]; ok { return archive, nil } - var fileInfo os.FileInfo - gopherjsBinary, err := os.Executable() - if err == nil { - fileInfo, err = os.Stat(gopherjsBinary) - if err == nil && fileInfo.ModTime().After(pkg.SrcModTime) { - pkg.SrcModTime = fileInfo.ModTime() - } - } - if err != nil { - os.Stderr.WriteString("Could not get GopherJS binary's modification timestamp. Please report issue.\n") - pkg.SrcModTime = time.Now() + if exeModTime := getExeModTime(); exeModTime.After(pkg.SrcModTime) { + pkg.SrcModTime = exeModTime } for _, importedPkgPath := range pkg.Imports { @@ -966,22 +984,18 @@ func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) { return nil, err } - impModTime := importedPkg.SrcModTime - if impModTime.After(pkg.SrcModTime) { + if impModTime := importedPkg.SrcModTime; impModTime.After(pkg.SrcModTime) { pkg.SrcModTime = impModTime } } - if pkg.FileModTime().After(pkg.SrcModTime) { - pkg.SrcModTime = pkg.FileModTime() + if fileModTime := pkg.FileModTime(); fileModTime.After(pkg.SrcModTime) { + pkg.SrcModTime = fileModTime } if !s.options.NoCache { - archive := s.buildCache.LoadArchive(pkg.ImportPath) - if archive != nil && !pkg.SrcModTime.After(archive.BuildTime) { - if err := archive.RegisterTypes(s.Types); err != nil { - panic(fmt.Errorf("failed to load type information from %v: %w", archive, err)) - } + archive := s.buildCache.LoadArchive(pkg.ImportPath, pkg.SrcModTime, s.Types) + if archive != nil { s.UpToDateArchives[pkg.ImportPath] = archive // Existing archive is up to date, no need to build it from scratch. return archive, nil @@ -1021,7 +1035,7 @@ func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) { fmt.Println(pkg.ImportPath) } - s.buildCache.StoreArchive(archive) + s.buildCache.StoreArchive(archive, time.Now()) s.UpToDateArchives[pkg.ImportPath] = archive return archive, nil diff --git a/build/cache/cache.go b/build/cache/cache.go index 2c4e5703a..90473c7fe 100644 --- a/build/cache/cache.go +++ b/build/cache/cache.go @@ -6,9 +6,11 @@ import ( "crypto/sha256" "fmt" "go/build" + "go/types" "os" "path" "path/filepath" + "time" "github.com/gopherjs/gopherjs/compiler" log "github.com/sirupsen/logrus" @@ -90,7 +92,7 @@ func (bc BuildCache) String() string { // StoreArchive compiled archive in the cache. Any error inside this method // will cause the cache not to be persisted. -func (bc *BuildCache) StoreArchive(a *compiler.Archive) { +func (bc *BuildCache) StoreArchive(a *compiler.Archive, buildTime time.Time) { if bc == nil { return // Caching is disabled. } @@ -106,7 +108,7 @@ func (bc *BuildCache) StoreArchive(a *compiler.Archive) { return } defer f.Close() - if err := compiler.WriteArchive(a, f); err != nil { + if err := compiler.WriteArchive(a, buildTime, f); err != nil { log.Warningf("Failed to write build cache archive %q: %v", a, err) // Make sure we don't leave a half-written archive behind. os.Remove(f.Name()) @@ -125,7 +127,10 @@ func (bc *BuildCache) StoreArchive(a *compiler.Archive) { // // The returned archive would have been built with the same configuration as // the build cache was. -func (bc *BuildCache) LoadArchive(importPath string) *compiler.Archive { +// +// The imports map is used to resolve package dependencies and may modify the +// map to include the package from the read archive. See [gcexportdata.Read]. +func (bc *BuildCache) LoadArchive(importPath string, srcModTime time.Time, imports map[string]*types.Package) *compiler.Archive { if bc == nil { return nil // Caching is disabled. } @@ -140,12 +145,16 @@ func (bc *BuildCache) LoadArchive(importPath string) *compiler.Archive { return nil // Cache miss. } defer f.Close() - a, err := compiler.ReadArchive(importPath, f) + a, buildTime, err := compiler.ReadArchive(importPath, f, srcModTime, imports) if err != nil { log.Warningf("Failed to read cached package archive for %q: %v", importPath, err) return nil // Invalid/corrupted archive, cache miss. } - log.Infof("Found cached package archive for %q, built at %v.", importPath, a.BuildTime) + if a == nil { + log.Infof("Found out-of-date package archive for %q, built at %v.", importPath, buildTime) + return nil // Archive is out-of-date, cache miss. + } + log.Infof("Found cached package archive for %q, built at %v.", importPath, buildTime) return a } diff --git a/build/cache/cache_test.go b/build/cache/cache_test.go index fd89ec187..0a0541f64 100644 --- a/build/cache/cache_test.go +++ b/build/cache/cache_test.go @@ -1,7 +1,9 @@ package cache import ( + "go/types" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/gopherjs/gopherjs/compiler" @@ -15,21 +17,24 @@ func TestStore(t *testing.T) { Imports: []string{"fake/dep"}, } + srcModTime := newTime(0.0) + buildTime := newTime(5.0) + imports := map[string]*types.Package{} bc := BuildCache{} - if got := bc.LoadArchive(want.ImportPath); got != nil { + if got := bc.LoadArchive(want.ImportPath, srcModTime, imports); got != nil { t.Errorf("Got: %s was found in the cache. Want: empty cache.", got.ImportPath) } - bc.StoreArchive(want) - got := bc.LoadArchive(want.ImportPath) + bc.StoreArchive(want, buildTime) + got := bc.LoadArchive(want.ImportPath, srcModTime, imports) if got == nil { - t.Errorf("Got: %s wan not found in the cache. Want: archive is can be loaded after store.", want.ImportPath) + t.Errorf("Got: %s was not found in the cache. Want: archive is can be loaded after store.", want.ImportPath) } if diff := cmp.Diff(want, got); diff != "" { t.Errorf("Loaded archive is different from stored (-want,+got):\n%s", diff) } // Make sure the package names are a part of the cache key. - if got := bc.LoadArchive("fake/other"); got != nil { + if got := bc.LoadArchive("fake/other", srcModTime, imports); got != nil { t.Errorf("Got: fake/other was found in cache: %#v. Want: nil for packages that weren't cached.", got) } } @@ -59,20 +64,54 @@ func TestInvalidation(t *testing.T) { }, } + srcModTime := newTime(0.0) + buildTime := newTime(5.0) + imports := map[string]*types.Package{} for _, test := range tests { a := &compiler.Archive{ImportPath: "package/fake"} - test.cache1.StoreArchive(a) + test.cache1.StoreArchive(a, buildTime) - if got := test.cache2.LoadArchive(a.ImportPath); got != nil { + if got := test.cache2.LoadArchive(a.ImportPath, srcModTime, imports); got != nil { t.Logf("-cache1,+cache2:\n%s", cmp.Diff(test.cache1, test.cache2)) t.Errorf("Got: %v loaded from cache. Want: build parameter change invalidates cache.", got) } } } +func TestOldArchive(t *testing.T) { + cacheForTest(t) + + want := &compiler.Archive{ + ImportPath: "fake/package", + Imports: []string{"fake/dep"}, + } + + buildTime := newTime(5.0) + imports := map[string]*types.Package{} + bc := BuildCache{} + bc.StoreArchive(want, buildTime) + + oldSrcModTime := newTime(2.0) // older than archive build time, so archive is up-to-date + got := bc.LoadArchive(want.ImportPath, oldSrcModTime, imports) + if got == nil { + t.Errorf("Got: %s was nil. Want: up-to-date archive to be loaded.", want.ImportPath) + } + + newerSrcModTime := newTime(7.0) // newer than archive build time, so archive is stale + got = bc.LoadArchive(want.ImportPath, newerSrcModTime, imports) + if got != nil { + t.Errorf("Got: %s was not nil. Want: stale archive to not be loaded.", want.ImportPath) + } +} + func cacheForTest(t *testing.T) { t.Helper() originalRoot := cacheRoot t.Cleanup(func() { cacheRoot = originalRoot }) cacheRoot = t.TempDir() } + +func newTime(seconds float64) time.Time { + return time.Date(1969, 7, 20, 20, 17, 0, 0, time.UTC). + Add(time.Duration(seconds * float64(time.Second))) +} diff --git a/compiler/compiler.go b/compiler/compiler.go index cffd4c86d..737777887 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -46,45 +46,26 @@ type Archive struct { // A list of full package import paths that the current package imports across // all source files. See go/types.Package.Imports(). Imports []string - // Serialized contents of go/types.Package in a binary format. This information - // is used by the compiler to type-check packages that import this one. See - // gcexportdata.Write(). - // - // TODO(nevkontakte): It would be more convenient to store go/types.Package - // itself and only serialize it when writing the archive onto disk. - ExportData []byte + // The package information is used by the compiler to type-check packages + // that import this one. See [gcexportdata.Write]. + Package *types.Package // Compiled package-level symbols. Declarations []*Decl // Concatenated contents of all raw .inc.js of the package. IncJSCode []byte - // JSON-serialized contents of go/token.FileSet. This is used to obtain source - // code locations for various symbols (e.g. for sourcemap generation). See - // token.FileSet.Write(). - // - // TODO(nevkontakte): This is also more convenient to store as the original - // object and only serialize before writing onto disk. - FileSet []byte + // The file set containing the source code locations for various symbols + // (e.g. for sourcemap generation). See [token.FileSet.Write]. + FileSet *token.FileSet // Whether or not the package was compiled with minification enabled. Minified bool // A list of go:linkname directives encountered in the package. GoLinknames []GoLinkname - // Time when this archive was built. - BuildTime time.Time } func (a Archive) String() string { return fmt.Sprintf("compiler.Archive{%s}", a.ImportPath) } -// RegisterTypes adds package type information from the archive into the provided map. -func (a *Archive) RegisterTypes(packages map[string]*types.Package) error { - var err error - // TODO(nevkontakte): Should this be shared throughout the build? - fset := token.NewFileSet() - packages[a.ImportPath], err = gcexportdata.Read(bytes.NewReader(a.ExportData), fset, packages, a.ImportPath) - return err -} - type Dependency struct { Pkg string Type string @@ -185,10 +166,7 @@ func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) err func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameSet, minify bool, w *SourceMapFilter) error { if w.MappingCallback != nil && pkg.FileSet != nil { - w.fileSet = token.NewFileSet() - if err := w.fileSet.Read(json.NewDecoder(bytes.NewReader(pkg.FileSet)).Decode); err != nil { - panic(err) - } + w.fileSet = pkg.FileSet } if _, err := w.Write(pkg.IncJSCode); err != nil { return err @@ -277,19 +255,98 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameS return nil } +type serializableArchive struct { + ImportPath string + Name string + Imports []string + ExportData []byte + Declarations []*Decl + IncJSCode []byte + FileSet []byte + Minified bool + GoLinknames []GoLinkname + BuildTime time.Time +} + // ReadArchive reads serialized compiled archive of the importPath package. -func ReadArchive(path string, r io.Reader) (*Archive, error) { +// +// The given srcModTime is used to determine if the archive is out-of-date. +// If the archive is out-of-date, the returned archive is nil. +// If there was not an error, the returned time is when the archive was built. +// +// The imports map is used to resolve package dependencies and may modify the +// map to include the package from the read archive. See [gcexportdata.Read]. +func ReadArchive(importPath string, r io.Reader, srcModTime time.Time, imports map[string]*types.Package) (*Archive, time.Time, error) { + var sa serializableArchive + if err := gob.NewDecoder(r).Decode(&sa); err != nil { + return nil, time.Time{}, err + } + + if srcModTime.After(sa.BuildTime) { + // Archive is out-of-date. + return nil, sa.BuildTime, nil + } + var a Archive - if err := gob.NewDecoder(r).Decode(&a); err != nil { - return nil, err + fset := token.NewFileSet() + if len(sa.ExportData) > 0 { + pkg, err := gcexportdata.Read(bytes.NewReader(sa.ExportData), fset, imports, importPath) + if err != nil { + return nil, sa.BuildTime, err + } + a.Package = pkg + } + + if len(sa.FileSet) > 0 { + a.FileSet = token.NewFileSet() + if err := a.FileSet.Read(json.NewDecoder(bytes.NewReader(sa.FileSet)).Decode); err != nil { + return nil, sa.BuildTime, err + } } - return &a, nil + a.ImportPath = sa.ImportPath + a.Name = sa.Name + a.Imports = sa.Imports + a.Declarations = sa.Declarations + a.IncJSCode = sa.IncJSCode + a.Minified = sa.Minified + a.GoLinknames = sa.GoLinknames + return &a, sa.BuildTime, nil } // WriteArchive writes compiled package archive on disk for later reuse. -func WriteArchive(a *Archive, w io.Writer) error { - return gob.NewEncoder(w).Encode(a) +// +// The passed in buildTime is used to determine if the archive is out-of-date. +// It should be set to time.Now() typically but it exposed for testing purposes. +func WriteArchive(a *Archive, buildTime time.Time, w io.Writer) error { + exportData := new(bytes.Buffer) + if a.Package != nil { + if err := gcexportdata.Write(exportData, nil, a.Package); err != nil { + return fmt.Errorf("failed to write export data: %w", err) + } + } + + encodedFileSet := new(bytes.Buffer) + if a.FileSet != nil { + if err := a.FileSet.Write(json.NewEncoder(encodedFileSet).Encode); err != nil { + return err + } + } + + sa := serializableArchive{ + ImportPath: a.ImportPath, + Name: a.Name, + Imports: a.Imports, + ExportData: exportData.Bytes(), + Declarations: a.Declarations, + IncJSCode: a.IncJSCode, + FileSet: encodedFileSet.Bytes(), + Minified: a.Minified, + GoLinknames: a.GoLinknames, + BuildTime: buildTime, + } + + return gob.NewEncoder(w).Encode(sa) } type SourceMapFilter struct { diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 65178e986..22a8f8ff8 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -6,6 +6,7 @@ import ( "regexp" "sort" "testing" + "time" "github.com/google/go-cmp/cmp" "golang.org/x/tools/go/packages" @@ -109,7 +110,7 @@ func TestDeclSelection_KeepUnusedUnexportedMethodForInterface(t *testing.T) { println("foo2") } - type IFoo interface { + type IFoo interface { Bar() baz() } @@ -405,6 +406,33 @@ func TestLengthParenthesizingIssue841(t *testing.T) { } } +func TestArchiveSelectionAfterSerialization(t *testing.T) { + src := ` + package main + type Foo interface{ int | string } + + type Bar[T Foo] struct{ v T } + func (b Bar[T]) Baz() { println(b.v) } + + var ghost = Bar[int]{v: 7} // unused + + func main() { + println("do nothing") + }` + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + rootPath := root.PkgPath + origArchives := compileProject(t, root, false) + readArchives := reloadCompiledProject(t, origArchives, rootPath) + + origJS := renderPackage(t, origArchives[rootPath], false) + readJS := renderPackage(t, readArchives[rootPath], false) + + if diff := cmp.Diff(string(origJS), string(readJS)); diff != "" { + t.Errorf("the reloaded files produce different JS:\n%s", diff) + } +} + func compareOrder(t *testing.T, sourceFiles []srctesting.Source, minify bool) { t.Helper() outputNormal := compile(t, sourceFiles, minify) @@ -481,12 +509,68 @@ func compileProject(t *testing.T, root *packages.Package, minify bool) map[strin return archiveCache } +// newTime creates an arbitrary time.Time offset by the given number of seconds. +// This is useful for quickly creating times that are before or after another. +func newTime(seconds float64) time.Time { + return time.Date(1969, 7, 20, 20, 17, 0, 0, time.UTC). + Add(time.Duration(seconds * float64(time.Second))) +} + +// reloadCompiledProject persists the given archives into memory then reloads +// them from memory to simulate a cache reload of a precompiled project. +func reloadCompiledProject(t *testing.T, archives map[string]*Archive, rootPkgPath string) map[string]*Archive { + t.Helper() + + buildTime := newTime(5.0) + serialized := map[string][]byte{} + for path, a := range archives { + buf := &bytes.Buffer{} + if err := WriteArchive(a, buildTime, buf); err != nil { + t.Fatalf(`failed to write archive for %s: %v`, path, err) + } + serialized[path] = buf.Bytes() + } + + srcModTime := newTime(0.0) + reloadCache := map[string]*Archive{} + var importContext *ImportContext + importContext = &ImportContext{ + Packages: map[string]*types.Package{}, + Import: func(path string) (*Archive, error) { + // find in local cache + if a, ok := reloadCache[path]; ok { + return a, nil + } + + // deserialize archive + buf, ok := serialized[path] + if !ok { + t.Fatalf(`archive not found for %s`, path) + } + a, _, err := ReadArchive(path, bytes.NewReader(buf), srcModTime, importContext.Packages) + if err != nil { + t.Fatalf(`failed to read archive for %s: %v`, path, err) + } + reloadCache[path] = a + return a, nil + }, + } + + _, err := importContext.Import(rootPkgPath) + if err != nil { + t.Fatal(`failed to reload archives:`, err) + } + return reloadCache +} + func renderPackage(t *testing.T, archive *Archive, minify bool) []byte { t.Helper() - selection := make(map[*Decl]struct{}) + + sel := &dce.Selector[*Decl]{} for _, d := range archive.Declarations { - selection[d] = struct{}{} + sel.Include(d, false) } + selection := sel.AliveDecls() buf := &bytes.Buffer{} diff --git a/compiler/decls.go b/compiler/decls.go index 5419c4c7d..9f6518875 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -23,6 +23,9 @@ import ( // // 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. +// +// When adding new fields to this struct, make sure the field is exported +// so that it Gob serializes correctly for the archive cache. type Decl struct { // The package- or receiver-type-qualified name of function or method obj. // See go/types.Func.FullName(). @@ -52,8 +55,8 @@ type Decl struct { // 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 - // dce stores the information for dead-code elimination. - dce dce.Info + // DCEInfo stores the information for dead-code elimination. + DCEInfo dce.Info // 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 @@ -73,7 +76,7 @@ func (d Decl) minify() Decl { // Dce gets the information for dead-code elimination. func (d *Decl) Dce() *dce.Info { - return &d.dce + return &d.DCEInfo } // topLevelObjects extracts package-level variables, functions and named types diff --git a/compiler/internal/dce/info.go b/compiler/internal/dce/info.go index 07d818855..6a45e9ef3 100644 --- a/compiler/internal/dce/info.go +++ b/compiler/internal/dce/info.go @@ -1,6 +1,8 @@ package dce import ( + "bytes" + "encoding/gob" "fmt" "go/types" "sort" @@ -117,3 +119,39 @@ func (id *Info) getDeps() []string { sort.Strings(deps) return deps } + +type serializableInfo struct { + Alive bool + ObjectFilter string + MethodFilter string + Deps []string +} + +func (id *Info) GobEncode() ([]byte, error) { + si := serializableInfo{ + Alive: id.alive, + ObjectFilter: id.objectFilter, + MethodFilter: id.methodFilter, + Deps: id.getDeps(), + } + + buf := &bytes.Buffer{} + err := gob.NewEncoder(buf).Encode(si) + return buf.Bytes(), err +} + +func (id *Info) GobDecode(data []byte) error { + var si serializableInfo + if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&si); err != nil { + return err + } + + id.alive = si.Alive + id.objectFilter = si.ObjectFilter + id.methodFilter = si.MethodFilter + id.deps = make(map[string]struct{}, len(si.Deps)) + for _, dep := range si.Deps { + id.deps[dep] = struct{}{} + } + return nil +} diff --git a/compiler/package.go b/compiler/package.go index 4cd800607..2f6af9c6b 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -1,21 +1,17 @@ package compiler import ( - "bytes" - "encoding/json" "fmt" "go/ast" "go/token" "go/types" "strings" - "time" "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" "github.com/gopherjs/gopherjs/internal/experiments" - "golang.org/x/tools/go/gcexportdata" "golang.org/x/tools/go/types/typeutil" ) @@ -285,25 +281,15 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor return nil, rootCtx.pkgCtx.errList } - exportData := new(bytes.Buffer) - if err := gcexportdata.Write(exportData, nil, typesPkg); err != nil { - return nil, fmt.Errorf("failed to write export data: %w", err) - } - encodedFileSet := new(bytes.Buffer) - if err := srcs.FileSet.Write(json.NewEncoder(encodedFileSet).Encode); err != nil { - return nil, err - } - return &Archive{ ImportPath: srcs.ImportPath, Name: typesPkg.Name(), Imports: importedPaths, - ExportData: exportData.Bytes(), + Package: typesPkg, Declarations: allDecls, - FileSet: encodedFileSet.Bytes(), + FileSet: srcs.FileSet, Minified: minify, GoLinknames: goLinknames, - BuildTime: time.Now(), }, nil } From 1fa608f5b1f374773e22d94c5d61d349b78137bd Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Mon, 9 Dec 2024 10:51:31 -0700 Subject: [PATCH 30/58] Added Helper calls to info_test --- compiler/internal/analysis/info_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go index 3556324cf..957e346d9 100644 --- a/compiler/internal/analysis/info_test.go +++ b/compiler/internal/analysis/info_test.go @@ -1691,6 +1691,7 @@ func newBlockingTestWithOtherPackage(t *testing.T, testSrc string, otherSrc stri } func (bt *blockingTest) assertFuncInstCount(expCount int) { + bt.f.T.Helper() if got := bt.pkgInfo.funcInstInfos.Len(); got != expCount { bt.f.T.Errorf(`Got %d function instance infos but expected %d.`, got, expCount) for i, inst := range bt.pkgInfo.funcInstInfos.Keys() { @@ -1700,6 +1701,7 @@ func (bt *blockingTest) assertFuncInstCount(expCount int) { } func (bt *blockingTest) assertFuncLitCount(expCount int) { + bt.f.T.Helper() got := 0 for _, fis := range bt.pkgInfo.funcLitInfos { got += len(fis) @@ -1722,12 +1724,14 @@ func (bt *blockingTest) assertFuncLitCount(expCount int) { } func (bt *blockingTest) assertBlocking(funcName string) { + bt.f.T.Helper() if !bt.isTypesFuncBlocking(funcName) { bt.f.T.Errorf(`Got %q as not blocking but expected it to be blocking.`, funcName) } } func (bt *blockingTest) assertNotBlocking(funcName string) { + bt.f.T.Helper() if bt.isTypesFuncBlocking(funcName) { bt.f.T.Errorf(`Got %q as blocking but expected it to be not blocking.`, funcName) } @@ -1748,6 +1752,7 @@ func getFuncDeclName(fd *ast.FuncDecl) string { } func (bt *blockingTest) isTypesFuncBlocking(funcName string) bool { + bt.f.T.Helper() var decl *ast.FuncDecl ast.Inspect(bt.file, func(n ast.Node) bool { if f, ok := n.(*ast.FuncDecl); ok && getFuncDeclName(f) == funcName { @@ -1771,18 +1776,21 @@ func (bt *blockingTest) isTypesFuncBlocking(funcName string) bool { } func (bt *blockingTest) assertBlockingLit(lineNo int, typeArgsStr string) { + bt.f.T.Helper() if !bt.isFuncLitBlocking(lineNo, typeArgsStr) { bt.f.T.Errorf(`Got FuncLit at line %d with type args %q as not blocking but expected it to be blocking.`, lineNo, typeArgsStr) } } func (bt *blockingTest) assertNotBlockingLit(lineNo int, typeArgsStr string) { + bt.f.T.Helper() if bt.isFuncLitBlocking(lineNo, typeArgsStr) { bt.f.T.Errorf(`Got FuncLit at line %d with type args %q as blocking but expected it to be not blocking.`, lineNo, typeArgsStr) } } func (bt *blockingTest) isFuncLitBlocking(lineNo int, typeArgsStr string) bool { + bt.f.T.Helper() fnLit := srctesting.GetNodeAtLineNo[*ast.FuncLit](bt.file, bt.f.FileSet, lineNo) if fnLit == nil { bt.f.T.Fatalf(`FuncLit on line %d not found in the AST.`, lineNo) @@ -1808,18 +1816,21 @@ func (bt *blockingTest) isFuncLitBlocking(lineNo int, typeArgsStr string) bool { } func (bt *blockingTest) assertBlockingInst(instanceStr string) { + bt.f.T.Helper() if !bt.isFuncInstBlocking(instanceStr) { bt.f.T.Errorf(`Got function instance of %q as not blocking but expected it to be blocking.`, instanceStr) } } func (bt *blockingTest) assertNotBlockingInst(instanceStr string) { + bt.f.T.Helper() if bt.isFuncInstBlocking(instanceStr) { bt.f.T.Errorf(`Got function instance of %q as blocking but expected it to be not blocking.`, instanceStr) } } func (bt *blockingTest) isFuncInstBlocking(instanceStr string) bool { + bt.f.T.Helper() instances := bt.pkgInfo.funcInstInfos.Keys() for _, inst := range instances { if inst.String() == instanceStr { @@ -1835,18 +1846,21 @@ func (bt *blockingTest) isFuncInstBlocking(instanceStr string) bool { } func (bt *blockingTest) assertBlockingReturn(lineNo int, typeArgsStr string) { + bt.f.T.Helper() if !bt.isReturnBlocking(lineNo, typeArgsStr) { bt.f.T.Errorf(`Got return at line %d (%q) as not blocking but expected it to be blocking.`, lineNo, typeArgsStr) } } func (bt *blockingTest) assertNotBlockingReturn(lineNo int, typeArgsStr string) { + bt.f.T.Helper() if bt.isReturnBlocking(lineNo, typeArgsStr) { bt.f.T.Errorf(`Got return at line %d (%q) as blocking but expected it to be not blocking.`, lineNo, typeArgsStr) } } func (bt *blockingTest) isReturnBlocking(lineNo int, typeArgsStr string) bool { + bt.f.T.Helper() ret := srctesting.GetNodeAtLineNo[*ast.ReturnStmt](bt.file, bt.f.FileSet, lineNo) if ret == nil { bt.f.T.Fatalf(`ReturnStmt on line %d not found in the AST.`, lineNo) From 11bf21deb86e53015e1bafd1dc55080d42d29ab7 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 12 Dec 2024 10:56:34 -0700 Subject: [PATCH 31/58] Using srcModTime instead of time.Now --- build/build.go | 2 +- build/cache/cache.go | 3 +++ compiler/compiler.go | 2 +- compiler/compiler_test.go | 20 ++++++++++---------- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/build/build.go b/build/build.go index 2ec712fc7..97f34cdaf 100644 --- a/build/build.go +++ b/build/build.go @@ -1035,7 +1035,7 @@ func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) { fmt.Println(pkg.ImportPath) } - s.buildCache.StoreArchive(archive, time.Now()) + s.buildCache.StoreArchive(archive, pkg.SrcModTime) s.UpToDateArchives[pkg.ImportPath] = archive return archive, nil diff --git a/build/cache/cache.go b/build/cache/cache.go index 90473c7fe..fc0949d67 100644 --- a/build/cache/cache.go +++ b/build/cache/cache.go @@ -92,6 +92,9 @@ func (bc BuildCache) String() string { // StoreArchive compiled archive in the cache. Any error inside this method // will cause the cache not to be persisted. +// +// The passed in buildTime is used to determine if the archive is out-of-date when reloaded. +// Typically it should be set to the srcModTime or time.Now(). func (bc *BuildCache) StoreArchive(a *compiler.Archive, buildTime time.Time) { if bc == nil { return // Caching is disabled. diff --git a/compiler/compiler.go b/compiler/compiler.go index 737777887..96ec390d8 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -317,7 +317,7 @@ func ReadArchive(importPath string, r io.Reader, srcModTime time.Time, imports m // WriteArchive writes compiled package archive on disk for later reuse. // // The passed in buildTime is used to determine if the archive is out-of-date. -// It should be set to time.Now() typically but it exposed for testing purposes. +// Typically it should be set to the srcModTime or time.Now() but it is exposed for testing purposes. func WriteArchive(a *Archive, buildTime time.Time, w io.Writer) error { exportData := new(bytes.Buffer) if a.Package != nil { diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 22a8f8ff8..16789971b 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -428,7 +428,7 @@ func TestArchiveSelectionAfterSerialization(t *testing.T) { origJS := renderPackage(t, origArchives[rootPath], false) readJS := renderPackage(t, readArchives[rootPath], false) - if diff := cmp.Diff(string(origJS), string(readJS)); diff != "" { + if diff := cmp.Diff(origJS, readJS); diff != "" { t.Errorf("the reloaded files produce different JS:\n%s", diff) } } @@ -444,12 +444,12 @@ func compareOrder(t *testing.T, sourceFiles []srctesting.Source, minify bool) { outputReversed := compile(t, sourceFiles, minify) - if diff := cmp.Diff(string(outputNormal), string(outputReversed)); diff != "" { + if diff := cmp.Diff(outputNormal, outputReversed); diff != "" { t.Errorf("files in different order produce different JS:\n%s", diff) } } -func compile(t *testing.T, sourceFiles []srctesting.Source, minify bool) []byte { +func compile(t *testing.T, sourceFiles []srctesting.Source, minify bool) string { t.Helper() rootPkg := srctesting.ParseSources(t, sourceFiles, nil) archives := compileProject(t, rootPkg, minify) @@ -460,11 +460,7 @@ func compile(t *testing.T, sourceFiles []srctesting.Source, minify bool) []byte t.Fatalf(`root package not found in archives: %s`, path) } - b := renderPackage(t, a, minify) - if len(b) == 0 { - t.Fatal(`compile had no output`) - } - return b + return renderPackage(t, a, minify) } // compileProject compiles the given root package and all packages imported by the root. @@ -563,7 +559,7 @@ func reloadCompiledProject(t *testing.T, archives map[string]*Archive, rootPkgPa return reloadCache } -func renderPackage(t *testing.T, archive *Archive, minify bool) []byte { +func renderPackage(t *testing.T, archive *Archive, minify bool) string { t.Helper() sel := &dce.Selector[*Decl]{} @@ -578,7 +574,11 @@ func renderPackage(t *testing.T, archive *Archive, minify bool) []byte { t.Fatal(err) } - return buf.Bytes() + b := buf.String() + if len(b) == 0 { + t.Fatal(`render package had no output`) + } + return b } type selectionTester struct { From 45e371209ae25c640202e79240606072ba59d210 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 12 Dec 2024 12:03:37 -0700 Subject: [PATCH 32/58] Setting the buildTime back to time.Now --- build/build.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/build.go b/build/build.go index 97f34cdaf..2ec712fc7 100644 --- a/build/build.go +++ b/build/build.go @@ -1035,7 +1035,7 @@ func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) { fmt.Println(pkg.ImportPath) } - s.buildCache.StoreArchive(archive, pkg.SrcModTime) + s.buildCache.StoreArchive(archive, time.Now()) s.UpToDateArchives[pkg.ImportPath] = archive return archive, nil From f0695e070d53a746919b86ab3fd8ea284bc077fe Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Tue, 17 Dec 2024 09:52:05 -0700 Subject: [PATCH 33/58] fixing grammar --- compiler/declNames.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/declNames.go b/compiler/declNames.go index ea4528d9e..4ba59e289 100644 --- a/compiler/declNames.go +++ b/compiler/declNames.go @@ -43,7 +43,7 @@ func mainFuncDeclFullName() string { // funcDeclFullName returns a name for a package-level function // declaration for the given instance of a function. -// The name is unique except unless the function is an `init` function. +// The name is unique unless the function is an `init` function. func funcDeclFullName(inst typeparams.Instance) string { return `func:` + inst.String() } From 75ff10ca9e14a6db1676da8057c72566de4723c2 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Mon, 10 Feb 2025 11:47:29 -0700 Subject: [PATCH 34/58] Skipping tests with expired certificate --- .../natives/src/crypto/tls/handshake_test.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/compiler/natives/src/crypto/tls/handshake_test.go b/compiler/natives/src/crypto/tls/handshake_test.go index c9e19d5ed..b5fe59a57 100644 --- a/compiler/natives/src/crypto/tls/handshake_test.go +++ b/compiler/natives/src/crypto/tls/handshake_test.go @@ -10,7 +10,7 @@ import ( // Same as upstream, except we check for GOARCH=ecmascript instead of wasm. // This override can be removed after https://github.com/golang/go/pull/51827 -// is available in the upstream (likely in Go 1.19). +// is available in the upstream (likely after Go 1.19). func TestServerHandshakeContextCancellation(t *testing.T) { c, s := localPipe(t) ctx, cancel := context.WithCancel(context.Background()) @@ -42,7 +42,7 @@ func TestServerHandshakeContextCancellation(t *testing.T) { // Same as upstream, except we check for GOARCH=ecmascript instead of wasm. // This override can be removed after https://github.com/golang/go/pull/51827 -// is available in the upstream (likely in Go 1.19). +// is available in the upstream (likely after Go 1.19). func TestClientHandshakeContextCancellation(t *testing.T) { c, s := localPipe(t) ctx, cancel := context.WithCancel(context.Background()) @@ -71,3 +71,15 @@ func TestClientHandshakeContextCancellation(t *testing.T) { t.Error("Client connection was not closed when the context was canceled") } } + +func TestVerifyConnection(t *testing.T) { + // This should be rechecked after upgrading to Go 1.20 or later. + // go1.19.13/src/crypto/tls/handshake_test.go:testRSACertificateIssuer has expired. + t.Skip("Skipping test that uses predefined certificate that expired in Jan 1st 2025") +} + +func TestResumptionKeepsOCSPAndSCT(t *testing.T) { + // This should be rechecked after upgrading to Go 1.20 or later. + // go1.19.13/src/crypto/tls/handshake_test.go:testRSACertificateIssuer has expired. + t.Skip("Skipping test that uses predefined certificate that expired in Jan 1st 2025") +} From a594ec1e4a5ba2207696f74134ff04f936392ae7 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Mon, 10 Feb 2025 13:59:31 -0700 Subject: [PATCH 35/58] separating parse and compile --- build/build.go | 255 +++++++++++++++++++++++------ internal/testmain/testmain.go | 20 +-- internal/testmain/testmain_test.go | 11 +- tool.go | 35 +--- 4 files changed, 223 insertions(+), 98 deletions(-) diff --git a/build/build.go b/build/build.go index 2ec712fc7..300e295bd 100644 --- a/build/build.go +++ b/build/build.go @@ -27,12 +27,11 @@ import ( "github.com/fsnotify/fsnotify" "github.com/gopherjs/gopherjs/compiler" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/internal/testmain" log "github.com/sirupsen/logrus" "github.com/neelance/sourcemap" "golang.org/x/tools/go/buildutil" - - "github.com/gopherjs/gopherjs/build/cache" ) // DefaultGOROOT is the default GOROOT value for builds. @@ -744,14 +743,60 @@ func (p *PackageData) InstallPath() string { return p.PkgObj } +// ParsedPackage is the results from building a package before it is compiled. +type ParsedPackage struct { + ImportPath string // import path of package ("" if unknown) + Dir string // directory containing package sources + + // GoFiles is the parsed and augmented Go AST files for the package. + GoFiles []*ast.File + FileSet *token.FileSet + JSFiles []JSFile +} + +// Imports calculates the import paths of the package's dependencies +// based on all the imports in the augmented files. +// +// The given skip paths will not be returned in the results. +// This will not return any `*_test` packages in the results. +func (p *ParsedPackage) Imports(skip ...string) []string { + seen := make(map[string]struct{}) + for _, s := range skip { + seen[s] = struct{}{} + } + imports := []string{} + for _, file := range p.GoFiles { + for _, imp := range file.Imports { + path := strings.Trim(imp.Path.Value, `"`) + if _, ok := seen[path]; !ok { + if !strings.HasSuffix(path, "_test") { + imports = append(imports, path) + } + seen[path] = struct{}{} + } + } + } + sort.Strings(imports) + return imports +} + // Session manages internal state GopherJS requires to perform a build. // // This is the main interface to GopherJS build system. Session lifetime is // roughly equivalent to a single GopherJS tool invocation. type Session struct { - options *Options - xctx XContext - buildCache cache.BuildCache + options *Options + xctx XContext + + // importPaths is a map of the resolved import paths given the srcDir + // and path. This is used to avoid redetermining build packages during + // compilation when we're looking up parsed packages. + importPaths map[string]map[string]string + + // parsePackage is a map of parsed packages that have been built and augmented. + // This is keyed using resolved import paths. This is used to avoid + // rebuilding and augmenting packages that are imported by several packages. + parsedPackages map[string]*ParsedPackage // Binary archives produced during the current session and assumed to be // up to date with input sources and dependencies. In the -w ("watch") mode @@ -767,6 +812,8 @@ func NewSession(options *Options) (*Session, error) { s := &Session{ options: options, + importPaths: make(map[string]map[string]string), + parsedPackages: make(map[string]*ParsedPackage), UpToDateArchives: make(map[string]*compiler.Archive), } s.xctx = NewBuildContext(s.InstallSuffix(), s.options.BuildTags) @@ -777,15 +824,6 @@ func NewSession(options *Options) (*Session, error) { return nil, err } - s.buildCache = cache.BuildCache{ - GOOS: env.GOOS, - GOARCH: env.GOARCH, - GOROOT: env.GOROOT, - GOPATH: env.GOPATH, - BuildTags: append([]string{}, env.BuildTags...), - Minify: options.Minify, - TestedPackage: options.TestedPackage, - } s.Types = make(map[string]*types.Package) if options.Watch { if out, err := exec.Command("ulimit", "-n").Output(); err == nil { @@ -900,7 +938,7 @@ func (s *Session) BuildFiles(filenames []string, pkgObj string, cwd string) erro }) } - archive, err := s.BuildPackage(pkg) + archive, err := s.BuildProject(pkg) if err != nil { return err } @@ -910,19 +948,76 @@ func (s *Session) BuildFiles(filenames []string, pkgObj string, cwd string) erro return s.WriteCommandPackage(archive, pkgObj) } -// BuildImportPath loads and compiles package with the given import path. -// -// Relative paths are interpreted relative to the current working dir. -func (s *Session) BuildImportPath(path string) (*compiler.Archive, error) { - _, archive, err := s.buildImportPathWithSrcDir(path, "") - return archive, err +// BuildProject builds a command project (one with a main method) or +// builds a test project (one with a synthesized test main package). +func (s *Session) BuildProject(pkg *PackageData) (*compiler.Archive, error) { + // ensure that runtime for gopherjs is imported + pkg.Imports = append(pkg.Imports, `runtime`) + + // Build the project to get the parsed packages. + var parsed *ParsedPackage + var err error + if pkg.IsTest { + parsed, err = s.buildTestPackage(pkg) + } else { + parsed, err = s.buildPackages(pkg) + } + if err != nil { + return nil, err + } + + // TODO(grantnelson-wf): At this point we have all the parsed packages we + // need to compile the whole project, including testmain, if needed. + // We can perform analysis on the whole project at this point to propagate + // flatten, blocking, etc. information and check types to get the type info + // with all the instances for all generics in the whole project. + + return s.compilePackages(parsed) +} + +func (s *Session) buildTestPackage(pkg *PackageData) (*ParsedPackage, error) { + _, err := s.buildPackages(pkg.TestPackage()) + if err != nil { + return nil, err + } + _, err = s.buildPackages(pkg.XTestPackage()) + if err != nil { + return nil, err + } + + // Generate a synthetic testmain package. + fset := token.NewFileSet() + tests := testmain.TestMain{Package: pkg.Package, Context: pkg.bctx} + tests.Scan(fset) + mainPkg, mainFile, err := tests.Synthesize(fset) + if err != nil { + return nil, fmt.Errorf("failed to generate testmain package for %s: %w", pkg.ImportPath, err) + } + + // Create a parsed package for the testmain package. + parsed := &ParsedPackage{ + ImportPath: mainPkg.ImportPath, + Dir: mainPkg.Dir, + GoFiles: []*ast.File{mainFile}, + FileSet: fset, + } + + // Import dependencies for the testmain package. + for _, importedPkgPath := range parsed.Imports() { + _, _, err := s.buildImportPathWithSrcDir(importedPkgPath, pkg.Dir) + if err != nil { + return nil, err + } + } + + return parsed, nil } -// buildImportPathWithSrcDir builds the package specified by the import path. +// buildImportPathWithSrcDir builds the parsed package specified by the import path. // // Relative import paths are interpreted relative to the passed srcDir. If // srcDir is empty, current working directory is assumed. -func (s *Session) buildImportPathWithSrcDir(path string, srcDir string) (*PackageData, *compiler.Archive, error) { +func (s *Session) buildImportPathWithSrcDir(path, srcDir string) (*PackageData, *ParsedPackage, error) { pkg, err := s.xctx.Import(path, srcDir, 0) if s.Watcher != nil && pkg != nil { // add watch even on error s.Watcher.Add(pkg.Dir) @@ -931,12 +1026,25 @@ func (s *Session) buildImportPathWithSrcDir(path string, srcDir string) (*Packag return nil, nil, err } - archive, err := s.BuildPackage(pkg) + parsed, err := s.buildPackages(pkg) if err != nil { return nil, nil, err } - return pkg, archive, nil + s.cacheImportPath(path, srcDir, pkg.ImportPath) + return pkg, parsed, nil +} + +// cacheImportPath stores the import path for the build package so we can look +// it up later without getting the whole build package. +// The given path and source directly are the ones passed into +// XContext.Import to the get the build package originally. +func (s *Session) cacheImportPath(path, srcDir, importPath string) { + if paths, ok := s.importPaths[srcDir]; ok { + paths[path] = importPath + } else { + s.importPaths[srcDir] = map[string]string{path: importPath} + } } // getExeModTime will determine the mod time of the GopherJS binary @@ -965,10 +1073,9 @@ var getExeModTime = func() func() time.Time { } }() -// BuildPackage compiles an already loaded package. -func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) { - if archive, ok := s.UpToDateArchives[pkg.ImportPath]; ok { - return archive, nil +func (s *Session) buildPackages(pkg *PackageData) (*ParsedPackage, error) { + if parsed, ok := s.parsedPackages[pkg.ImportPath]; ok { + return parsed, nil } if exeModTime := getExeModTime(); exeModTime.After(pkg.SrcModTime) { @@ -993,16 +1100,7 @@ func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) { pkg.SrcModTime = fileModTime } - if !s.options.NoCache { - archive := s.buildCache.LoadArchive(pkg.ImportPath, pkg.SrcModTime, s.Types) - if archive != nil { - s.UpToDateArchives[pkg.ImportPath] = archive - // Existing archive is up to date, no need to build it from scratch. - return archive, nil - } - } - - // Existing archive is out of date or doesn't exist, let's build the package. + // Build the package by parsing and augmenting the original files with overlay files. fileSet := token.NewFileSet() files, overlayJsFiles, err := parseAndAugment(s.xctx, pkg, pkg.IsTest, fileSet) if err != nil { @@ -1016,16 +1114,42 @@ func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) { files = append(files, embed) } + parsed := &ParsedPackage{ + ImportPath: pkg.ImportPath, + Dir: pkg.Dir, + GoFiles: files, + FileSet: fileSet, + JSFiles: append(pkg.JSFiles, overlayJsFiles...), + } + s.parsedPackages[pkg.ImportPath] = parsed + + // Import dependencies from the augmented files, + // whilst skipping any that have been already imported. + for _, importedPkgPath := range parsed.Imports(pkg.Imports...) { + _, _, err := s.buildImportPathWithSrcDir(importedPkgPath, pkg.Dir) + if err != nil { + return nil, err + } + } + + return parsed, nil +} + +func (s *Session) compilePackages(pkg *ParsedPackage) (*compiler.Archive, error) { + if archive, ok := s.UpToDateArchives[pkg.ImportPath]; ok { + return archive, nil + } + importContext := &compiler.ImportContext{ Packages: s.Types, - Import: s.ImportResolverFor(pkg), + Import: s.ImportResolverFor(pkg.Dir), } - archive, err := compiler.Compile(pkg.ImportPath, files, fileSet, importContext, s.options.Minify) + archive, err := compiler.Compile(pkg.ImportPath, pkg.GoFiles, pkg.FileSet, importContext, s.options.Minify) if err != nil { return nil, err } - for _, jsFile := range append(pkg.JSFiles, overlayJsFiles...) { + for _, jsFile := range pkg.JSFiles { archive.IncJSCode = append(archive.IncJSCode, []byte("\t(function() {\n")...) archive.IncJSCode = append(archive.IncJSCode, jsFile.Content...) archive.IncJSCode = append(archive.IncJSCode, []byte("\n\t}).call($global);\n")...) @@ -1035,21 +1159,50 @@ func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) { fmt.Println(pkg.ImportPath) } - s.buildCache.StoreArchive(archive, time.Now()) s.UpToDateArchives[pkg.ImportPath] = archive return archive, nil } +func (s *Session) getImportPath(path, srcDir string) (string, error) { + // If path is for an xtest package, just return it. + if strings.HasSuffix(path, "_test") { + return path, nil + } + + // Check if the import path is already cached. + if importPath, ok := s.importPaths[srcDir][path]; ok { + return importPath, nil + } + + // Fall back to the slop import of the build package. + pkg, err := s.xctx.Import(path, srcDir, 0) + if err != nil { + return ``, err + } + s.cacheImportPath(path, srcDir, pkg.ImportPath) + return pkg.ImportPath, nil +} + // ImportResolverFor returns a function which returns a compiled package archive // given an import path. -func (s *Session) ImportResolverFor(pkg *PackageData) func(string) (*compiler.Archive, error) { +func (s *Session) ImportResolverFor(srcDir string) func(string) (*compiler.Archive, error) { return func(path string) (*compiler.Archive, error) { - if archive, ok := s.UpToDateArchives[path]; ok { + importPath, err := s.getImportPath(path, srcDir) + if err != nil { + return nil, err + } + + if archive, ok := s.UpToDateArchives[importPath]; ok { return archive, nil } - _, archive, err := s.buildImportPathWithSrcDir(path, pkg.Dir) - return archive, err + + // The archive hasn't been compiled yet so compile it with the parsed package. + if parsed, ok := s.parsedPackages[importPath]; ok { + return s.compilePackages(parsed) + } + + return nil, fmt.Errorf(`parsed package for %q not found`, importPath) } } @@ -1087,13 +1240,7 @@ func (s *Session) WriteCommandPackage(archive *compiler.Archive, pkgObj string) sourceMapFilter.MappingCallback = s.SourceMappingCallback(m) } - deps, err := compiler.ImportDependencies(archive, func(path string) (*compiler.Archive, error) { - if archive, ok := s.UpToDateArchives[path]; ok { - return archive, nil - } - _, archive, err := s.buildImportPathWithSrcDir(path, "") - return archive, err - }) + deps, err := compiler.ImportDependencies(archive, s.ImportResolverFor("")) if err != nil { return err } diff --git a/internal/testmain/testmain.go b/internal/testmain/testmain.go index f1b3257d5..3de87d382 100644 --- a/internal/testmain/testmain.go +++ b/internal/testmain/testmain.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" "go/ast" - gobuild "go/build" + "go/build" "go/doc" "go/parser" "go/token" @@ -16,7 +16,6 @@ import ( "unicode" "unicode/utf8" - "github.com/gopherjs/gopherjs/build" "golang.org/x/tools/go/buildutil" ) @@ -66,7 +65,8 @@ func (ef ExampleFunc) Executable() bool { // TestMain is a helper type responsible for generation of the test main package. type TestMain struct { - Package *build.PackageData + Package *build.Package + Context *build.Context Tests []TestFunc Benchmarks []TestFunc Fuzz []TestFunc @@ -88,7 +88,7 @@ func (tm *TestMain) Scan(fset *token.FileSet) error { func (tm *TestMain) scanPkg(fset *token.FileSet, files []string, loc FuncLocation) error { for _, name := range files { srcPath := path.Join(tm.Package.Dir, name) - f, err := buildutil.OpenFile(tm.Package.InternalBuildContext(), srcPath) + f, err := buildutil.OpenFile(tm.Context, srcPath) if err != nil { return fmt.Errorf("failed to open source file %q: %w", srcPath, err) } @@ -158,7 +158,7 @@ func (tm *TestMain) scanFile(f *ast.File, loc FuncLocation) error { } // Synthesize main package for the tests. -func (tm *TestMain) Synthesize(fset *token.FileSet) (*build.PackageData, *ast.File, error) { +func (tm *TestMain) Synthesize(fset *token.FileSet) (*build.Package, *ast.File, error) { buf := &bytes.Buffer{} if err := testmainTmpl.Execute(buf, tm); err != nil { return nil, nil, fmt.Errorf("failed to generate testmain source for package %s: %w", tm.Package.ImportPath, err) @@ -167,12 +167,10 @@ func (tm *TestMain) Synthesize(fset *token.FileSet) (*build.PackageData, *ast.Fi if err != nil { return nil, nil, fmt.Errorf("failed to parse testmain source for package %s: %w", tm.Package.ImportPath, err) } - pkg := &build.PackageData{ - Package: &gobuild.Package{ - ImportPath: tm.Package.ImportPath + ".testmain", - Name: "main", - GoFiles: []string{"_testmain.go"}, - }, + pkg := &build.Package{ + ImportPath: tm.Package.ImportPath + ".testmain", + Name: "main", + GoFiles: []string{"_testmain.go"}, } return pkg, src, nil } diff --git a/internal/testmain/testmain_test.go b/internal/testmain/testmain_test.go index 01c92cc76..8e0b268d2 100644 --- a/internal/testmain/testmain_test.go +++ b/internal/testmain/testmain_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/gopherjs/gopherjs/build" "github.com/gopherjs/gopherjs/internal/srctesting" . "github.com/gopherjs/gopherjs/internal/testmain" @@ -21,7 +22,10 @@ func TestScan(t *testing.T) { fset := token.NewFileSet() - got := TestMain{Package: pkg} + got := TestMain{ + Package: pkg.Package, + Context: pkg.InternalBuildContext(), + } if err := got.Scan(fset); err != nil { t.Fatalf("Got: tm.Scan() returned error: %s. Want: no error.", err) } @@ -47,6 +51,7 @@ func TestScan(t *testing.T) { } opts := cmp.Options{ cmpopts.IgnoreFields(TestMain{}, "Package"), // Inputs. + cmpopts.IgnoreFields(TestMain{}, "Context"), } if diff := cmp.Diff(want, got, opts...); diff != "" { t.Errorf("List of test function is different from expected (-want,+got):\n%s", diff) @@ -54,9 +59,7 @@ func TestScan(t *testing.T) { } func TestSynthesize(t *testing.T) { - pkg := &build.PackageData{ - Package: &gobuild.Package{ImportPath: "foo/bar"}, - } + pkg := &gobuild.Package{ImportPath: "foo/bar"} tests := []struct { descr string diff --git a/tool.go b/tool.go index d72e187c6..9f791af1b 100644 --- a/tool.go +++ b/tool.go @@ -4,10 +4,8 @@ import ( "bytes" "errors" "fmt" - "go/ast" "go/build" "go/scanner" - "go/token" "go/types" "io" "net" @@ -29,7 +27,6 @@ import ( "github.com/gopherjs/gopherjs/build/cache" "github.com/gopherjs/gopherjs/compiler" "github.com/gopherjs/gopherjs/internal/sysutil" - "github.com/gopherjs/gopherjs/internal/testmain" "github.com/neelance/sourcemap" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -147,7 +144,7 @@ func main() { if err != nil { return err } - archive, err := s.BuildPackage(pkg) + archive, err := s.BuildProject(pkg) if err != nil { return err } @@ -214,8 +211,7 @@ func main() { if err != nil { return err } - - archive, err := s.BuildPackage(pkg) + archive, err := s.BuildProject(pkg) if err != nil { return err } @@ -371,27 +367,8 @@ func main() { return err } - _, err = s.BuildPackage(pkg.TestPackage()) - if err != nil { - return err - } - _, err = s.BuildPackage(pkg.XTestPackage()) - if err != nil { - return err - } - - fset := token.NewFileSet() - tests := testmain.TestMain{Package: pkg} - tests.Scan(fset) - mainPkg, mainFile, err := tests.Synthesize(fset) - if err != nil { - return fmt.Errorf("failed to generate testmain package for %s: %w", pkg.ImportPath, err) - } - importContext := &compiler.ImportContext{ - Packages: s.Types, - Import: s.ImportResolverFor(mainPkg), - } - mainPkgArchive, err := compiler.Compile(mainPkg.ImportPath, []*ast.File{mainFile}, fset, importContext, options.Minify) + pkg.IsTest = true + mainPkgArchive, err := s.BuildProject(pkg) if err != nil { return fmt.Errorf("failed to compile testmain package for %s: %w", pkg.ImportPath, err) } @@ -664,7 +641,7 @@ func (fs serveCommandFileSystem) Open(requestName string) (http.File, error) { buf := new(bytes.Buffer) browserErrors := new(bytes.Buffer) err := func() error { - archive, err := s.BuildPackage(pkg) + archive, err := s.BuildProject(pkg) if err != nil { return err } @@ -673,7 +650,7 @@ func (fs serveCommandFileSystem) Open(requestName string) (http.File, error) { m := &sourcemap.Map{File: base + ".js"} sourceMapFilter.MappingCallback = s.SourceMappingCallback(m) - deps, err := compiler.ImportDependencies(archive, s.BuildImportPath) + deps, err := compiler.ImportDependencies(archive, s.ImportResolverFor("")) if err != nil { return err } From 2a9b9a42d559dc48b0f554c635d7c3125290cfaa Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Fri, 14 Feb 2025 09:25:42 -0700 Subject: [PATCH 36/58] Fixing spelling mistake --- build/build.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/build.go b/build/build.go index 300e295bd..62b65d0b2 100644 --- a/build/build.go +++ b/build/build.go @@ -1175,7 +1175,7 @@ func (s *Session) getImportPath(path, srcDir string) (string, error) { return importPath, nil } - // Fall back to the slop import of the build package. + // Fall back to the slow import of the build package. pkg, err := s.xctx.Import(path, srcDir, 0) if err != nil { return ``, err From 82034adb465df44aa89dce3dd8c1f5e7f1a3a35a Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 20 Feb 2025 09:55:00 -0700 Subject: [PATCH 37/58] using Sources instead of ParsedPackage --- build/build.go | 134 ++++++----------- build/context.go | 41 +---- compiler/compiler_test.go | 17 ++- compiler/decls.go | 3 +- .../{errors.go => errorList/errorList.go} | 2 +- compiler/jsFile/jsFile.go | 55 +++++++ compiler/linkname.go | 3 +- compiler/package.go | 65 +++++--- compiler/sources.go | 123 --------------- compiler/sources/errorCollectingImporter.go | 23 +++ compiler/sources/sources.go | 141 ++++++++++++++++++ tool.go | 3 +- 12 files changed, 333 insertions(+), 277 deletions(-) rename compiler/{errors.go => errorList/errorList.go} (98%) create mode 100644 compiler/jsFile/jsFile.go delete mode 100644 compiler/sources.go create mode 100644 compiler/sources/errorCollectingImporter.go create mode 100644 compiler/sources/sources.go diff --git a/build/build.go b/build/build.go index 62b65d0b2..0bd32aff8 100644 --- a/build/build.go +++ b/build/build.go @@ -27,6 +27,9 @@ import ( "github.com/fsnotify/fsnotify" "github.com/gopherjs/gopherjs/compiler" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/errorList" + "github.com/gopherjs/gopherjs/compiler/jsFile" + "github.com/gopherjs/gopherjs/compiler/sources" "github.com/gopherjs/gopherjs/internal/testmain" log "github.com/sirupsen/logrus" @@ -163,7 +166,7 @@ type overrideInfo struct { // - Otherwise for identifiers that exist in the original and the overrides, // the original is removed. // - New identifiers that don't exist in original package get added. -func parseAndAugment(xctx XContext, pkg *PackageData, isTest bool, fileSet *token.FileSet) ([]*ast.File, []JSFile, error) { +func parseAndAugment(xctx XContext, pkg *PackageData, isTest bool, fileSet *token.FileSet) ([]*ast.File, []jsFile.JSFile, error) { jsFiles, overlayFiles := parseOverlayFiles(xctx, pkg, isTest, fileSet) originalFiles, err := parserOriginalFiles(pkg, fileSet) @@ -192,7 +195,7 @@ func parseAndAugment(xctx XContext, pkg *PackageData, isTest bool, fileSet *toke // parseOverlayFiles loads and parses overlay files // to augment the original files with. -func parseOverlayFiles(xctx XContext, pkg *PackageData, isTest bool, fileSet *token.FileSet) ([]JSFile, []*ast.File) { +func parseOverlayFiles(xctx XContext, pkg *PackageData, isTest bool, fileSet *token.FileSet) ([]jsFile.JSFile, []*ast.File) { isXTest := strings.HasSuffix(pkg.ImportPath, "_test") importPath := pkg.ImportPath if isXTest { @@ -238,7 +241,7 @@ func parseOverlayFiles(xctx XContext, pkg *PackageData, isTest bool, fileSet *to // parserOriginalFiles loads and parses the original files to augment. func parserOriginalFiles(pkg *PackageData, fileSet *token.FileSet) ([]*ast.File, error) { var files []*ast.File - var errList compiler.ErrorList + var errList errorList.ErrorList for _, name := range pkg.GoFiles { if !filepath.IsAbs(name) { // name might be absolute if specified directly. E.g., `gopherjs build /abs/file.go`. name = filepath.Join(pkg.Dir, name) @@ -622,18 +625,11 @@ func (o *Options) PrintSuccess(format string, a ...interface{}) { fmt.Fprintf(os.Stderr, format, a...) } -// JSFile represents a *.inc.js file metadata and content. -type JSFile struct { - Path string // Full file path for the build context the file came from. - ModTime time.Time - Content []byte -} - // PackageData is an extension of go/build.Package with additional metadata // GopherJS requires. type PackageData struct { *build.Package - JSFiles []JSFile + JSFiles []jsFile.JSFile // IsTest is true if the package is being built for running tests. IsTest bool SrcModTime time.Time @@ -743,43 +739,6 @@ func (p *PackageData) InstallPath() string { return p.PkgObj } -// ParsedPackage is the results from building a package before it is compiled. -type ParsedPackage struct { - ImportPath string // import path of package ("" if unknown) - Dir string // directory containing package sources - - // GoFiles is the parsed and augmented Go AST files for the package. - GoFiles []*ast.File - FileSet *token.FileSet - JSFiles []JSFile -} - -// Imports calculates the import paths of the package's dependencies -// based on all the imports in the augmented files. -// -// The given skip paths will not be returned in the results. -// This will not return any `*_test` packages in the results. -func (p *ParsedPackage) Imports(skip ...string) []string { - seen := make(map[string]struct{}) - for _, s := range skip { - seen[s] = struct{}{} - } - imports := []string{} - for _, file := range p.GoFiles { - for _, imp := range file.Imports { - path := strings.Trim(imp.Path.Value, `"`) - if _, ok := seen[path]; !ok { - if !strings.HasSuffix(path, "_test") { - imports = append(imports, path) - } - seen[path] = struct{}{} - } - } - } - sort.Strings(imports) - return imports -} - // Session manages internal state GopherJS requires to perform a build. // // This is the main interface to GopherJS build system. Session lifetime is @@ -793,10 +752,11 @@ type Session struct { // compilation when we're looking up parsed packages. importPaths map[string]map[string]string - // parsePackage is a map of parsed packages that have been built and augmented. + // sources is a map of parsed packages that have been built and augmented. // This is keyed using resolved import paths. This is used to avoid // rebuilding and augmenting packages that are imported by several packages. - parsedPackages map[string]*ParsedPackage + // These sources haven't been sorted nor simplified yet. + sources map[string]*sources.Sources // Binary archives produced during the current session and assumed to be // up to date with input sources and dependencies. In the -w ("watch") mode @@ -813,7 +773,7 @@ func NewSession(options *Options) (*Session, error) { s := &Session{ options: options, importPaths: make(map[string]map[string]string), - parsedPackages: make(map[string]*ParsedPackage), + sources: make(map[string]*sources.Sources), UpToDateArchives: make(map[string]*compiler.Archive), } s.xctx = NewBuildContext(s.InstallSuffix(), s.options.BuildTags) @@ -931,7 +891,7 @@ func (s *Session) BuildFiles(filenames []string, pkgObj string, cwd string) erro if err != nil { return fmt.Errorf("failed to stat %s: %w", file, err) } - pkg.JSFiles = append(pkg.JSFiles, JSFile{ + pkg.JSFiles = append(pkg.JSFiles, jsFile.JSFile{ Path: filepath.Join(pkg.Dir, filepath.Base(file)), ModTime: info.ModTime(), Content: content, @@ -954,13 +914,13 @@ func (s *Session) BuildProject(pkg *PackageData) (*compiler.Archive, error) { // ensure that runtime for gopherjs is imported pkg.Imports = append(pkg.Imports, `runtime`) - // Build the project to get the parsed packages. - var parsed *ParsedPackage + // Build the project to get the sources for the parsed packages. + var srcs *sources.Sources var err error if pkg.IsTest { - parsed, err = s.buildTestPackage(pkg) + srcs, err = s.buildTestPackage(pkg) } else { - parsed, err = s.buildPackages(pkg) + srcs, err = s.buildPackages(pkg) } if err != nil { return nil, err @@ -972,10 +932,10 @@ func (s *Session) BuildProject(pkg *PackageData) (*compiler.Archive, error) { // flatten, blocking, etc. information and check types to get the type info // with all the instances for all generics in the whole project. - return s.compilePackages(parsed) + return s.compilePackages(srcs) } -func (s *Session) buildTestPackage(pkg *PackageData) (*ParsedPackage, error) { +func (s *Session) buildTestPackage(pkg *PackageData) (*sources.Sources, error) { _, err := s.buildPackages(pkg.TestPackage()) if err != nil { return nil, err @@ -994,30 +954,30 @@ func (s *Session) buildTestPackage(pkg *PackageData) (*ParsedPackage, error) { return nil, fmt.Errorf("failed to generate testmain package for %s: %w", pkg.ImportPath, err) } - // Create a parsed package for the testmain package. - parsed := &ParsedPackage{ + // Create the sources for parsed package for the testmain package. + srcs := &sources.Sources{ ImportPath: mainPkg.ImportPath, Dir: mainPkg.Dir, - GoFiles: []*ast.File{mainFile}, + Files: []*ast.File{mainFile}, FileSet: fset, } // Import dependencies for the testmain package. - for _, importedPkgPath := range parsed.Imports() { + for _, importedPkgPath := range srcs.Imports() { _, _, err := s.buildImportPathWithSrcDir(importedPkgPath, pkg.Dir) if err != nil { return nil, err } } - return parsed, nil + return srcs, nil } -// buildImportPathWithSrcDir builds the parsed package specified by the import path. +// buildImportPathWithSrcDir builds the sources for a package specified by the import path. // // Relative import paths are interpreted relative to the passed srcDir. If // srcDir is empty, current working directory is assumed. -func (s *Session) buildImportPathWithSrcDir(path, srcDir string) (*PackageData, *ParsedPackage, error) { +func (s *Session) buildImportPathWithSrcDir(path, srcDir string) (*PackageData, *sources.Sources, error) { pkg, err := s.xctx.Import(path, srcDir, 0) if s.Watcher != nil && pkg != nil { // add watch even on error s.Watcher.Add(pkg.Dir) @@ -1026,13 +986,13 @@ func (s *Session) buildImportPathWithSrcDir(path, srcDir string) (*PackageData, return nil, nil, err } - parsed, err := s.buildPackages(pkg) + srcs, err := s.buildPackages(pkg) if err != nil { return nil, nil, err } s.cacheImportPath(path, srcDir, pkg.ImportPath) - return pkg, parsed, nil + return pkg, srcs, nil } // cacheImportPath stores the import path for the build package so we can look @@ -1073,9 +1033,9 @@ var getExeModTime = func() func() time.Time { } }() -func (s *Session) buildPackages(pkg *PackageData) (*ParsedPackage, error) { - if parsed, ok := s.parsedPackages[pkg.ImportPath]; ok { - return parsed, nil +func (s *Session) buildPackages(pkg *PackageData) (*sources.Sources, error) { + if srcs, ok := s.sources[pkg.ImportPath]; ok { + return srcs, nil } if exeModTime := getExeModTime(); exeModTime.After(pkg.SrcModTime) { @@ -1114,52 +1074,52 @@ func (s *Session) buildPackages(pkg *PackageData) (*ParsedPackage, error) { files = append(files, embed) } - parsed := &ParsedPackage{ + srcs := &sources.Sources{ ImportPath: pkg.ImportPath, Dir: pkg.Dir, - GoFiles: files, + Files: files, FileSet: fileSet, JSFiles: append(pkg.JSFiles, overlayJsFiles...), } - s.parsedPackages[pkg.ImportPath] = parsed + s.sources[pkg.ImportPath] = srcs // Import dependencies from the augmented files, // whilst skipping any that have been already imported. - for _, importedPkgPath := range parsed.Imports(pkg.Imports...) { + for _, importedPkgPath := range srcs.Imports(pkg.Imports...) { _, _, err := s.buildImportPathWithSrcDir(importedPkgPath, pkg.Dir) if err != nil { return nil, err } } - return parsed, nil + return srcs, nil } -func (s *Session) compilePackages(pkg *ParsedPackage) (*compiler.Archive, error) { - if archive, ok := s.UpToDateArchives[pkg.ImportPath]; ok { +func (s *Session) compilePackages(srcs *sources.Sources) (*compiler.Archive, error) { + if archive, ok := s.UpToDateArchives[srcs.ImportPath]; ok { return archive, nil } importContext := &compiler.ImportContext{ - Packages: s.Types, - Import: s.ImportResolverFor(pkg.Dir), + Packages: s.Types, + ImportArchive: s.ImportResolverFor(srcs.Dir), } - archive, err := compiler.Compile(pkg.ImportPath, pkg.GoFiles, pkg.FileSet, importContext, s.options.Minify) + archive, err := compiler.Compile(*srcs, importContext, s.options.Minify) if err != nil { return nil, err } - for _, jsFile := range pkg.JSFiles { + for _, jsFile := range srcs.JSFiles { archive.IncJSCode = append(archive.IncJSCode, []byte("\t(function() {\n")...) archive.IncJSCode = append(archive.IncJSCode, jsFile.Content...) archive.IncJSCode = append(archive.IncJSCode, []byte("\n\t}).call($global);\n")...) } if s.options.Verbose { - fmt.Println(pkg.ImportPath) + fmt.Println(srcs.ImportPath) } - s.UpToDateArchives[pkg.ImportPath] = archive + s.UpToDateArchives[srcs.ImportPath] = archive return archive, nil } @@ -1197,12 +1157,12 @@ func (s *Session) ImportResolverFor(srcDir string) func(string) (*compiler.Archi return archive, nil } - // The archive hasn't been compiled yet so compile it with the parsed package. - if parsed, ok := s.parsedPackages[importPath]; ok { - return s.compilePackages(parsed) + // The archive hasn't been compiled yet so compile it with the sources. + if srcs, ok := s.sources[importPath]; ok { + return s.compilePackages(srcs) } - return nil, fmt.Errorf(`parsed package for %q not found`, importPath) + return nil, fmt.Errorf(`sources for %q not found`, importPath) } } diff --git a/build/context.go b/build/context.go index 316bfb2bb..657300839 100644 --- a/build/context.go +++ b/build/context.go @@ -4,7 +4,6 @@ import ( "fmt" "go/build" "go/token" - "io" "net/http" "os" "os/exec" @@ -16,6 +15,7 @@ import ( _ "github.com/gopherjs/gopherjs/build/versionhack" // go/build release tags hack. "github.com/gopherjs/gopherjs/compiler" "github.com/gopherjs/gopherjs/compiler/gopherjspkg" + "github.com/gopherjs/gopherjs/compiler/jsFile" "github.com/gopherjs/gopherjs/compiler/natives" "golang.org/x/tools/go/buildutil" ) @@ -91,7 +91,7 @@ func (sc simpleCtx) Import(importPath string, srcDir string, mode build.ImportMo if err != nil { return nil, err } - jsFiles, err := jsFilesFromDir(&sc.bctx, pkg.Dir) + jsFiles, err := jsFile.JSFilesFromDir(&sc.bctx, pkg.Dir) if err != nil { return nil, fmt.Errorf("failed to enumerate .inc.js files in %s: %w", pkg.Dir, err) } @@ -440,40 +440,3 @@ func updateImports(sources []string, importPos map[string][]token.Position) (new sort.Strings(newImports) return newImports, newImportPos } - -// jsFilesFromDir finds and loads any *.inc.js packages in the build context -// directory. -func jsFilesFromDir(bctx *build.Context, dir string) ([]JSFile, error) { - files, err := buildutil.ReadDir(bctx, dir) - if err != nil { - return nil, err - } - var jsFiles []JSFile - for _, file := range files { - if !strings.HasSuffix(file.Name(), ".inc.js") || file.IsDir() { - continue - } - if file.Name()[0] == '_' || file.Name()[0] == '.' { - continue // Skip "hidden" files that are typically ignored by the Go build system. - } - - path := buildutil.JoinPath(bctx, dir, file.Name()) - f, err := buildutil.OpenFile(bctx, path) - if err != nil { - return nil, fmt.Errorf("failed to open %s from %v: %w", path, bctx, err) - } - defer f.Close() - - content, err := io.ReadAll(f) - if err != nil { - return nil, fmt.Errorf("failed to read %s from %v: %w", path, bctx, err) - } - - jsFiles = append(jsFiles, JSFile{ - Path: path, - ModTime: file.ModTime(), - Content: content, - }) - } - return jsFiles, nil -} diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index fe75880b1..4c1dd4dba 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -12,6 +12,7 @@ import ( "golang.org/x/tools/go/packages" "github.com/gopherjs/gopherjs/compiler/internal/dce" + "github.com/gopherjs/gopherjs/compiler/sources" "github.com/gopherjs/gopherjs/internal/srctesting" ) @@ -681,7 +682,7 @@ func compileProject(t *testing.T, root *packages.Package, minify bool) map[strin var importContext *ImportContext importContext = &ImportContext{ Packages: map[string]*types.Package{}, - Import: func(path string) (*Archive, error) { + ImportArchive: func(path string) (*Archive, error) { // find in local cache if a, ok := archiveCache[path]; ok { return a, nil @@ -693,8 +694,14 @@ func compileProject(t *testing.T, root *packages.Package, minify bool) map[strin } importContext.Packages[path] = pkg.Types + srcs := sources.Sources{ + ImportPath: path, + Files: pkg.Syntax, + FileSet: pkg.Fset, + } + // compile package - a, err := Compile(path, pkg.Syntax, pkg.Fset, importContext, minify) + a, err := Compile(srcs, importContext, minify) if err != nil { return nil, err } @@ -703,7 +710,7 @@ func compileProject(t *testing.T, root *packages.Package, minify bool) map[strin }, } - _, err := importContext.Import(root.PkgPath) + _, err := importContext.ImportArchive(root.PkgPath) if err != nil { t.Fatal(`failed to compile:`, err) } @@ -737,7 +744,7 @@ func reloadCompiledProject(t *testing.T, archives map[string]*Archive, rootPkgPa var importContext *ImportContext importContext = &ImportContext{ Packages: map[string]*types.Package{}, - Import: func(path string) (*Archive, error) { + ImportArchive: func(path string) (*Archive, error) { // find in local cache if a, ok := reloadCache[path]; ok { return a, nil @@ -757,7 +764,7 @@ func reloadCompiledProject(t *testing.T, archives map[string]*Archive, rootPkgPa }, } - _, err := importContext.Import(rootPkgPath) + _, err := importContext.ImportArchive(rootPkgPath) if err != nil { t.Fatal(`failed to reload archives:`, err) } diff --git a/compiler/decls.go b/compiler/decls.go index e292ad07c..0694181f6 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -16,6 +16,7 @@ import ( "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/sources" "github.com/gopherjs/gopherjs/compiler/typesutil" ) @@ -81,7 +82,7 @@ func (d *Decl) Dce() *dce.Info { // 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 typesutil.TypeNames) { +func (fc *funcContext) topLevelObjects(srcs sources.Sources) (vars []*types.Var, functions []*ast.FuncDecl, typeNames typesutil.TypeNames) { if !fc.isRoot() { panic(bailout(fmt.Errorf("functionContext.discoverObjects() must be only called on the package-level context"))) } diff --git a/compiler/errors.go b/compiler/errorList/errorList.go similarity index 98% rename from compiler/errors.go rename to compiler/errorList/errorList.go index 48aed48ec..531a0f4e0 100644 --- a/compiler/errors.go +++ b/compiler/errorList/errorList.go @@ -1,4 +1,4 @@ -package compiler +package errorList import ( "errors" diff --git a/compiler/jsFile/jsFile.go b/compiler/jsFile/jsFile.go new file mode 100644 index 000000000..b8ae9421f --- /dev/null +++ b/compiler/jsFile/jsFile.go @@ -0,0 +1,55 @@ +package jsFile + +import ( + "fmt" + "go/build" + "io" + "strings" + "time" + + "golang.org/x/tools/go/buildutil" +) + +// JSFile represents a *.inc.js file metadata and content. +type JSFile struct { + Path string // Full file path for the build context the file came from. + ModTime time.Time + Content []byte +} + +// JSFilesFromDir finds and loads any *.inc.js packages in the build context +// directory. +func JSFilesFromDir(bctx *build.Context, dir string) ([]JSFile, error) { + files, err := buildutil.ReadDir(bctx, dir) + if err != nil { + return nil, err + } + var jsFiles []JSFile + for _, file := range files { + if !strings.HasSuffix(file.Name(), ".inc.js") || file.IsDir() { + continue + } + if file.Name()[0] == '_' || file.Name()[0] == '.' { + continue // Skip "hidden" files that are typically ignored by the Go build system. + } + + path := buildutil.JoinPath(bctx, dir, file.Name()) + f, err := buildutil.OpenFile(bctx, path) + if err != nil { + return nil, fmt.Errorf("failed to open %s from %v: %w", path, bctx, err) + } + defer f.Close() + + content, err := io.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("failed to read %s from %v: %w", path, bctx, err) + } + + jsFiles = append(jsFiles, JSFile{ + Path: path, + ModTime: file.ModTime(), + Content: content, + }) + } + return jsFiles, nil +} diff --git a/compiler/linkname.go b/compiler/linkname.go index c4f15a23e..3a9b47934 100644 --- a/compiler/linkname.go +++ b/compiler/linkname.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/errorList" "github.com/gopherjs/gopherjs/compiler/internal/symbol" ) @@ -38,7 +39,7 @@ type GoLinkname struct { // words, it can only "import" an external function implementation into the // local scope). func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]GoLinkname, error) { - var errs ErrorList = nil + var errs errorList.ErrorList = nil var directives []GoLinkname isUnsafe := astutil.ImportsUnsafe(file) diff --git a/compiler/package.go b/compiler/package.go index ba018a66b..2039758f0 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -7,12 +7,15 @@ import ( "go/types" "strings" + "golang.org/x/tools/go/types/typeutil" + + "github.com/gopherjs/gopherjs/compiler/errorList" "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/sources" "github.com/gopherjs/gopherjs/compiler/typesutil" "github.com/gopherjs/gopherjs/internal/experiments" - "golang.org/x/tools/go/types/typeutil" ) // pkgContext maintains compiler context for a specific package. @@ -35,7 +38,7 @@ type pkgContext struct { indentation int minify bool fileSet *token.FileSet - errList ErrorList + errList errorList.ErrorList instanceSet *typeparams.PackageInstanceSets } @@ -114,7 +117,7 @@ type funcContext struct { funcLitCounter int } -func newRootCtx(tContext *types.Context, srcs sources, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(typeparams.Instance) bool, minify bool) *funcContext { +func newRootCtx(tContext *types.Context, srcs sources.Sources, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(typeparams.Instance) bool, minify bool) *funcContext { tc := typeparams.Collector{ TContext: tContext, Info: typesInfo, @@ -159,10 +162,10 @@ type flowData struct { type ImportContext struct { // Mapping for an absolute import path to the package type information. Packages map[string]*types.Package - // 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) + // ImportArchive 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. + ImportArchive func(importPath string) (*Archive, error) } // isBlocking returns true if an _imported_ function is blocking. It will panic @@ -178,7 +181,7 @@ func (ic *ImportContext) isBlocking(inst typeparams.Instance) bool { panic(bailout(fmt.Errorf("can't determine if instance %v is blocking: instance isn't for a function object", inst))) } - archive, err := ic.Import(f.Pkg().Path()) + archive, err := ic.ImportArchive(f.Pkg().Path()) if err != nil { panic(err) } @@ -192,11 +195,39 @@ func (ic *ImportContext) isBlocking(inst typeparams.Instance) bool { panic(bailout(fmt.Errorf("can't determine if function %s is blocking: decl not found in package archive", fullName))) } +// Import implements go/types.Importer interface for ImportContext. +func (ic *ImportContext) Import(path string) (*types.Package, error) { + if path == "unsafe" { + return types.Unsafe, nil + } + + // By importing the archive, the package will compile if it hasn't been + // compiled yet and the package will be added to the Packages map. + a, err := ic.ImportArchive(path) + if err != nil { + return nil, err + } + + return ic.Packages[a.ImportPath], nil +} + +// parseAllGoLinknames extracts all //go:linkname compiler directive from the sources. +func parseAllGoLinknames(s sources.Sources) ([]GoLinkname, error) { + goLinknames := []GoLinkname{} + var errs errorList.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() +} + // 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) { +func Compile(srcs sources.Sources, importContext *ImportContext, minify bool) (_ *Archive, err error) { defer func() { e := recover() if e == nil { @@ -204,32 +235,28 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor } if fe, ok := bailingOut(e); ok { // Orderly bailout, return whatever clues we already have. - fmt.Fprintf(fe, `building package %q`, importPath) + fmt.Fprintf(fe, `building package %q`, srcs.ImportPath) err = fe return } // Some other unexpected panic, catch the stack trace and return as an error. - err = bailout(fmt.Errorf("unexpected compiler panic while building package %q: %v", importPath, e)) + err = bailout(fmt.Errorf("unexpected compiler panic while building package %q: %v", srcs.ImportPath, e)) }() - srcs := sources{ - ImportPath: importPath, - Files: files, - FileSet: fileSet, - }.Sort() + srcs.Sort() tContext := types.NewContext() - typesInfo, typesPkg, err := srcs.TypeCheck(importContext, tContext) + typesInfo, typesPkg, err := srcs.TypeCheck(importContext, sizes32, tContext) if err != nil { return nil, err } if genErr := typeparams.RequiresGenericsSupport(typesInfo); genErr != nil && !experiments.Env.Generics { - return nil, fmt.Errorf("package %s requires generics support (https://github.com/gopherjs/gopherjs/issues/1013): %w", importPath, genErr) + return nil, fmt.Errorf("package %s requires generics support (https://github.com/gopherjs/gopherjs/issues/1013): %w", srcs.ImportPath, genErr) } importContext.Packages[srcs.ImportPath] = typesPkg // Extract all go:linkname compiler directives from the package source. - goLinknames, err := srcs.ParseGoLinknames() + goLinknames, err := parseAllGoLinknames(srcs) if err != nil { return nil, err } diff --git a/compiler/sources.go b/compiler/sources.go deleted file mode 100644 index e6c3710f4..000000000 --- a/compiler/sources.go +++ /dev/null @@ -1,123 +0,0 @@ -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 -} - -// Simplified 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, tContext *types.Context) (*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{ - Context: tContext, - 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/sources/errorCollectingImporter.go b/compiler/sources/errorCollectingImporter.go new file mode 100644 index 000000000..bb9325230 --- /dev/null +++ b/compiler/sources/errorCollectingImporter.go @@ -0,0 +1,23 @@ +package sources + +import ( + "go/types" + + "github.com/gopherjs/gopherjs/compiler/errorList" +) + +// errorCollectingImporter implements go/types.Importer interface and +// wraps it to collect import errors. +type errorCollectingImporter struct { + Importer types.Importer + Errors errorList.ErrorList +} + +func (ei *errorCollectingImporter) Import(path string) (*types.Package, error) { + pkg, err := ei.Importer.Import(path) + if err != nil { + ei.Errors = ei.Errors.AppendDistinct(err) + return nil, err + } + return pkg, nil +} diff --git a/compiler/sources/sources.go b/compiler/sources/sources.go new file mode 100644 index 000000000..92b38d966 --- /dev/null +++ b/compiler/sources/sources.go @@ -0,0 +1,141 @@ +package sources + +import ( + "go/ast" + "go/token" + "go/types" + "sort" + "strings" + + "github.com/gopherjs/gopherjs/compiler/errorList" + "github.com/gopherjs/gopherjs/compiler/jsFile" + "github.com/neelance/astrewrite" +) + +// Sources is a slice of parsed Go sources and additional data for a package. +// +// 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. + // Otherwise this must be the absolute import path for a package. + ImportPath string + + // Dir is the directory containing package sources + Dir string + + // Files is the parsed and augmented Go AST files for the package. + Files []*ast.File + + // FileSet is the file set for the parsed files. + FileSet *token.FileSet + + // JSFiles is the JavaScript files that are part of the package. + JSFiles []jsFile.JSFile +} + +// Sort the Go 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.nameOfFileAtIndex(i) > s.nameOfFileAtIndex(j) + }) + return s +} + +// nameOfFileAtIndex gets the name of the Go source file at the given index. +func (s Sources) nameOfFileAtIndex(i int) string { + return s.FileSet.File(s.Files[i].Pos()).Name() +} + +// Simplified returns a new sources instance with each Files entry processed by +// astrewrite.Simplify. The JSFiles are copied unchanged. +func (s Sources) Simplified(typesInfo *types.Info) Sources { + simplified := Sources{ + ImportPath: s.ImportPath, + Dir: s.Dir, + Files: make([]*ast.File, len(s.Files)), + FileSet: s.FileSet, + JSFiles: s.JSFiles, + } + for i, file := range s.Files { + simplified.Files[i] = 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(importer types.Importer, sizes types.Sizes, tContext *types.Context) (*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.ErrorList + + ecImporter := &errorCollectingImporter{Importer: importer} + + config := &types.Config{ + Context: tContext, + Importer: ecImporter, + Sizes: sizes, + 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 ecImporter.Errors.ErrOrNil() != nil { + return nil, nil, ecImporter.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 +} + +// Imports calculates the import paths of the package's dependencies +// based on all the imports in the augmented Go AST files. +// +// The given skip paths will not be returned in the results. +// This will not return any `*_test` packages in the results. +func (s Sources) Imports(skip ...string) []string { + seen := make(map[string]struct{}) + for _, sk := range skip { + seen[sk] = struct{}{} + } + imports := []string{} + for _, file := range s.Files { + for _, imp := range file.Imports { + path := strings.Trim(imp.Path.Value, `"`) + if _, ok := seen[path]; !ok { + if !strings.HasSuffix(path, "_test") { + imports = append(imports, path) + } + seen[path] = struct{}{} + } + } + } + sort.Strings(imports) + return imports +} diff --git a/tool.go b/tool.go index 9f791af1b..c1b63ffd7 100644 --- a/tool.go +++ b/tool.go @@ -26,6 +26,7 @@ import ( gbuild "github.com/gopherjs/gopherjs/build" "github.com/gopherjs/gopherjs/build/cache" "github.com/gopherjs/gopherjs/compiler" + "github.com/gopherjs/gopherjs/compiler/errorList" "github.com/gopherjs/gopherjs/internal/sysutil" "github.com/neelance/sourcemap" log "github.com/sirupsen/logrus" @@ -766,7 +767,7 @@ func handleError(err error, options *gbuild.Options, browserErrors *bytes.Buffer switch err := err.(type) { case nil: return 0 - case compiler.ErrorList: + case errorList.ErrorList: for _, entry := range err { printError(entry, options, browserErrors) } From ba2ea8d05c63459aec7067740f9af3560b67f04b Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Mon, 24 Feb 2025 12:58:13 -0700 Subject: [PATCH 38/58] clarifying naming and comments --- build/build.go | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/build/build.go b/build/build.go index 62b65d0b2..833e7da92 100644 --- a/build/build.go +++ b/build/build.go @@ -788,9 +788,13 @@ type Session struct { options *Options xctx XContext - // importPaths is a map of the resolved import paths given the srcDir - // and path. This is used to avoid redetermining build packages during - // compilation when we're looking up parsed packages. + // importPaths is a map of the resolved import paths given the + // source directory (first key) and the unresolved import path (second key). + // This is used to cache the resolved import returned from XContext.Import. + // XContent.Import can be slow, so we cache the resolved path that is used + // as the map key by parsedPackages and UpToDateArchives. + // This makes subsequent lookups faster during compilation when all we have + // is the unresolved import path and source directory. importPaths map[string]map[string]string // parsePackage is a map of parsed packages that have been built and augmented. @@ -958,9 +962,9 @@ func (s *Session) BuildProject(pkg *PackageData) (*compiler.Archive, error) { var parsed *ParsedPackage var err error if pkg.IsTest { - parsed, err = s.buildTestPackage(pkg) + parsed, err = s.loadTestPackage(pkg) } else { - parsed, err = s.buildPackages(pkg) + parsed, err = s.loadPackages(pkg) } if err != nil { return nil, err @@ -975,12 +979,12 @@ func (s *Session) BuildProject(pkg *PackageData) (*compiler.Archive, error) { return s.compilePackages(parsed) } -func (s *Session) buildTestPackage(pkg *PackageData) (*ParsedPackage, error) { - _, err := s.buildPackages(pkg.TestPackage()) +func (s *Session) loadTestPackage(pkg *PackageData) (*ParsedPackage, error) { + _, err := s.loadPackages(pkg.TestPackage()) if err != nil { return nil, err } - _, err = s.buildPackages(pkg.XTestPackage()) + _, err = s.loadPackages(pkg.XTestPackage()) if err != nil { return nil, err } @@ -1004,7 +1008,7 @@ func (s *Session) buildTestPackage(pkg *PackageData) (*ParsedPackage, error) { // Import dependencies for the testmain package. for _, importedPkgPath := range parsed.Imports() { - _, _, err := s.buildImportPathWithSrcDir(importedPkgPath, pkg.Dir) + _, _, err := s.loadImportPathWithSrcDir(importedPkgPath, pkg.Dir) if err != nil { return nil, err } @@ -1013,11 +1017,11 @@ func (s *Session) buildTestPackage(pkg *PackageData) (*ParsedPackage, error) { return parsed, nil } -// buildImportPathWithSrcDir builds the parsed package specified by the import path. +// loadImportPathWithSrcDir gets the parsed package specified by the import path. // -// Relative import paths are interpreted relative to the passed srcDir. If -// srcDir is empty, current working directory is assumed. -func (s *Session) buildImportPathWithSrcDir(path, srcDir string) (*PackageData, *ParsedPackage, error) { +// Relative import paths are interpreted relative to the passed srcDir. +// If srcDir is empty, current working directory is assumed. +func (s *Session) loadImportPathWithSrcDir(path, srcDir string) (*PackageData, *ParsedPackage, error) { pkg, err := s.xctx.Import(path, srcDir, 0) if s.Watcher != nil && pkg != nil { // add watch even on error s.Watcher.Add(pkg.Dir) @@ -1026,7 +1030,7 @@ func (s *Session) buildImportPathWithSrcDir(path, srcDir string) (*PackageData, return nil, nil, err } - parsed, err := s.buildPackages(pkg) + parsed, err := s.loadPackages(pkg) if err != nil { return nil, nil, err } @@ -1035,8 +1039,8 @@ func (s *Session) buildImportPathWithSrcDir(path, srcDir string) (*PackageData, return pkg, parsed, nil } -// cacheImportPath stores the import path for the build package so we can look -// it up later without getting the whole build package. +// cacheImportPath stores the resolved import path for the build package +// so we can look it up later without getting the whole build package. // The given path and source directly are the ones passed into // XContext.Import to the get the build package originally. func (s *Session) cacheImportPath(path, srcDir, importPath string) { @@ -1073,7 +1077,7 @@ var getExeModTime = func() func() time.Time { } }() -func (s *Session) buildPackages(pkg *PackageData) (*ParsedPackage, error) { +func (s *Session) loadPackages(pkg *PackageData) (*ParsedPackage, error) { if parsed, ok := s.parsedPackages[pkg.ImportPath]; ok { return parsed, nil } @@ -1086,7 +1090,7 @@ func (s *Session) buildPackages(pkg *PackageData) (*ParsedPackage, error) { if importedPkgPath == "unsafe" { continue } - importedPkg, _, err := s.buildImportPathWithSrcDir(importedPkgPath, pkg.Dir) + importedPkg, _, err := s.loadImportPathWithSrcDir(importedPkgPath, pkg.Dir) if err != nil { return nil, err } @@ -1126,7 +1130,7 @@ func (s *Session) buildPackages(pkg *PackageData) (*ParsedPackage, error) { // Import dependencies from the augmented files, // whilst skipping any that have been already imported. for _, importedPkgPath := range parsed.Imports(pkg.Imports...) { - _, _, err := s.buildImportPathWithSrcDir(importedPkgPath, pkg.Dir) + _, _, err := s.loadImportPathWithSrcDir(importedPkgPath, pkg.Dir) if err != nil { return nil, err } From 5740abfa806052fe76f92e661fe7f8340304b4ab Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Wed, 26 Feb 2025 10:38:36 -0700 Subject: [PATCH 39/58] made requested changes from prior ticket and did some cleanup --- build/build.go | 10 +++++++--- compiler/linkname.go | 2 +- compiler/package.go | 2 +- compiler/sources/errorCollectingImporter.go | 2 +- compiler/sources/sources.go | 13 +++++++++---- {compiler => internal}/errorList/errorList.go | 0 tool.go | 2 +- 7 files changed, 20 insertions(+), 11 deletions(-) rename {compiler => internal}/errorList/errorList.go (100%) diff --git a/build/build.go b/build/build.go index c0be20fe6..4517979c0 100644 --- a/build/build.go +++ b/build/build.go @@ -27,9 +27,9 @@ import ( "github.com/fsnotify/fsnotify" "github.com/gopherjs/gopherjs/compiler" "github.com/gopherjs/gopherjs/compiler/astutil" - "github.com/gopherjs/gopherjs/compiler/errorList" "github.com/gopherjs/gopherjs/compiler/jsFile" "github.com/gopherjs/gopherjs/compiler/sources" + "github.com/gopherjs/gopherjs/internal/errorList" "github.com/gopherjs/gopherjs/internal/testmain" log "github.com/sirupsen/logrus" @@ -967,7 +967,7 @@ func (s *Session) loadTestPackage(pkg *PackageData) (*sources.Sources, error) { } // Import dependencies for the testmain package. - for _, importedPkgPath := range srcs.Imports() { + for _, importedPkgPath := range srcs.UnresolvedImports() { _, _, err := s.loadImportPathWithSrcDir(importedPkgPath, pkg.Dir) if err != nil { return nil, err @@ -1037,6 +1037,10 @@ var getExeModTime = func() func() time.Time { } }() +// loadPackages will recursively load and parse the given package and +// its dependencies. This will return the sources for the given package. +// The returned source and sources for the dependencies will be added +// to the session's sources map. func (s *Session) loadPackages(pkg *PackageData) (*sources.Sources, error) { if srcs, ok := s.sources[pkg.ImportPath]; ok { return srcs, nil @@ -1089,7 +1093,7 @@ func (s *Session) loadPackages(pkg *PackageData) (*sources.Sources, error) { // Import dependencies from the augmented files, // whilst skipping any that have been already imported. - for _, importedPkgPath := range srcs.Imports(pkg.Imports...) { + for _, importedPkgPath := range srcs.UnresolvedImports(pkg.Imports...) { _, _, err := s.loadImportPathWithSrcDir(importedPkgPath, pkg.Dir) if err != nil { return nil, err diff --git a/compiler/linkname.go b/compiler/linkname.go index 3a9b47934..af3ef6cc0 100644 --- a/compiler/linkname.go +++ b/compiler/linkname.go @@ -7,8 +7,8 @@ import ( "strings" "github.com/gopherjs/gopherjs/compiler/astutil" - "github.com/gopherjs/gopherjs/compiler/errorList" "github.com/gopherjs/gopherjs/compiler/internal/symbol" + "github.com/gopherjs/gopherjs/internal/errorList" ) // GoLinkname describes a go:linkname compiler directive found in the source code. diff --git a/compiler/package.go b/compiler/package.go index 2039758f0..239c9bf75 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -9,12 +9,12 @@ import ( "golang.org/x/tools/go/types/typeutil" - "github.com/gopherjs/gopherjs/compiler/errorList" "github.com/gopherjs/gopherjs/compiler/internal/analysis" "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/sources" "github.com/gopherjs/gopherjs/compiler/typesutil" + "github.com/gopherjs/gopherjs/internal/errorList" "github.com/gopherjs/gopherjs/internal/experiments" ) diff --git a/compiler/sources/errorCollectingImporter.go b/compiler/sources/errorCollectingImporter.go index bb9325230..73bd3c65d 100644 --- a/compiler/sources/errorCollectingImporter.go +++ b/compiler/sources/errorCollectingImporter.go @@ -3,7 +3,7 @@ package sources import ( "go/types" - "github.com/gopherjs/gopherjs/compiler/errorList" + "github.com/gopherjs/gopherjs/internal/errorList" ) // errorCollectingImporter implements go/types.Importer interface and diff --git a/compiler/sources/sources.go b/compiler/sources/sources.go index 92b38d966..fbcc98b95 100644 --- a/compiler/sources/sources.go +++ b/compiler/sources/sources.go @@ -7,8 +7,8 @@ import ( "sort" "strings" - "github.com/gopherjs/gopherjs/compiler/errorList" "github.com/gopherjs/gopherjs/compiler/jsFile" + "github.com/gopherjs/gopherjs/internal/errorList" "github.com/neelance/astrewrite" ) @@ -114,12 +114,17 @@ func (s Sources) TypeCheck(importer types.Importer, sizes types.Sizes, tContext return typesInfo, typesPkg, nil } -// Imports calculates the import paths of the package's dependencies +// UnresolvedImports calculates the import paths of the package's dependencies // based on all the imports in the augmented Go AST files. // -// The given skip paths will not be returned in the results. +// This is used to determine the unresolved imports that weren't in the +// PackageData.Imports slice since they were added during augmentation or +// during template generation. +// +// The given skip paths (typically those imports from PackageData.Imports) +// will not be returned in the results. // This will not return any `*_test` packages in the results. -func (s Sources) Imports(skip ...string) []string { +func (s Sources) UnresolvedImports(skip ...string) []string { seen := make(map[string]struct{}) for _, sk := range skip { seen[sk] = struct{}{} diff --git a/compiler/errorList/errorList.go b/internal/errorList/errorList.go similarity index 100% rename from compiler/errorList/errorList.go rename to internal/errorList/errorList.go diff --git a/tool.go b/tool.go index c1b63ffd7..46d6a6edc 100644 --- a/tool.go +++ b/tool.go @@ -26,7 +26,7 @@ import ( gbuild "github.com/gopherjs/gopherjs/build" "github.com/gopherjs/gopherjs/build/cache" "github.com/gopherjs/gopherjs/compiler" - "github.com/gopherjs/gopherjs/compiler/errorList" + "github.com/gopherjs/gopherjs/internal/errorList" "github.com/gopherjs/gopherjs/internal/sysutil" "github.com/neelance/sourcemap" log "github.com/sirupsen/logrus" From c8b8905d151fc4aa5bdfac288bb1b8c713f9d71b Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 27 Feb 2025 15:34:43 -0700 Subject: [PATCH 40/58] moved linkname to its own package --- compiler/compiler.go | 9 +++-- compiler/compiler_test.go | 3 +- compiler/{ => linkname}/linkname.go | 23 ++++++----- compiler/{ => linkname}/linkname_test.go | 4 +- compiler/package.go | 14 +------ compiler/sources/errorCollectingImporter.go | 23 ----------- compiler/sources/sources.go | 43 +++++++++++++++++---- compiler/utils.go | 5 --- 8 files changed, 60 insertions(+), 64 deletions(-) rename compiler/{ => linkname}/linkname.go (88%) rename compiler/{ => linkname}/linkname_test.go (98%) delete mode 100644 compiler/sources/errorCollectingImporter.go diff --git a/compiler/compiler.go b/compiler/compiler.go index 96ec390d8..e8264c946 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -18,6 +18,7 @@ import ( "time" "github.com/gopherjs/gopherjs/compiler/internal/dce" + "github.com/gopherjs/gopherjs/compiler/linkname" "github.com/gopherjs/gopherjs/compiler/prelude" "golang.org/x/tools/go/gcexportdata" ) @@ -59,7 +60,7 @@ type Archive struct { // Whether or not the package was compiled with minification enabled. Minified bool // A list of go:linkname directives encountered in the package. - GoLinknames []GoLinkname + GoLinknames []linkname.GoLinkname } func (a Archive) String() string { @@ -112,7 +113,7 @@ func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) err minify := mainPkg.Minified // Aggregate all go:linkname directives in the program together. - gls := goLinknameSet{} + gls := linkname.GoLinknameSet{} for _, pkg := range pkgs { gls.Add(pkg.GoLinknames) } @@ -164,7 +165,7 @@ func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) err return nil } -func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameSet, minify bool, w *SourceMapFilter) error { +func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls linkname.GoLinknameSet, minify bool, w *SourceMapFilter) error { if w.MappingCallback != nil && pkg.FileSet != nil { w.fileSet = pkg.FileSet } @@ -264,7 +265,7 @@ type serializableArchive struct { IncJSCode []byte FileSet []byte Minified bool - GoLinknames []GoLinkname + GoLinknames []linkname.GoLinkname BuildTime time.Time } diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 4c1dd4dba..0742cbaaa 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -12,6 +12,7 @@ import ( "golang.org/x/tools/go/packages" "github.com/gopherjs/gopherjs/compiler/internal/dce" + "github.com/gopherjs/gopherjs/compiler/linkname" "github.com/gopherjs/gopherjs/compiler/sources" "github.com/gopherjs/gopherjs/internal/srctesting" ) @@ -782,7 +783,7 @@ func renderPackage(t *testing.T, archive *Archive, minify bool) string { buf := &bytes.Buffer{} - if err := WritePkgCode(archive, selection, goLinknameSet{}, minify, &SourceMapFilter{Writer: buf}); err != nil { + if err := WritePkgCode(archive, selection, linkname.GoLinknameSet{}, minify, &SourceMapFilter{Writer: buf}); err != nil { t.Fatal(err) } diff --git a/compiler/linkname.go b/compiler/linkname/linkname.go similarity index 88% rename from compiler/linkname.go rename to compiler/linkname/linkname.go index af3ef6cc0..6c3a9623c 100644 --- a/compiler/linkname.go +++ b/compiler/linkname/linkname.go @@ -1,4 +1,4 @@ -package compiler +package linkname import ( "fmt" @@ -22,7 +22,7 @@ type GoLinkname struct { Reference symbol.Name } -// parseGoLinknames processed comments in a source file and extracts //go:linkname +// ParseGoLinknames processed comments in a source file and extracts //go:linkname // compiler directive from the comments. // // The following directive format is supported: @@ -38,7 +38,7 @@ type GoLinkname struct { // - The local function referenced by the directive must have no body (in other // words, it can only "import" an external function implementation into the // local scope). -func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]GoLinkname, error) { +func ParseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]GoLinkname, error) { var errs errorList.ErrorList = nil var directives []GoLinkname @@ -108,7 +108,7 @@ func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]Go for _, cg := range file.Comments { for _, c := range cg.List { if err := processComment(c); err != nil { - errs = append(errs, ErrorAt(err, fset, c.Pos())) + errs = append(errs, errorAt(err, fset, c.Pos())) } } } @@ -116,15 +116,20 @@ func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]Go return directives, errs.ErrOrNil() } -// goLinknameSet is a utility that enables quick lookup of whether a decl is +// 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) +} + +// GoLinknameSet is a utility that enables quick lookup of whether a decl is // affected by any go:linkname directive in the program. -type goLinknameSet struct { +type GoLinknameSet struct { byImplementation map[symbol.Name][]GoLinkname byReference map[symbol.Name]GoLinkname } // Add more GoLinkname directives into the set. -func (gls *goLinknameSet) Add(entries []GoLinkname) error { +func (gls *GoLinknameSet) Add(entries []GoLinkname) error { if gls.byImplementation == nil { gls.byImplementation = map[symbol.Name][]GoLinkname{} } @@ -144,7 +149,7 @@ func (gls *goLinknameSet) Add(entries []GoLinkname) error { // IsImplementation returns true if there is a directive referencing this symbol // as an implementation. -func (gls *goLinknameSet) IsImplementation(sym symbol.Name) bool { +func (gls *GoLinknameSet) IsImplementation(sym symbol.Name) bool { _, found := gls.byImplementation[sym] return found } @@ -152,7 +157,7 @@ func (gls *goLinknameSet) IsImplementation(sym symbol.Name) bool { // FindImplementation returns a symbol name, which provides the implementation // for the given symbol. The second value indicates whether the implementation // was found. -func (gls *goLinknameSet) FindImplementation(sym symbol.Name) (symbol.Name, bool) { +func (gls *GoLinknameSet) FindImplementation(sym symbol.Name) (symbol.Name, bool) { directive, found := gls.byReference[sym] return directive.Implementation, found } diff --git a/compiler/linkname_test.go b/compiler/linkname/linkname_test.go similarity index 98% rename from compiler/linkname_test.go rename to compiler/linkname/linkname_test.go index 7f46c6cfb..e2abc2825 100644 --- a/compiler/linkname_test.go +++ b/compiler/linkname/linkname_test.go @@ -1,4 +1,4 @@ -package compiler +package linkname import ( "go/ast" @@ -151,7 +151,7 @@ func TestParseGoLinknames(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { file, fset := parseSource(t, test.src) - directives, err := parseGoLinknames(fset, "testcase", file) + directives, err := ParseGoLinknames(fset, "testcase", file) if test.wantError != "" { if err == nil { diff --git a/compiler/package.go b/compiler/package.go index 239c9bf75..5345b5666 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -211,18 +211,6 @@ func (ic *ImportContext) Import(path string) (*types.Package, error) { return ic.Packages[a.ImportPath], nil } -// parseAllGoLinknames extracts all //go:linkname compiler directive from the sources. -func parseAllGoLinknames(s sources.Sources) ([]GoLinkname, error) { - goLinknames := []GoLinkname{} - var errs errorList.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() -} - // Compile the provided Go sources as a single package. // // Import path must be the absolute import path for a package. Provided sources @@ -256,7 +244,7 @@ func Compile(srcs sources.Sources, importContext *ImportContext, minify bool) (_ importContext.Packages[srcs.ImportPath] = typesPkg // Extract all go:linkname compiler directives from the package source. - goLinknames, err := parseAllGoLinknames(srcs) + goLinknames, err := srcs.ParseGoLinknames() if err != nil { return nil, err } diff --git a/compiler/sources/errorCollectingImporter.go b/compiler/sources/errorCollectingImporter.go deleted file mode 100644 index 73bd3c65d..000000000 --- a/compiler/sources/errorCollectingImporter.go +++ /dev/null @@ -1,23 +0,0 @@ -package sources - -import ( - "go/types" - - "github.com/gopherjs/gopherjs/internal/errorList" -) - -// errorCollectingImporter implements go/types.Importer interface and -// wraps it to collect import errors. -type errorCollectingImporter struct { - Importer types.Importer - Errors errorList.ErrorList -} - -func (ei *errorCollectingImporter) Import(path string) (*types.Package, error) { - pkg, err := ei.Importer.Import(path) - if err != nil { - ei.Errors = ei.Errors.AppendDistinct(err) - return nil, err - } - return pkg, nil -} diff --git a/compiler/sources/sources.go b/compiler/sources/sources.go index fbcc98b95..e66fa2243 100644 --- a/compiler/sources/sources.go +++ b/compiler/sources/sources.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/gopherjs/gopherjs/compiler/jsFile" + "github.com/gopherjs/gopherjs/compiler/linkname" "github.com/gopherjs/gopherjs/internal/errorList" "github.com/neelance/astrewrite" ) @@ -45,16 +46,11 @@ type Sources struct { // Note this function mutates the original slice. func (s Sources) Sort() Sources { sort.Slice(s.Files, func(i, j int) bool { - return s.nameOfFileAtIndex(i) > s.nameOfFileAtIndex(j) + return s.FileSet.File(s.Files[i].Pos()).Name() > s.FileSet.File(s.Files[j].Pos()).Name() }) return s } -// nameOfFileAtIndex gets the name of the Go source file at the given index. -func (s Sources) nameOfFileAtIndex(i int) string { - return s.FileSet.File(s.Files[i].Pos()).Name() -} - // Simplified returns a new sources instance with each Files entry processed by // astrewrite.Simplify. The JSFiles are copied unchanged. func (s Sources) Simplified(typesInfo *types.Info) Sources { @@ -88,7 +84,7 @@ func (s Sources) TypeCheck(importer types.Importer, sizes types.Sizes, tContext var typeErrs errorList.ErrorList - ecImporter := &errorCollectingImporter{Importer: importer} + ecImporter := &packageImporter{Importer: importer} config := &types.Config{ Context: tContext, @@ -114,6 +110,18 @@ func (s Sources) TypeCheck(importer types.Importer, sizes types.Sizes, tContext return typesInfo, typesPkg, nil } +// ParseGoLinknames extracts all //go:linkname compiler directive from the sources. +func (s Sources) ParseGoLinknames() ([]linkname.GoLinkname, error) { + goLinknames := []linkname.GoLinkname{} + var errs errorList.ErrorList + for _, file := range s.Files { + found, err := linkname.ParseGoLinknames(s.FileSet, s.ImportPath, file) + errs = errs.Append(err) + goLinknames = append(goLinknames, found...) + } + return goLinknames, errs.ErrOrNil() +} + // UnresolvedImports calculates the import paths of the package's dependencies // based on all the imports in the augmented Go AST files. // @@ -144,3 +152,24 @@ func (s Sources) UnresolvedImports(skip ...string) []string { sort.Strings(imports) return imports } + +// packageImporter implements go/types.Importer interface and +// wraps it to collect import errors. +type packageImporter struct { + Importer types.Importer + Errors errorList.ErrorList +} + +func (ei *packageImporter) Import(path string) (*types.Package, error) { + if path == "unsafe" { + return types.Unsafe, nil + } + + pkg, err := ei.Importer.Import(path) + if err != nil { + ei.Errors = ei.Errors.AppendDistinct(err) + return nil, err + } + + return pkg, nil +} diff --git a/compiler/utils.go b/compiler/utils.go index 5d2cb4629..83b826ce2 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -933,11 +933,6 @@ func formatJSStructTagVal(jsTag string) string { return "." + jsTag } -// 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) -} - // FatalError is an error compiler panics with when it encountered a fatal error. // // FatalError implements io.Writer, which can be used to record any free-form From 887f51e7643542feedceabf562e5b01637e6d892 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Wed, 5 Mar 2025 12:02:37 -0700 Subject: [PATCH 41/58] Updated to cross package analysis --- build/build.go | 97 ++++++++---- compiler/compiler_test.go | 186 +++++++++++++++++++----- compiler/decls.go | 2 +- compiler/internal/analysis/info.go | 62 +++++--- compiler/internal/analysis/info_test.go | 52 +++---- compiler/package.go | 161 +++++++++----------- compiler/sources/sources.go | 177 +++++++++++++++++----- internal/srctesting/srctesting.go | 4 +- 8 files changed, 509 insertions(+), 232 deletions(-) diff --git a/build/build.go b/build/build.go index 4517979c0..46786a30b 100644 --- a/build/build.go +++ b/build/build.go @@ -759,14 +759,13 @@ type Session struct { // sources is a map of parsed packages that have been built and augmented. // This is keyed using resolved import paths. This is used to avoid // rebuilding and augmenting packages that are imported by several packages. - // These sources haven't been sorted nor simplified yet. + // The files in these sources haven't been sorted nor simplified yet. sources map[string]*sources.Sources // Binary archives produced during the current session and assumed to be // up to date with input sources and dependencies. In the -w ("watch") mode // must be cleared upon entering watching. UpToDateArchives map[string]*compiler.Archive - Types map[string]*types.Package Watcher *fsnotify.Watcher } @@ -788,7 +787,6 @@ func NewSession(options *Options) (*Session, error) { return nil, err } - s.Types = make(map[string]*types.Package) if options.Watch { if out, err := exec.Command("ulimit", "-n").Output(); err == nil { if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil && n < 1024 { @@ -906,7 +904,7 @@ func (s *Session) BuildFiles(filenames []string, pkgObj string, cwd string) erro if err != nil { return err } - if s.Types["main"].Name() != "main" { + if s.sources["main"].Package.Name() != "main" { return fmt.Errorf("cannot build/run non-main package") } return s.WriteCommandPackage(archive, pkgObj) @@ -918,25 +916,38 @@ func (s *Session) BuildProject(pkg *PackageData) (*compiler.Archive, error) { // ensure that runtime for gopherjs is imported pkg.Imports = append(pkg.Imports, `runtime`) - // Build the project to get the sources for the parsed packages. - var srcs *sources.Sources + // Load the project to get the sources for the parsed packages. + var rootSrcs *sources.Sources var err error if pkg.IsTest { - srcs, err = s.loadTestPackage(pkg) + rootSrcs, err = s.loadTestPackage(pkg) } else { - srcs, err = s.loadPackages(pkg) + rootSrcs, err = s.loadPackages(pkg) } if err != nil { return nil, err } - // TODO(grantnelson-wf): At this point we have all the parsed packages we - // need to compile the whole project, including testmain, if needed. - // We can perform analysis on the whole project at this point to propagate - // flatten, blocking, etc. information and check types to get the type info - // with all the instances for all generics in the whole project. + // TODO(grantnelson-wf): We could investigate caching the results of + // the sources prior to preparing them to avoid re-parsing the same + // sources and augmenting them when the files on disk haven't changed. + // This would require a way to determine if the sources are up-to-date + // which could be done with the left over srcModTime from when the archives + // were being cached. - return s.compilePackages(srcs) + // Compile the project into Archives containing the generated JS. + return s.prepareAndCompilePackages(rootSrcs) +} + +// getSortedSources returns the sources sorted by import path. +// The files in the sources may still not be sorted yet. +func (s *Session) getSortedSources() []*sources.Sources { + allSources := make([]*sources.Sources, 0, len(s.sources)) + for _, srcs := range s.sources { + allSources = append(allSources, srcs) + } + sources.SortedSourcesSlice(allSources) + return allSources } func (s *Session) loadTestPackage(pkg *PackageData) (*sources.Sources, error) { @@ -965,6 +976,7 @@ func (s *Session) loadTestPackage(pkg *PackageData) (*sources.Sources, error) { Files: []*ast.File{mainFile}, FileSet: fset, } + s.sources[srcs.ImportPath] = srcs // Import dependencies for the testmain package. for _, importedPkgPath := range srcs.UnresolvedImports() { @@ -1103,16 +1115,37 @@ func (s *Session) loadPackages(pkg *PackageData) (*sources.Sources, error) { return srcs, nil } -func (s *Session) compilePackages(srcs *sources.Sources) (*compiler.Archive, error) { +func (s *Session) prepareAndCompilePackages(rootSrcs *sources.Sources) (*compiler.Archive, error) { + tContext := types.NewContext() + allSources := s.getSortedSources() + + // Prepare and analyze the source code. + // This will be performed recursively for all dependencies. + if err := compiler.PrepareAllSources(allSources, s.SourcesForImport, tContext); err != nil { + return nil, err + } + + // Compile all the sources into archives. + for _, srcs := range allSources { + if _, err := s.compilePackage(srcs, tContext); err != nil { + return nil, err + } + } + + rootArchive, ok := s.UpToDateArchives[rootSrcs.ImportPath] + if !ok { + // This is confirmation that the root package is in the sources map and got compiled. + return nil, fmt.Errorf(`root package %q was not found in archives`, rootSrcs.ImportPath) + } + return rootArchive, nil +} + +func (s *Session) compilePackage(srcs *sources.Sources, tContext *types.Context) (*compiler.Archive, error) { if archive, ok := s.UpToDateArchives[srcs.ImportPath]; ok { return archive, nil } - importContext := &compiler.ImportContext{ - Packages: s.Types, - ImportArchive: s.ImportResolverFor(srcs.Dir), - } - archive, err := compiler.Compile(*srcs, importContext, s.options.Minify) + archive, err := compiler.Compile(srcs, tContext, s.options.Minify) if err != nil { return nil, err } @@ -1152,6 +1185,20 @@ func (s *Session) getImportPath(path, srcDir string) (string, error) { return pkg.ImportPath, nil } +func (s *Session) SourcesForImport(path, srcDir string) (*sources.Sources, error) { + importPath, err := s.getImportPath(path, srcDir) + if err != nil { + return nil, err + } + + srcs, ok := s.sources[importPath] + if !ok { + return nil, fmt.Errorf(`sources for %q not found`, path) + } + + return srcs, nil +} + // ImportResolverFor returns a function which returns a compiled package archive // given an import path. func (s *Session) ImportResolverFor(srcDir string) func(string) (*compiler.Archive, error) { @@ -1165,12 +1212,7 @@ func (s *Session) ImportResolverFor(srcDir string) func(string) (*compiler.Archi return archive, nil } - // The archive hasn't been compiled yet so compile it with the sources. - if srcs, ok := s.sources[importPath]; ok { - return s.compilePackages(srcs) - } - - return nil, fmt.Errorf(`sources for %q not found`, importPath) + return nil, fmt.Errorf(`archive for %q not found`, importPath) } } @@ -1258,8 +1300,9 @@ func hasGopathPrefix(file, gopath string) (hasGopathPrefix bool, prefixLen int) func (s *Session) WaitForChange() { // Will need to re-validate up-to-dateness of all archives, so flush them from // memory. + s.importPaths = map[string]map[string]string{} + s.sources = map[string]*sources.Sources{} s.UpToDateArchives = map[string]*compiler.Archive{} - s.Types = map[string]*types.Package{} s.options.PrintSuccess("watching for changes...\n") for { diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 0742cbaaa..e7add0015 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -613,6 +613,115 @@ func TestDeclNaming_VarsAndTypes(t *testing.T) { ) } +func Test_CrossPackageAnalysis(t *testing.T) { + src1 := ` + package main + import "github.com/gopherjs/gopherjs/compiler/stable" + + func main() { + m := map[string]int{ + "one": 1, + "two": 2, + "three": 3, + } + stable.Print(m) + }` + src2 := ` + package collections + import "github.com/gopherjs/gopherjs/compiler/cmp" + + func Keys[K cmp.Ordered, V any, M ~map[K]V](m M) []K { + keys := make([]K, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + return keys + }` + src3 := ` + package collections + import "github.com/gopherjs/gopherjs/compiler/cmp" + + func Values[K cmp.Ordered, V any, M ~map[K]V](m M) []V { + values := make([]V, 0, len(m)) + for _, v := range m { + values = append(values, v) + } + return values + }` + src4 := ` + package sorts + import "github.com/gopherjs/gopherjs/compiler/cmp" + + func Pair[K cmp.Ordered, V any, SK ~[]K, SV ~[]V](k SK, v SV) { + Bubble(len(k), + func(i, j int) bool { return k[i] < k[j] }, + func(i, j int) { k[i], v[i], k[j], v[j] = k[j], v[j], k[i], v[i] }) + } + + func Bubble(length int, less func(i, j int) bool, swap func(i, j int)) { + for i := 0; i < length; i++ { + for j := i + 1; j < length; j++ { + if less(j, i) { + swap(i, j) + } + } + } + }` + src5 := ` + package stable + import ( + "github.com/gopherjs/gopherjs/compiler/collections" + "github.com/gopherjs/gopherjs/compiler/sorts" + "github.com/gopherjs/gopherjs/compiler/cmp" + ) + + func Print[K cmp.Ordered, V any, M ~map[K]V](m M) { + keys := collections.Keys(m) + values := collections.Values(m) + sorts.Pair(keys, values) + for i, k := range keys { + println(i, k, values[i]) + } + }` + src6 := ` + package cmp + type Ordered interface { ~int | ~uint | ~float64 | ~string }` + + root := srctesting.ParseSources(t, + []srctesting.Source{ + {Name: `main.go`, Contents: []byte(src1)}, + }, + []srctesting.Source{ + {Name: `collections/keys.go`, Contents: []byte(src2)}, + {Name: `collections/values.go`, Contents: []byte(src3)}, + {Name: `sorts/sorts.go`, Contents: []byte(src4)}, + {Name: `stable/print.go`, Contents: []byte(src5)}, + {Name: `cmp/ordered.go`, Contents: []byte(src6)}, + }) + + archives := compileProject(t, root, false) + checkForDeclFullNames(t, archives, + // collections + `funcVar:github.com/gopherjs/gopherjs/compiler/collections.Values`, + `func:github.com/gopherjs/gopherjs/compiler/collections.Values`, + `funcVar:github.com/gopherjs/gopherjs/compiler/collections.Keys`, + `func:github.com/gopherjs/gopherjs/compiler/collections.Keys`, + + // sorts + `funcVar:github.com/gopherjs/gopherjs/compiler/sorts.Pair`, + `func:github.com/gopherjs/gopherjs/compiler/sorts.Pair`, + `funcVar:github.com/gopherjs/gopherjs/compiler/sorts.Bubble`, + `func:github.com/gopherjs/gopherjs/compiler/sorts.Bubble`, + + // stable + `funcVar:github.com/gopherjs/gopherjs/compiler/stable.Print`, + `func:github.com/gopherjs/gopherjs/compiler/stable.Print`, + + // main + `init:main`, + ) +} + func TestArchiveSelectionAfterSerialization(t *testing.T) { src := ` package main @@ -679,43 +788,43 @@ func compileProject(t *testing.T, root *packages.Package, minify bool) map[strin pkgMap[pkg.PkgPath] = pkg }) - archiveCache := map[string]*Archive{} - var importContext *ImportContext - importContext = &ImportContext{ - Packages: map[string]*types.Package{}, - ImportArchive: func(path string) (*Archive, error) { - // find in local cache - if a, ok := archiveCache[path]; ok { - return a, nil - } - - pkg, ok := pkgMap[path] - if !ok { - t.Fatal(`package not found:`, path) - } - importContext.Packages[path] = pkg.Types - - srcs := sources.Sources{ - ImportPath: path, - Files: pkg.Syntax, - FileSet: pkg.Fset, - } + allSrcs := map[string]*sources.Sources{} + for _, pkg := range pkgMap { + srcs := &sources.Sources{ + ImportPath: pkg.PkgPath, + Dir: ``, + Files: pkg.Syntax, + FileSet: pkg.Fset, + } + allSrcs[pkg.PkgPath] = srcs + } - // compile package - a, err := Compile(srcs, importContext, minify) - if err != nil { - return nil, err - } - archiveCache[path] = a - return a, nil - }, + importer := func(path, srcDir string) (*sources.Sources, error) { + srcs, ok := allSrcs[path] + if !ok { + t.Fatal(`package not found:`, path) + return nil, nil + } + return srcs, nil } - _, err := importContext.ImportArchive(root.PkgPath) - if err != nil { - t.Fatal(`failed to compile:`, err) + tContext := types.NewContext() + sortedSources := make([]*sources.Sources, 0, len(allSrcs)) + for _, srcs := range allSrcs { + sortedSources = append(sortedSources, srcs) } - return archiveCache + sources.SortedSourcesSlice(sortedSources) + PrepareAllSources(sortedSources, importer, tContext) + + archives := map[string]*Archive{} + for _, srcs := range allSrcs { + a, err := Compile(srcs, tContext, minify) + if err != nil { + t.Fatal(`failed to compile:`, err) + } + archives[srcs.ImportPath] = a + } + return archives } // newTime creates an arbitrary time.Time offset by the given number of seconds. @@ -730,6 +839,13 @@ func newTime(seconds float64) time.Time { func reloadCompiledProject(t *testing.T, archives map[string]*Archive, rootPkgPath string) map[string]*Archive { t.Helper() + // TODO(grantnelson-wf): The tests using this function are out-of-date + // since they are testing the old archive caching that has been disabled. + // At some point, these tests should be updated to test any new caching + // mechanism that is implemented or removed. As is this function is faking + // the old recursive archive loading that is no longer used since it + // doesn't allow cross package analysis for generings. + buildTime := newTime(5.0) serialized := map[string][]byte{} for path, a := range archives { @@ -742,6 +858,10 @@ func reloadCompiledProject(t *testing.T, archives map[string]*Archive, rootPkgPa srcModTime := newTime(0.0) reloadCache := map[string]*Archive{} + type ImportContext struct { + Packages map[string]*types.Package + ImportArchive func(path string) (*Archive, error) + } var importContext *ImportContext importContext = &ImportContext{ Packages: map[string]*types.Package{}, diff --git a/compiler/decls.go b/compiler/decls.go index 0694181f6..5b760fb15 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -82,7 +82,7 @@ func (d *Decl) Dce() *dce.Info { // topLevelObjects extracts package-level variables, functions and named types // from the package AST. -func (fc *funcContext) topLevelObjects(srcs sources.Sources) (vars []*types.Var, functions []*ast.FuncDecl, typeNames typesutil.TypeNames) { +func (fc *funcContext) topLevelObjects(srcs *sources.Sources) (vars []*types.Var, functions []*ast.FuncDecl, typeNames typesutil.TypeNames) { if !fc.isRoot() { panic(bailout(fmt.Errorf("functionContext.discoverObjects() must be only called on the package-level context"))) } diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go index 803952b24..d05f9a6d1 100644 --- a/compiler/internal/analysis/info.go +++ b/compiler/internal/analysis/info.go @@ -52,16 +52,20 @@ type Info struct { *types.Info Pkg *types.Package typeCtx *types.Context - instanceSets *typeparams.PackageInstanceSets + InstanceSets *typeparams.PackageInstanceSets HasPointer map[*types.Var]bool funcInstInfos *typeparams.InstanceMap[*FuncInfo] funcLitInfos map[*ast.FuncLit][]*FuncInfo InitFuncInfo *FuncInfo // Context for package variable initialization. - isImportedBlocking func(typeparams.Instance) bool // For functions from other packages. - allInfos []*FuncInfo + infoImporter InfoImporter // To get `Info` for other packages. + allInfos []*FuncInfo } +// InfoImporter is used to get the `Info` for another package. +// The path is the resolved import path of the package to get the `Info` for. +type InfoImporter func(path string) (*Info, error) + func (info *Info) newFuncInfo(n ast.Node, obj types.Object, typeArgs typesutil.TypeList, resolver *typeparams.Resolver) *FuncInfo { funcInfo := &FuncInfo{ pkgInfo: info, @@ -106,7 +110,7 @@ func (info *Info) newFuncInfo(n ast.Node, obj types.Object, typeArgs typesutil.T func (info *Info) newFuncInfoInstances(fd *ast.FuncDecl) []*FuncInfo { obj := info.Defs[fd.Name] - instances := info.instanceSets.Pkg(info.Pkg).ForObj(obj) + instances := info.InstanceSets.Pkg(info.Pkg).ForObj(obj) if len(instances) == 0 { if typeparams.HasTypeParams(obj.Type()) { // This is a generic function, but no instances were found, @@ -132,11 +136,16 @@ func (info *Info) newFuncInfoInstances(fd *ast.FuncDecl) []*FuncInfo { } // IsBlocking returns true if the function may contain blocking calls or operations. -// If inst is from a different package, this will use the isImportedBlocking +// If inst is from a different package, this will use the getImportInfo function // to lookup the information from the other package. func (info *Info) IsBlocking(inst typeparams.Instance) bool { if inst.Object.Pkg() != info.Pkg { - return info.isImportedBlocking(inst) + path := inst.Object.Pkg().Path() + otherInfo, err := info.infoImporter(path) + if err != nil { + panic(fmt.Errorf(`failed to get info for package %q: %v`, path, err)) + } + return otherInfo.IsBlocking(inst) } if funInfo := info.FuncInfo(inst); funInfo != nil { return funInfo.IsBlocking() @@ -174,16 +183,21 @@ func (info *Info) VarsWithInitializers() map[*types.Var]bool { return result } -func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info, typeCtx *types.Context, typesPkg *types.Package, instanceSets *typeparams.PackageInstanceSets, isBlocking func(typeparams.Instance) bool) *Info { +// AnalyzePkg analyzes the given package for blocking calls, defers, etc. +// +// Note that at the end of this call the analysis information +// has NOT been propagated across packages yet. Once all the packages +// have been analyzed, call PropagateAnalysis to propagate the information. +func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info, typeCtx *types.Context, typesPkg *types.Package, instanceSets *typeparams.PackageInstanceSets, infoImporter InfoImporter) *Info { info := &Info{ - Info: typesInfo, - Pkg: typesPkg, - typeCtx: typeCtx, - instanceSets: instanceSets, - HasPointer: make(map[*types.Var]bool), - isImportedBlocking: isBlocking, - funcInstInfos: new(typeparams.InstanceMap[*FuncInfo]), - funcLitInfos: make(map[*ast.FuncLit][]*FuncInfo), + Info: typesInfo, + Pkg: typesPkg, + typeCtx: typeCtx, + InstanceSets: instanceSets, + HasPointer: make(map[*types.Var]bool), + infoImporter: infoImporter, + funcInstInfos: new(typeparams.InstanceMap[*FuncInfo]), + funcLitInfos: make(map[*ast.FuncLit][]*FuncInfo), } info.InitFuncInfo = info.newFuncInfo(nil, nil, nil, nil) @@ -193,13 +207,25 @@ func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info ast.Walk(info.InitFuncInfo, file) } + return info +} + +// PropagateAnalysis will propagate analysis information across package +// boundaries to finish the analysis of a whole project. +func PropagateAnalysis(allInfo []*Info) { done := false for !done { - done = info.propagateFunctionBlocking() + done = true + for _, info := range allInfo { + if !info.propagateFunctionBlocking() { + done = false + } + } } - info.propagateControlStatementBlocking() - return info + for _, info := range allInfo { + info.propagateControlStatementBlocking() + } } // propagateFunctionBlocking propagates information about blocking calls diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go index 957e346d9..73428207e 100644 --- a/compiler/internal/analysis/info_test.go +++ b/compiler/internal/analysis/info_test.go @@ -1,6 +1,7 @@ package analysis import ( + "fmt" "go/ast" "go/types" "sort" @@ -1575,15 +1576,13 @@ func TestBlocking_IsImportBlocking_ForwardInstances(t *testing.T) { } func TestBlocking_IsImportBlocking_BackwardInstances(t *testing.T) { - t.Skip(`isImportedBlocking doesn't fully handle instances yet`) - // TODO(grantnelson-wf): This test is currently failing because the info - // for the test package is need while creating the instances for FooBaz - // while analyzing the other package. However the other package is analyzed - // first since the test package is dependent on it. One possible fix is that - // we add some mechanism similar to the localInstCallees but for remote - // instances then perform the blocking propagation steps for all packages - // including the localInstCallees propagation at the same time. After all the - // propagation of the calls then the flow control statements can be marked. + // This tests propagation of information across package boundaries. + // `FooBaz` has no instances in it until it is referenced in the `test` package. + // That instance information needs to propagate back across the package + // boundary to the `other` package. The information for `BazBlocker` and + // `BazNotBlocker` is propagated back to `FooBaz[BazBlocker]` and + // `FooBaz[BazNotBlocker]`. That information is then propagated forward + // to the `blocking` and `notBlocking` functions in the `test` package. otherSrc := `package other @@ -1629,8 +1628,9 @@ type blockingTest struct { func newBlockingTest(t *testing.T, src string) *blockingTest { f := srctesting.New(t) + tContext := types.NewContext() tc := typeparams.Collector{ - TContext: types.NewContext(), + TContext: tContext, Info: f.Info, Instances: &typeparams.PackageInstanceSets{}, } @@ -1639,11 +1639,11 @@ func newBlockingTest(t *testing.T, src string) *blockingTest { testInfo, testPkg := f.Check(`pkg/test`, file) tc.Scan(testPkg, file) - isImportBlocking := func(i typeparams.Instance) bool { - t.Fatalf(`isImportBlocking should not be called in this test, called with %v`, i) - return true + getImportInfo := func(path string) (*Info, error) { + return nil, fmt.Errorf(`getImportInfo should not be called in this test, called with %v`, path) } - pkgInfo := AnalyzePkg([]*ast.File{file}, f.FileSet, testInfo, types.NewContext(), testPkg, tc.Instances, isImportBlocking) + pkgInfo := AnalyzePkg([]*ast.File{file}, f.FileSet, testInfo, tContext, testPkg, tc.Instances, getImportInfo) + PropagateAnalysis([]*Info{pkgInfo}) return &blockingTest{ f: f, @@ -1654,19 +1654,19 @@ func newBlockingTest(t *testing.T, src string) *blockingTest { func newBlockingTestWithOtherPackage(t *testing.T, testSrc string, otherSrc string) *blockingTest { f := srctesting.New(t) + tContext := types.NewContext() tc := typeparams.Collector{ - TContext: types.NewContext(), + TContext: tContext, Info: f.Info, Instances: &typeparams.PackageInstanceSets{}, } - pkgInfo := map[*types.Package]*Info{} - isImportBlocking := func(i typeparams.Instance) bool { - if info, ok := pkgInfo[i.Object.Pkg()]; ok { - return info.IsBlocking(i) + pkgInfo := map[string]*Info{} + getImportInfo := func(path string) (*Info, error) { + if info, ok := pkgInfo[path]; ok { + return info, nil } - t.Fatalf(`unexpected package in isImportBlocking for %v`, i) - return true + return nil, fmt.Errorf(`unexpected package in getImportInfo for %v`, path) } otherFile := f.Parse(`other.go`, otherSrc) @@ -1677,11 +1677,13 @@ func newBlockingTestWithOtherPackage(t *testing.T, testSrc string, otherSrc stri _, testPkg := f.Check(`pkg/test`, testFile) tc.Scan(testPkg, testFile) - otherPkgInfo := AnalyzePkg([]*ast.File{otherFile}, f.FileSet, f.Info, types.NewContext(), otherPkg, tc.Instances, isImportBlocking) - pkgInfo[otherPkg] = otherPkgInfo + otherPkgInfo := AnalyzePkg([]*ast.File{otherFile}, f.FileSet, f.Info, tContext, otherPkg, tc.Instances, getImportInfo) + pkgInfo[otherPkg.Path()] = otherPkgInfo - testPkgInfo := AnalyzePkg([]*ast.File{testFile}, f.FileSet, f.Info, types.NewContext(), testPkg, tc.Instances, isImportBlocking) - pkgInfo[testPkg] = testPkgInfo + testPkgInfo := AnalyzePkg([]*ast.File{testFile}, f.FileSet, f.Info, tContext, testPkg, tc.Instances, getImportInfo) + pkgInfo[testPkg.Path()] = testPkgInfo + + PropagateAnalysis([]*Info{otherPkgInfo, testPkgInfo}) return &blockingTest{ f: f, diff --git a/compiler/package.go b/compiler/package.go index 5345b5666..8f336130d 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -15,7 +15,6 @@ import ( "github.com/gopherjs/gopherjs/compiler/sources" "github.com/gopherjs/gopherjs/compiler/typesutil" "github.com/gopherjs/gopherjs/internal/errorList" - "github.com/gopherjs/gopherjs/internal/experiments" ) // pkgContext maintains compiler context for a specific package. @@ -117,18 +116,11 @@ type funcContext struct { funcLitCounter int } -func newRootCtx(tContext *types.Context, srcs sources.Sources, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(typeparams.Instance) bool, minify bool) *funcContext { - tc := typeparams.Collector{ - TContext: tContext, - Info: typesInfo, - Instances: &typeparams.PackageInstanceSets{}, - } - tc.Scan(typesPkg, srcs.Files...) - pkgInfo := analysis.AnalyzePkg(srcs.Files, srcs.FileSet, typesInfo, tContext, typesPkg, tc.Instances, isBlocking) +func newRootCtx(tContext *types.Context, srcs *sources.Sources, minify bool) *funcContext { funcCtx := &funcContext{ - FuncInfo: pkgInfo.InitFuncInfo, + FuncInfo: srcs.TypeInfo.InitFuncInfo, pkgCtx: &pkgContext{ - Info: pkgInfo, + Info: srcs.TypeInfo, additionalSelections: make(map[*ast.SelectorExpr]typesutil.Selection), typesCtx: tContext, @@ -138,7 +130,7 @@ func newRootCtx(tContext *types.Context, srcs sources.Sources, typesInfo *types. indentation: 1, minify: minify, fileSet: srcs.FileSet, - instanceSet: tc.Instances, + instanceSet: srcs.TypeInfo.InstanceSets, }, allVars: make(map[string]int), flowDatas: map[*types.Label]*flowData{nil: {}}, @@ -158,64 +150,11 @@ 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 - // ImportArchive 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. - ImportArchive func(importPath string) (*Archive, error) -} - -// 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(inst typeparams.Instance) bool { - f, ok := inst.Object.(*types.Func) - if !ok { - panic(bailout(fmt.Errorf("can't determine if instance %v is blocking: instance isn't for a function object", inst))) - } - - archive, err := ic.ImportArchive(f.Pkg().Path()) - if err != nil { - panic(err) - } - - fullName := funcDeclFullName(inst) - for _, d := range archive.Declarations { - if d.FullName == fullName { - return d.Blocking - } - } - panic(bailout(fmt.Errorf("can't determine if function %s is blocking: decl not found in package archive", fullName))) -} - -// Import implements go/types.Importer interface for ImportContext. -func (ic *ImportContext) Import(path string) (*types.Package, error) { - if path == "unsafe" { - return types.Unsafe, nil - } - - // By importing the archive, the package will compile if it hasn't been - // compiled yet and the package will be added to the Packages map. - a, err := ic.ImportArchive(path) - if err != nil { - return nil, err - } - - return ic.Packages[a.ImportPath], nil -} - // 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(srcs sources.Sources, importContext *ImportContext, minify bool) (_ *Archive, err error) { +// Provided sources must be prepared so that the type information has been determined, +// and the source files have been sorted by name to ensure reproducible JavaScript output. +func Compile(srcs *sources.Sources, tContext *types.Context, minify bool) (_ *Archive, err error) { defer func() { e := recover() if e == nil { @@ -231,27 +170,7 @@ func Compile(srcs sources.Sources, importContext *ImportContext, minify bool) (_ err = bailout(fmt.Errorf("unexpected compiler panic while building package %q: %v", srcs.ImportPath, e)) }() - srcs.Sort() - - tContext := types.NewContext() - typesInfo, typesPkg, err := srcs.TypeCheck(importContext, sizes32, tContext) - if err != nil { - return nil, err - } - if genErr := typeparams.RequiresGenericsSupport(typesInfo); genErr != nil && !experiments.Env.Generics { - return nil, fmt.Errorf("package %s requires generics support (https://github.com/gopherjs/gopherjs/issues/1013): %w", srcs.ImportPath, genErr) - } - importContext.Packages[srcs.ImportPath] = typesPkg - - // Extract all go:linkname compiler directives from the package source. - goLinknames, err := srcs.ParseGoLinknames() - if err != nil { - return nil, err - } - - srcs = srcs.Simplified(typesInfo) - - rootCtx := newRootCtx(tContext, srcs, typesInfo, typesPkg, importContext.isBlocking, minify) + rootCtx := newRootCtx(tContext, srcs, minify) importedPaths, importDecls := rootCtx.importDecls() @@ -295,16 +214,74 @@ func Compile(srcs sources.Sources, importContext *ImportContext, minify bool) (_ return &Archive{ ImportPath: srcs.ImportPath, - Name: typesPkg.Name(), + Name: srcs.Package.Name(), Imports: importedPaths, - Package: typesPkg, + Package: srcs.Package, Declarations: allDecls, FileSet: srcs.FileSet, Minified: minify, - GoLinknames: goLinknames, + GoLinknames: srcs.GoLinknames, }, nil } +// PrepareAllSources prepares all sources for compilation by +// parsing go linknames, type checking, sorting, simplifying, and +// performing cross package analysis. +// The results are stored in the provided sources. +// +// All sources must be given at the same time for cross package analysis to +// work correctly. For consistency, the sources should be sorted by import path. +func PrepareAllSources(allSources []*sources.Sources, importer sources.Importer, tContext *types.Context) error { + // Sort the files by name in each source to ensure consistent order of processing. + for _, srcs := range allSources { + srcs.Sort() + } + + // This will be performed recursively for all dependencies + // to get the packages for the sources. + // Since some packages might not be recursively reached via the root sources, + // e.g. runtime, we need to try to TypeCheck all of them here. + // Any sources that have already been type checked will no-op. + for _, srcs := range allSources { + if err := srcs.TypeCheck(importer, sizes32, tContext); err != nil { + return err + } + } + + // Extract all go:linkname compiler directives from the package source. + for _, srcs := range allSources { + if err := srcs.ParseGoLinknames(); err != nil { + return err + } + } + + // Simply the source files. + for _, srcs := range allSources { + srcs.Simplify() + } + + // Collect all the generic type instances from all the packages. + // This must be done for all sources prior to any analysis. + instances := &typeparams.PackageInstanceSets{} + for _, srcs := range allSources { + srcs.CollectInstances(tContext, instances) + } + + // Analyze the package to determine type parameters instances, blocking, + // and other type information. This will not populate the information. + for _, srcs := range allSources { + srcs.Analyze(importer, tContext, instances) + } + + // Propagate the analysis information across all packages. + allInfo := make([]*analysis.Info, len(allSources)) + for i, src := range allSources { + allInfo[i] = src.TypeInfo + } + analysis.PropagateAnalysis(allInfo) + return nil +} + func (fc *funcContext) initArgs(ty types.Type) string { switch t := ty.(type) { case *types.Array: diff --git a/compiler/sources/sources.go b/compiler/sources/sources.go index e66fa2243..8e2d12946 100644 --- a/compiler/sources/sources.go +++ b/compiler/sources/sources.go @@ -1,16 +1,21 @@ package sources import ( + "fmt" "go/ast" "go/token" "go/types" "sort" "strings" + "github.com/neelance/astrewrite" + + "github.com/gopherjs/gopherjs/compiler/internal/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/jsFile" "github.com/gopherjs/gopherjs/compiler/linkname" "github.com/gopherjs/gopherjs/internal/errorList" - "github.com/neelance/astrewrite" + "github.com/gopherjs/gopherjs/internal/experiments" ) // Sources is a slice of parsed Go sources and additional data for a package. @@ -38,38 +43,67 @@ type Sources struct { // JSFiles is the JavaScript files that are part of the package. JSFiles []jsFile.JSFile + + // TypeInfo is the type information this package. + // This is nil until set by Analyze. + TypeInfo *analysis.Info + + // baseInfo is the base type information this package. + // This is nil until set by TypeCheck. + baseInfo *types.Info + + // Package is the types package for these source files. + // This is nil until set by TypeCheck. + Package *types.Package + + // GoLinknames is the set of Go linknames for this package. + // This is nil until set by ParseGoLinknames. + GoLinknames []linkname.GoLinkname } -// Sort the Go files slice by the original source name to ensure consistent order +type Importer func(path, srcDir string) (*Sources, error) + +// sort the Go 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 { +// Note this function mutates the original Files slice. +func (s *Sources) Sort() { 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.getFileName(s.Files[i]) > s.getFileName(s.Files[j]) }) - return s } -// Simplified returns a new sources instance with each Files entry processed by -// astrewrite.Simplify. The JSFiles are copied unchanged. -func (s Sources) Simplified(typesInfo *types.Info) Sources { - simplified := Sources{ - ImportPath: s.ImportPath, - Dir: s.Dir, - Files: make([]*ast.File, len(s.Files)), - FileSet: s.FileSet, - JSFiles: s.JSFiles, - } +func (s *Sources) getFileName(file *ast.File) string { + return s.FileSet.File(file.Pos()).Name() +} + +// Simplify processed each Files entry with astrewrite.Simplify. +// +// Note this function mutates the original Files slice. +// This must be called after TypeCheck and before analyze since +// this will change the pointers in the AST. For example, the pointers +// to function literals will change, making it impossible to find them +// in the type information, if analyze is called first. +func (s *Sources) Simplify() { for i, file := range s.Files { - simplified.Files[i] = astrewrite.Simplify(file, typesInfo, false) + s.Files[i] = astrewrite.Simplify(file, s.baseInfo, false) } - return simplified } // TypeCheck the sources. Returns information about declared package types and // type information for the supplied AST. -func (s Sources) TypeCheck(importer types.Importer, sizes types.Sizes, tContext *types.Context) (*types.Info, *types.Package, error) { +// This will set the Package field on the Sources. +// +// If the Package field is not nil, e.g. this function has already been run, +// this will be a no-op. +// +// This must be called prior to simplify to get the types.Info used by simplify. +func (s *Sources) TypeCheck(importer Importer, sizes types.Sizes, tContext *types.Context) error { + if s.Package != nil && s.baseInfo != nil { + // type checking has already been done so return early. + return nil + } + const errLimit = 10 // Max number of type checking errors to return. typesInfo := &types.Info{ @@ -84,11 +118,16 @@ func (s Sources) TypeCheck(importer types.Importer, sizes types.Sizes, tContext var typeErrs errorList.ErrorList - ecImporter := &packageImporter{Importer: importer} + pkgImporter := &packageImporter{ + srcDir: s.Dir, + importer: importer, + sizes: sizes, + tContext: tContext, + } config := &types.Config{ Context: tContext, - Importer: ecImporter, + Importer: pkgImporter, Sizes: sizes, Error: func(err error) { typeErrs = typeErrs.AppendDistinct(err) }, } @@ -96,22 +135,68 @@ func (s Sources) TypeCheck(importer types.Importer, sizes types.Sizes, tContext // 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 ecImporter.Errors.ErrOrNil() != nil { - return nil, nil, ecImporter.Errors.Trim(errLimit).ErrOrNil() + if pkgImporter.Errors.ErrOrNil() != nil { + return pkgImporter.Errors.Trim(errLimit).ErrOrNil() } // Return any other type errors. if typeErrs.ErrOrNil() != nil { - return nil, nil, typeErrs.Trim(errLimit).ErrOrNil() + return typeErrs.Trim(errLimit).ErrOrNil() } // Any general errors that may have occurred during type checking. if err != nil { - return nil, nil, err + return err + } + + // If generics are not enabled, ensure the package does not requires generics support. + if !experiments.Env.Generics { + if genErr := typeparams.RequiresGenericsSupport(typesInfo); genErr != nil { + return fmt.Errorf("some packages requires generics support (https://github.com/gopherjs/gopherjs/issues/1013): %w", genErr) + } } - return typesInfo, typesPkg, nil + + s.baseInfo = typesInfo + s.Package = typesPkg + return nil +} + +// CollectInstances will determine the type parameters instances for the package. +// +// This must be called before Analyze to have the type parameters instances +// needed during analysis. +func (s *Sources) CollectInstances(tContext *types.Context, instances *typeparams.PackageInstanceSets) { + tc := typeparams.Collector{ + TContext: tContext, + Info: s.baseInfo, + Instances: instances, + } + tc.Scan(s.Package, s.Files...) +} + +// Analyze will determine the type parameters instances, blocking, +// and other type information for the package. +// This will set the TypeInfo and Instances fields on the Sources. +// +// This must be called after to simplify to ensure the pointers +// in the AST are still valid. +// The instances must be collected prior to this call. +// +// Note that at the end of this call the analysis information +// has NOT been propagated across packages yet. +func (s *Sources) Analyze(importer Importer, tContext *types.Context, instances *typeparams.PackageInstanceSets) { + infoImporter := func(path string) (*analysis.Info, error) { + srcs, err := importer(path, s.Dir) + if err != nil { + return nil, err + } + return srcs.TypeInfo, nil + } + s.TypeInfo = analysis.AnalyzePkg(s.Files, s.FileSet, s.baseInfo, tContext, s.Package, instances, infoImporter) } // ParseGoLinknames extracts all //go:linkname compiler directive from the sources. -func (s Sources) ParseGoLinknames() ([]linkname.GoLinkname, error) { +// +// This will set the GoLinknames field on the Sources. +func (s *Sources) ParseGoLinknames() error { goLinknames := []linkname.GoLinkname{} var errs errorList.ErrorList for _, file := range s.Files { @@ -119,7 +204,11 @@ func (s Sources) ParseGoLinknames() ([]linkname.GoLinkname, error) { errs = errs.Append(err) goLinknames = append(goLinknames, found...) } - return goLinknames, errs.ErrOrNil() + if err := errs.ErrOrNil(); err != nil { + return err + } + s.GoLinknames = goLinknames + return nil } // UnresolvedImports calculates the import paths of the package's dependencies @@ -132,7 +221,7 @@ func (s Sources) ParseGoLinknames() ([]linkname.GoLinkname, error) { // The given skip paths (typically those imports from PackageData.Imports) // will not be returned in the results. // This will not return any `*_test` packages in the results. -func (s Sources) UnresolvedImports(skip ...string) []string { +func (s *Sources) UnresolvedImports(skip ...string) []string { seen := make(map[string]struct{}) for _, sk := range skip { seen[sk] = struct{}{} @@ -156,20 +245,40 @@ func (s Sources) UnresolvedImports(skip ...string) []string { // packageImporter implements go/types.Importer interface and // wraps it to collect import errors. type packageImporter struct { - Importer types.Importer + srcDir string + importer Importer + sizes types.Sizes + tContext *types.Context Errors errorList.ErrorList } -func (ei *packageImporter) Import(path string) (*types.Package, error) { +func (pi *packageImporter) Import(path string) (*types.Package, error) { if path == "unsafe" { return types.Unsafe, nil } - pkg, err := ei.Importer.Import(path) + srcs, err := pi.importer(path, pi.srcDir) + if err != nil { + pi.Errors = pi.Errors.AppendDistinct(err) + return nil, err + } + + // If the sources doesn't have the package determined yet, get it now, + // otherwise this will be a no-op. + // This will recursively get the packages for all of it's dependencies too. + err = srcs.TypeCheck(pi.importer, pi.sizes, pi.tContext) if err != nil { - ei.Errors = ei.Errors.AppendDistinct(err) + pi.Errors = pi.Errors.AppendDistinct(err) return nil, err } - return pkg, nil + return srcs.Package, nil +} + +// SortedSourcesSlice in place sorts the given slice of Sources by ImportPath. +// This will not change the order of the files within any Sources. +func SortedSourcesSlice(sourcesSlice []*Sources) { + sort.Slice(sourcesSlice, func(i, j int) bool { + return sourcesSlice[i].ImportPath < sourcesSlice[j].ImportPath + }) } diff --git a/internal/srctesting/srctesting.go b/internal/srctesting/srctesting.go index 83499c6dd..bf74bce51 100644 --- a/internal/srctesting/srctesting.go +++ b/internal/srctesting/srctesting.go @@ -187,9 +187,9 @@ type Source struct { // root package. At least one source file must be given. // The root package's path will be `command-line-arguments`. // -// The auxillary files can be for different packages but should have paths +// The auxiliary files can be for different packages but should have paths // added to the source name so that they can be grouped together by package. -// To import an auxillary package, the path should be prepended by +// To import an auxiliary package, the path should be prepended by // `github.com/gopherjs/gopherjs/compiler`. func ParseSources(t *testing.T, sourceFiles []Source, auxFiles []Source) *packages.Package { t.Helper() From bfb3fd5b943d1800d7dde6abfbb1beb918eedd6b Mon Sep 17 00:00:00 2001 From: Ingwie Phoenix Date: Tue, 8 Apr 2025 21:25:08 +0200 Subject: [PATCH 42/58] chore(deps): Replace outdated goembed repo with a fixed version. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bf9d40d8e..74066f886 100644 --- a/go.mod +++ b/go.mod @@ -6,13 +6,13 @@ require ( github.com/evanw/esbuild v0.18.0 github.com/fsnotify/fsnotify v1.5.1 github.com/google/go-cmp v0.5.8 + github.com/msvitok77/goembed v0.3.5 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 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 - github.com/visualfc/goembed v0.3.3 golang.org/x/sync v0.5.0 golang.org/x/sys v0.10.0 golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 diff --git a/go.sum b/go.sum index 5c6b0d919..65d4c5b33 100644 --- a/go.sum +++ b/go.sum @@ -192,6 +192,8 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/msvitok77/goembed v0.3.5 h1:SNdkLLipv4YGNVWCVCn+/N01aSp7Ga6/YOcB+kYxnhk= +github.com/msvitok77/goembed v0.3.5/go.mod h1:ycBNmh+53HrsZPQfWOJHYXbu7vLwb1QYdJISOyKlnnc= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c h1:bY6ktFuJkt+ZXkX0RChQch2FtHpWQLVS8Qo1YasiIVk= @@ -233,8 +235,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/visualfc/goembed v0.3.3 h1:pOL02L715tHKsLQVMcZz06tTzRDAHkJKJLRnCA22G9Q= -github.com/visualfc/goembed v0.3.3/go.mod h1:jCVCz/yTJGyslo6Hta+pYxWWBuq9ADCcIVZBTQ0/iVI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= From 18c9a0009dba0080c912cf37b58dc1c9835950cc Mon Sep 17 00:00:00 2001 From: Ingwie Phoenix Date: Tue, 8 Apr 2025 21:25:51 +0200 Subject: [PATCH 43/58] fix: Fixes #1352 by using a forked version of goembed --- build/embed.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/embed.go b/build/embed.go index c749eeb50..a68fb9494 100644 --- a/build/embed.go +++ b/build/embed.go @@ -8,7 +8,7 @@ import ( "go/token" "strconv" - "github.com/visualfc/goembed" + "github.com/msvitok77/goembed" ) func buildIdent(name string) string { From 138e9e0f035fa7f3cc66e8348d637778464436dd Mon Sep 17 00:00:00 2001 From: LRGoldenPony <“brian.holt@logrhythm.com”> Date: Mon, 12 May 2025 17:48:30 +1000 Subject: [PATCH 44/58] Update spf13/cobra, spf13/pflag, and evanw/esbuild to resolve direct and transitive vulnerabilities --- go.mod | 8 +- go.sum | 578 ++------------------------------------------------------- 2 files changed, 16 insertions(+), 570 deletions(-) diff --git a/go.mod b/go.mod index 74066f886..faa94f070 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/gopherjs/gopherjs go 1.18 require ( - github.com/evanw/esbuild v0.18.0 + github.com/evanw/esbuild v0.25.4 github.com/fsnotify/fsnotify v1.5.1 github.com/google/go-cmp v0.5.8 github.com/msvitok77/goembed v0.3.5 @@ -11,8 +11,8 @@ require ( github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 github.com/sirupsen/logrus v1.8.1 - github.com/spf13/cobra v1.2.1 - github.com/spf13/pflag v1.0.5 + github.com/spf13/cobra v1.9.1 + github.com/spf13/pflag v1.0.6 golang.org/x/sync v0.5.0 golang.org/x/sys v0.10.0 golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 @@ -20,6 +20,6 @@ require ( ) require ( - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect golang.org/x/mod v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 65d4c5b33..29dc8900b 100644 --- a/go.sum +++ b/go.sum @@ -1,599 +1,45 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanw/esbuild v0.18.0 h1:zJrquhC5ZiricRVQxMQTWqO8zYcV7F7OfUXstB9Ucbg= -github.com/evanw/esbuild v0.18.0/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/evanw/esbuild v0.25.4 h1:k1bTSim+usBG27w7BfOCorhgx3tO+6bAfMj5pR+6SKg= +github.com/evanw/esbuild v0.25.4/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -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.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= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/msvitok77/goembed v0.3.5 h1:SNdkLLipv4YGNVWCVCn+/N01aSp7Ga6/YOcB+kYxnhk= github.com/msvitok77/goembed v0.3.5/go.mod h1:ycBNmh+53HrsZPQfWOJHYXbu7vLwb1QYdJISOyKlnnc= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c h1:bY6ktFuJkt+ZXkX0RChQch2FtHpWQLVS8Qo1YasiIVk= github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISeOcal5irEhJd1M+IrApc0PdcN7e7Aj4yuEnOrfQ= github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -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/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= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -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= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -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/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= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -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.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -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= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 3e9afa2b8f6bd0242cf9fceeaf0ee05587cedfea Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Mon, 12 May 2025 14:26:22 -0600 Subject: [PATCH 45/58] Adding type parameters from a function into the nested types inside of that function. --- compiler/compiler_test.go | 138 +++++++ compiler/decls.go | 27 +- compiler/expressions.go | 19 +- compiler/functions.go | 4 +- compiler/internal/analysis/info.go | 4 +- compiler/internal/analysis/info_test.go | 1 + compiler/internal/symbol/symbol.go | 11 +- compiler/internal/typeparams/collect.go | 202 +++++++-- compiler/internal/typeparams/collect_test.go | 407 ++++++++++++++++++- compiler/internal/typeparams/instance.go | 85 +++- compiler/internal/typeparams/map.go | 21 +- compiler/internal/typeparams/map_test.go | 12 +- compiler/internal/typeparams/utils.go | 81 ++++ compiler/package.go | 10 +- compiler/utils.go | 41 +- internal/govendor/subst/export.go | 37 +- internal/govendor/subst/subst.go | 11 +- internal/govendor/subst/subst_test.go | 1 + internal/govendor/subst/util.go | 6 +- internal/srctesting/srctesting.go | 6 + 20 files changed, 987 insertions(+), 137 deletions(-) diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index e7add0015..88d8e525e 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -5,6 +5,7 @@ import ( "go/types" "regexp" "sort" + "strings" "testing" "time" @@ -749,6 +750,143 @@ func TestArchiveSelectionAfterSerialization(t *testing.T) { } } +func TestNestedConcreteTypeInGenericFunc(t *testing.T) { + // This is a test of a type defined inside a generic function + // that uses the type parameter of the function as a field type. + // The `T` type is unique for each instance of `F`. + // The use of `A` as a field is do demonstrate the difference in the types + // however even if T had no fields, the type would still be different. + // + // Change `print(F[?]())` to `fmt.Printf("%T\n", F[?]())` for + // golang playground to print the type of T in the different F instances. + // (I just didn't want this test to depend on `fmt` when it doesn't need to.) + + src := ` + package main + func F[A any]() any { + type T struct{ + a A + } + return T{} + } + func main() { + type Int int + print(F[int]()) + print(F[Int]()) + } + ` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + archives := compileProject(t, root, false) + mainPkg := archives[root.PkgPath] + insts := collectDeclInstances(t, mainPkg) + + exp := []string{ + `F[int]`, + `F[main.Int]`, // Go prints `F[main.Int·2]` + `T[int;]`, // `T` from `F[int]` (Go prints `T[int]`) + `T[main.Int;]`, // `T` from `F[main.Int]` (Go prints `T[main.Int·2]`) + } + if diff := cmp.Diff(exp, insts); len(diff) > 0 { + t.Errorf("the instances of generics are different:\n%s", diff) + } +} + +func TestNestedGenericTypeInGenericFunc(t *testing.T) { + // This is a subset of the type param nested test from the go repo. + // See https://github.com/golang/go/blob/go1.19.13/test/typeparam/nested.go + // The test is failing because nested types aren't being typed differently. + // For example the type of `T[int]` below is different based on `F[X]` + // instance for different `X` type parameters, hence Go prints the type as + // `T[X;int]` instead of `T[int]`. + + src := ` + package main + func F[A any]() any { + type T[B any] struct{ + a A + b B + } + return T[int]{} + } + func main() { + type Int int + print(F[int]()) + print(F[Int]()) + } + ` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + archives := compileProject(t, root, false) + mainPkg := archives[root.PkgPath] + insts := collectDeclInstances(t, mainPkg) + + exp := []string{ + `F[int]`, + `F[main.Int]`, + `T[int; int]`, + `T[main.Int; int]`, + } + if diff := cmp.Diff(exp, insts); len(diff) > 0 { + t.Errorf("the instances of generics are different:\n%s", diff) + } +} + +func TestNestedGenericTypeInGenericFuncWithSharedTArgs(t *testing.T) { + src := ` + package main + func F[A any]() any { + type T[B any] struct { + b B + } + return T[A]{} + } + func main() { + type Int int + print(F[int]()) + print(F[Int]()) + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + root := srctesting.ParseSources(t, srcFiles, nil) + archives := compileProject(t, root, false) + mainPkg := archives[root.PkgPath] + insts := collectDeclInstances(t, mainPkg) + + exp := []string{ + `F[int]`, + `F[main.Int]`, + `T[int; int]`, + `T[main.Int; main.Int]`, + // Make sure that T[int;main.Int] and T[main.Int;int] aren't created. + } + if diff := cmp.Diff(exp, insts); len(diff) > 0 { + t.Errorf("the instances of generics are different:\n%s", diff) + } +} + +func collectDeclInstances(t *testing.T, pkg *Archive) []string { + t.Helper() + + // Regex to match strings like `Foo[42 /* bar */] =` and capture + // the name (`Foo`), the index (`42`), and the instance type (`bar`). + rex := regexp.MustCompile(`^\s*(\w+)\s*\[\s*(\d+)\s*\/\*(.+)\*\/\s*\]\s*\=`) + + // Collect all instances of generics (e.g. `Foo[bar] @ 2`) written to the decl code. + insts := []string{} + for _, decl := range pkg.Declarations { + if match := rex.FindAllStringSubmatch(string(decl.DeclCode), 1); len(match) > 0 { + instance := match[0][1] + `[` + strings.TrimSpace(match[0][3]) + `]` + instance = strings.ReplaceAll(instance, `command-line-arguments`, pkg.Name) + insts = append(insts, instance) + } + } + sort.Strings(insts) + return insts +} + func compareOrder(t *testing.T, sourceFiles []srctesting.Source, minify bool) { t.Helper() outputNormal := compile(t, sourceFiles, minify) diff --git a/compiler/decls.go b/compiler/decls.go index 5b760fb15..eb95cd2f7 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -433,7 +433,7 @@ func (fc *funcContext) newNamedTypeVarDecl(obj *types.TypeName) *Decl { FullName: typeVarDeclFullName(obj), Vars: []string{name}, } - if typeparams.HasTypeParams(obj.Type()) { + if fc.pkgCtx.instanceSet.Pkg(obj.Pkg()).ObjHasInstances(obj) { varDecl.DeclCode = fc.CatchOutput(0, func() { fc.Printf("%s = {};", name) }) @@ -451,16 +451,28 @@ func (fc *funcContext) newNamedTypeVarDecl(obj *types.TypeName) *Decl { func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, error) { originType := inst.Object.Type().(*types.Named) - fc.typeResolver = typeparams.NewResolver(fc.pkgCtx.typesCtx, typeparams.ToSlice(originType.TypeParams()), inst.TArgs) + var nestResolver *typeparams.Resolver + if len(inst.TNest) > 0 { + fn := typeparams.FindNestingFunc(inst.Object) + tp := typeparams.SignatureTypeParams(fn.Type().(*types.Signature)) + nestResolver = typeparams.NewResolver(fc.pkgCtx.typesCtx, tp, inst.TNest, nil) + } + fc.typeResolver = typeparams.NewResolver(fc.pkgCtx.typesCtx, originType.TypeParams(), inst.TArgs, nestResolver) defer func() { fc.typeResolver = nil }() instanceType := originType if !inst.IsTrivial() { - instantiated, err := types.Instantiate(fc.pkgCtx.typesCtx, originType, inst.TArgs, true) - if err != nil { - return nil, fmt.Errorf("failed to instantiate type %v with args %v: %w", originType, inst.TArgs, err) + if len(inst.TArgs) > 0 { + instantiated, err := types.Instantiate(fc.pkgCtx.typesCtx, originType, inst.TArgs, true) + if err != nil { + return nil, fmt.Errorf("failed to instantiate type %v with args %v: %w", originType, inst.TArgs, err) + } + instanceType = instantiated.(*types.Named) + } + if len(inst.TNest) > 0 { + instantiated := nestResolver.Substitute(instanceType) + instanceType = instantiated.(*types.Named) } - instanceType = instantiated.(*types.Named) } underlying := instanceType.Underlying() @@ -541,7 +553,8 @@ func (fc *funcContext) structConstructor(t *types.Struct) string { // If no arguments were passed, zero-initialize all fields. fmt.Fprintf(constructor, "\t\tif (arguments.length === 0) {\n") for i := 0; i < t.NumFields(); i++ { - fmt.Fprintf(constructor, "\t\t\tthis.%s = %s;\n", fieldName(t, i), fc.translateExpr(fc.zeroValue(t.Field(i).Type())).String()) + zeroValue := fc.zeroValue(fc.fieldType(t, i)) + fmt.Fprintf(constructor, "\t\t\tthis.%s = %s;\n", fieldName(t, i), fc.translateExpr(zeroValue).String()) } fmt.Fprintf(constructor, "\t\t\treturn;\n") fmt.Fprintf(constructor, "\t\t}\n") diff --git a/compiler/expressions.go b/compiler/expressions.go index f1c2e68f5..781a37a3e 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -178,18 +178,18 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } if !isKeyValue { for i, element := range e.Elts { - elements[i] = fc.translateImplicitConversionWithCloning(element, t.Field(i).Type()).String() + elements[i] = fc.translateImplicitConversionWithCloning(element, fc.fieldType(t, i)).String() } } if isKeyValue { for i := range elements { - elements[i] = fc.translateExpr(fc.zeroValue(t.Field(i).Type())).String() + elements[i] = fc.translateExpr(fc.zeroValue(fc.fieldType(t, i))).String() } for _, element := range e.Elts { kve := element.(*ast.KeyValueExpr) for j := range elements { if kve.Key.(*ast.Ident).Name == t.Field(j).Name() { - elements[j] = fc.translateImplicitConversionWithCloning(kve.Value, t.Field(j).Type()).String() + elements[j] = fc.translateImplicitConversionWithCloning(kve.Value, fc.fieldType(t, j)).String() break } } @@ -801,7 +801,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { switch t := exprType.Underlying().(type) { case *types.Basic: if t.Kind() != types.UnsafePointer { - panic("unexpected basic type") + panic(fmt.Errorf(`unexpected basic type: %v in %v`, t, e.Name)) } return fc.formatExpr("0") case *types.Slice, *types.Pointer: @@ -917,7 +917,7 @@ func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression { recvType = ptr.Elem() } s := recvType.Underlying().(*types.Struct) - recvType = s.Field(index).Type() + recvType = fc.fieldType(s, index) } fakeSel := &ast.SelectorExpr{X: x, Sel: ast.NewIdent("o")} @@ -1314,12 +1314,13 @@ func (fc *funcContext) loadStruct(array, target string, s *types.Struct) string var collectFields func(s *types.Struct, path string) collectFields = func(s *types.Struct, path string) { for i := 0; i < s.NumFields(); i++ { - field := s.Field(i) - if fs, isStruct := field.Type().Underlying().(*types.Struct); isStruct { - collectFields(fs, path+"."+fieldName(s, i)) + fieldName := path + "." + fieldName(s, i) + fieldType := fc.fieldType(s, i) + if fs, isStruct := fieldType.Underlying().(*types.Struct); isStruct { + collectFields(fs, fieldName) continue } - fields = append(fields, types.NewVar(0, nil, path+"."+fieldName(s, i), field.Type())) + fields = append(fields, types.NewVar(0, nil, fieldName, fieldType)) } } collectFields(s, target) diff --git a/compiler/functions.go b/compiler/functions.go index 592992efc..361c92f0f 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -49,9 +49,9 @@ func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, inst typep } if sig.TypeParams().Len() > 0 { - c.typeResolver = typeparams.NewResolver(c.pkgCtx.typesCtx, typeparams.ToSlice(sig.TypeParams()), inst.TArgs) + c.typeResolver = typeparams.NewResolver(c.pkgCtx.typesCtx, sig.TypeParams(), inst.TArgs, nil) } else if sig.RecvTypeParams().Len() > 0 { - c.typeResolver = typeparams.NewResolver(c.pkgCtx.typesCtx, typeparams.ToSlice(sig.RecvTypeParams()), inst.TArgs) + c.typeResolver = typeparams.NewResolver(c.pkgCtx.typesCtx, sig.RecvTypeParams(), inst.TArgs, nil) } if c.objectNames == nil { c.objectNames = map[types.Object]string{} diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go index d05f9a6d1..e400c870c 100644 --- a/compiler/internal/analysis/info.go +++ b/compiler/internal/analysis/info.go @@ -126,8 +126,8 @@ func (info *Info) newFuncInfoInstances(fd *ast.FuncDecl) []*FuncInfo { for _, inst := range instances { var resolver *typeparams.Resolver if sig, ok := obj.Type().(*types.Signature); ok { - tp := typeparams.ToSlice(typeparams.SignatureTypeParams(sig)) - resolver = typeparams.NewResolver(info.typeCtx, tp, inst.TArgs) + tp := typeparams.SignatureTypeParams(sig) + resolver = typeparams.NewResolver(info.typeCtx, tp, inst.TArgs, nil) } fi := info.newFuncInfo(fd, inst.Object, inst.TArgs, resolver) funcInfos = append(funcInfos, fi) diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go index 73428207e..0df26b0b9 100644 --- a/compiler/internal/analysis/info_test.go +++ b/compiler/internal/analysis/info_test.go @@ -382,6 +382,7 @@ func TestBlocking_Defers_WithMultipleReturns(t *testing.T) { // of which flow control statements (e.g. if-statements) are terminating // or not. Any defers added in a terminating control flow would not // propagate to returns that are not in that block. + // See golang.org/x/tools/go/ssa for flow control analysis. // // For now we simply build up the list of defers as we go making // the return on line 31 also blocking. diff --git a/compiler/internal/symbol/symbol.go b/compiler/internal/symbol/symbol.go index 851ca1ef6..d460ea86d 100644 --- a/compiler/internal/symbol/symbol.go +++ b/compiler/internal/symbol/symbol.go @@ -21,6 +21,11 @@ type Name struct { // New constructs SymName for a given named symbol. func New(o types.Object) Name { + pkgPath := `_` + if pkg := o.Pkg(); pkg != nil { + pkgPath = pkg.Path() + } + if fun, ok := o.(*types.Func); ok { sig := fun.Type().(*types.Signature) if recv := sig.Recv(); recv != nil { @@ -28,18 +33,18 @@ func New(o types.Object) Name { typ := recv.Type() if ptr, ok := typ.(*types.Pointer); ok { return Name{ - PkgPath: o.Pkg().Path(), + PkgPath: pkgPath, Name: "(*" + ptr.Elem().(*types.Named).Obj().Name() + ")." + o.Name(), } } return Name{ - PkgPath: o.Pkg().Path(), + PkgPath: pkgPath, Name: typ.(*types.Named).Obj().Name() + "." + o.Name(), } } } return Name{ - PkgPath: o.Pkg().Path(), + PkgPath: pkgPath, Name: o.Name(), } } diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index 723172d4f..940690e83 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -4,6 +4,7 @@ import ( "fmt" "go/ast" "go/types" + "strings" "github.com/gopherjs/gopherjs/compiler/typesutil" "github.com/gopherjs/gopherjs/internal/govendor/subst" @@ -12,27 +13,67 @@ import ( // Resolver translates types defined in terms of type parameters into concrete // types, given a mapping from type params to type arguments. type Resolver struct { + tParams *types.TypeParamList + tArgs []types.Type + parent *Resolver + + // subster is the substitution helper that will perform the actual + // substitutions. This maybe nil when there are no substitutions but + // will still usable when nil. subster *subst.Subster selMemo map[typesutil.Selection]typesutil.Selection } // NewResolver creates a new Resolver with tParams entries mapping to tArgs // entries with the same index. -func NewResolver(tc *types.Context, tParams []*types.TypeParam, tArgs []types.Type) *Resolver { +func NewResolver(tc *types.Context, tParams *types.TypeParamList, tArgs []types.Type, parent *Resolver) *Resolver { r := &Resolver{ + tParams: tParams, + tArgs: tArgs, + parent: parent, subster: subst.New(tc, tParams, tArgs), selMemo: map[typesutil.Selection]typesutil.Selection{}, } return r } +// TypeParams is the list of type parameters that this resolver +// (not any parent) will substitute. +func (r *Resolver) TypeParams() *types.TypeParamList { + if r == nil { + return nil + } + return r.tParams +} + +// TypeArgs is the list of type arguments that this resolver +// (not any parent) will resolve to. +func (r *Resolver) TypeArgs() []types.Type { + if r == nil { + return nil + } + return r.tArgs +} + +// Parent is the resolver for the function or method that this resolver +// is nested in. This may be nil if the context for this resolver is not +// nested in another generic function or method. +func (r *Resolver) Parent() *Resolver { + if r == nil { + return nil + } + return r.parent +} + // Substitute replaces references to type params in the provided type definition // with the corresponding concrete types. func (r *Resolver) Substitute(typ types.Type) types.Type { - if r == nil || r.subster == nil || typ == nil { + if r == nil || typ == nil { return typ // No substitutions to be made. } - return r.subster.Type(typ) + typ = r.subster.Type(typ) + typ = r.parent.Substitute(typ) + return typ } // SubstituteAll same as Substitute, but accepts a TypeList are returns @@ -49,7 +90,7 @@ func (r *Resolver) SubstituteAll(list *types.TypeList) []types.Type { // defined in terms of type parameters with a method selection on a concrete // instantiation of the type. func (r *Resolver) SubstituteSelection(sel typesutil.Selection) typesutil.Selection { - if r == nil || r.subster == nil || sel == nil { + if r == nil || sel == nil { return sel // No substitutions to be made. } if concrete, ok := r.selMemo[sel]; ok { @@ -82,13 +123,22 @@ func (r *Resolver) SubstituteSelection(sel typesutil.Selection) typesutil.Select } } -// ToSlice converts TypeParamList into a slice with the same order of entries. -func ToSlice(tpl *types.TypeParamList) []*types.TypeParam { - result := make([]*types.TypeParam, tpl.Len()) - for i := range result { - result[i] = tpl.At(i) +// String gets a strings representation of the resolver for debugging. +func (r *Resolver) String() string { + if r == nil { + return `{}` } - return result + + parts := make([]string, 0, len(r.tArgs)) + for i, ta := range r.tArgs { + parts = append(parts, fmt.Sprintf("%s->%s", r.tParams.At(i), ta)) + } + + nestStr := `` + if r.parent != nil { + nestStr = r.parent.String() + `:` + } + return nestStr + `{` + strings.Join(parts, `, `) + `}` } // visitor implements ast.Visitor and collects instances of generic types and @@ -101,24 +151,35 @@ type visitor struct { instances *PackageInstanceSets resolver *Resolver info *types.Info + tNest []types.Type // The type arguments for a nested context. } var _ ast.Visitor = &visitor{} -func (c *visitor) Visit(n ast.Node) (w ast.Visitor) { - w = c // Always traverse the full depth of the AST tree. +func (c *visitor) Visit(n ast.Node) ast.Visitor { + if ident, ok := n.(*ast.Ident); ok { + c.visitIdent(ident) + } + return c +} - ident, ok := n.(*ast.Ident) - if !ok { - return +func (c *visitor) visitIdent(ident *ast.Ident) { + if inst, ok := c.info.Instances[ident]; ok { + // Found the use of a generic type or function. + c.visitInstance(ident, inst) } - instance, ok := c.info.Instances[ident] - if !ok { - return + if len(c.resolver.TypeArgs()) > 0 { + if obj, ok := c.info.Defs[ident]; ok && obj != nil { + // Found instance of a type defined inside a generic context. + c.visitNestedType(obj) + } } +} - obj := c.info.ObjectOf(ident) +func (c *visitor) visitInstance(ident *ast.Ident, inst types.Instance) { + obj := c.info.Uses[ident] + tArgs := inst.TypeArgs // For types embedded in structs, the object the identifier resolves to is a // *types.Var representing the implicitly declared struct field. However, the @@ -131,9 +192,53 @@ func (c *visitor) Visit(n ast.Node) (w ast.Visitor) { if t, ok := typ.(*types.Named); ok { obj = t.Obj() } + + // If the object is defined in the same scope as the instance, + // then we apply the current nested type arguments. + var tNest []types.Type + if obj.Parent().Contains(ident.Pos()) { + tNest = c.tNest + } + + c.addInstance(obj, tArgs, tNest) +} + +func (c *visitor) visitNestedType(obj types.Object) { + if _, ok := obj.(*types.TypeName); !ok { + // Found a variable or function, not a type, so skip it. + return + } + + typ := obj.Type() + if ptr, ok := typ.(*types.Pointer); ok { + typ = ptr.Elem() + } + + t, ok := typ.(*types.Named) + if !ok || t.TypeParams().Len() > 0 { + // Found a generic type or an unnamed type (e.g. type parameter). + // Don't add generic types yet because they + // will be added when we find an instance of them. + return + } + + c.addInstance(obj, nil, c.resolver.TypeArgs()) +} + +func (c *visitor) addInstance(obj types.Object, tArgList *types.TypeList, tNest []types.Type) { + tArgs := c.resolver.SubstituteAll(tArgList) + if isGeneric(tArgs...) { + // Skip any instances that still have type parameters in them after + // substitution. This occurs when a type is defined while nested + // in a generic context and is not fully instantiated yet. + // We need to wait until we find a full instantiation of the type. + return + } + c.instances.Add(Instance{ Object: obj, - TArgs: c.resolver.SubstituteAll(instance.TypeArgs), + TArgs: tArgs, + TNest: tNest, }) if t, ok := obj.Type().(*types.Named); ok { @@ -141,11 +246,11 @@ func (c *visitor) Visit(n ast.Node) (w ast.Visitor) { method := t.Method(i) c.instances.Add(Instance{ Object: method.Origin(), - TArgs: c.resolver.SubstituteAll(instance.TypeArgs), + TArgs: tArgs, + TNest: tNest, }) } } - return } // seedVisitor implements ast.Visitor that collects information necessary to @@ -241,22 +346,49 @@ func (c *Collector) Scan(pkg *types.Package, files ...*ast.File) { for iset := c.Instances.Pkg(pkg); !iset.exhausted(); { inst, _ := iset.next() + switch typ := inst.Object.Type().(type) { case *types.Signature: - v := visitor{ - instances: c.Instances, - resolver: NewResolver(c.TContext, ToSlice(SignatureTypeParams(typ)), inst.TArgs), - info: c.Info, - } - ast.Walk(&v, objMap[inst.Object]) + c.scanSignature(inst, typ, objMap) + case *types.Named: - obj := typ.Obj() - v := visitor{ - instances: c.Instances, - resolver: NewResolver(c.TContext, ToSlice(typ.TypeParams()), inst.TArgs), - info: c.Info, - } - ast.Walk(&v, objMap[obj]) + c.scanNamed(inst, typ, objMap) } } } + +func (c *Collector) scanSignature(inst Instance, typ *types.Signature, objMap map[types.Object]ast.Node) { + tParams := SignatureTypeParams(typ) + v := visitor{ + instances: c.Instances, + resolver: NewResolver(c.TContext, tParams, inst.TArgs, nil), + info: c.Info, + tNest: inst.TArgs, + } + ast.Walk(&v, objMap[inst.Object]) +} + +func (c *Collector) scanNamed(inst Instance, typ *types.Named, objMap map[types.Object]ast.Node) { + obj := typ.Obj() + node := objMap[obj] + if node == nil { + // Types without an entry in objMap are concrete types + // that are defined in a generic context. Skip them. + return + } + + var nestResolver *Resolver + if len(inst.TNest) > 0 { + fn := FindNestingFunc(inst.Object) + tp := SignatureTypeParams(fn.Type().(*types.Signature)) + nestResolver = NewResolver(c.TContext, tp, inst.TNest, nil) + } + + v := visitor{ + instances: c.Instances, + resolver: NewResolver(c.TContext, typ.TypeParams(), inst.TArgs, nestResolver), + info: c.Info, + tNest: inst.TNest, + } + ast.Walk(&v, node) +} diff --git a/compiler/internal/typeparams/collect_test.go b/compiler/internal/typeparams/collect_test.go index 9bd5faee4..6864e5ead 100644 --- a/compiler/internal/typeparams/collect_test.go +++ b/compiler/internal/typeparams/collect_test.go @@ -2,7 +2,9 @@ package typeparams import ( "go/ast" + "go/token" "go/types" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -35,7 +37,11 @@ func TestVisitor(t *testing.T) { t := typ[int, A]{} t.method(0) (*typ[int32, A]).method(nil, 0) + type x struct{ T []typ[int64, A] } + type y[X any] struct{ T []typ[A, X] } + _ = y[int8]{} + _ = y[A]{} return } @@ -49,7 +55,11 @@ func TestVisitor(t *testing.T) { t := typ[int, T]{} t.method(0) (*typ[int32, T]).method(nil, 0) + type x struct{ T []typ[int64, T] } + type y[X any] struct{ T []typ[T, X] } + _ = y[int8]{} + _ = y[T]{} return } @@ -67,7 +77,11 @@ func TestVisitor(t *testing.T) { t := typ[int, T]{} t.method(0) (*typ[int32, T]).method(nil, 0) + type x struct{ T []typ[int64, T] } + type y[X any] struct{ T []typ[T, X] } + _ = y[int8]{} + _ = y[T]{} return } @@ -189,22 +203,50 @@ func TestVisitor(t *testing.T) { descr: "non-generic function", resolver: nil, node: lookupDecl("entry1"), - want: instancesInFunc(lookupType("A")), + want: append( + instancesInFunc(lookupType("A")), + Instance{ + Object: lookupObj("entry1.y"), + TArgs: []types.Type{types.Typ[types.Int8]}, + }, + Instance{ + Object: lookupObj("entry1.y"), + TArgs: []types.Type{lookupType("A")}, + }, + ), }, { descr: "generic function", resolver: NewResolver( types.NewContext(), - ToSlice(lookupType("entry2").(*types.Signature).TypeParams()), + lookupType("entry2").(*types.Signature).TypeParams(), []types.Type{lookupType("B")}, + nil, ), node: lookupDecl("entry2"), - want: instancesInFunc(lookupType("B")), + want: append( + instancesInFunc(lookupType("B")), + Instance{ + Object: lookupObj("entry2.x"), + TNest: []types.Type{lookupType("B")}, + }, + Instance{ + Object: lookupObj("entry1.y"), + TNest: []types.Type{lookupType("B")}, + TArgs: []types.Type{types.Typ[types.Int8]}, + }, + Instance{ + Object: lookupObj("entry2.y"), + TNest: []types.Type{lookupType("B")}, + TArgs: []types.Type{lookupType("B")}, + }, + ), }, { descr: "generic method", resolver: NewResolver( types.NewContext(), - ToSlice(lookupType("entry3.method").(*types.Signature).RecvTypeParams()), + lookupType("entry3.method").(*types.Signature).RecvTypeParams(), []types.Type{lookupType("C")}, + nil, ), node: lookupDecl("entry3.method"), want: append( @@ -217,13 +259,28 @@ func TestVisitor(t *testing.T) { Object: lookupObj("entry3.method"), TArgs: []types.Type{lookupType("C")}, }, + Instance{ + Object: lookupObj("entry3.method.x"), + TNest: []types.Type{lookupType("C")}, + }, + Instance{ + Object: lookupObj("entry3.method.y"), + TNest: []types.Type{lookupType("C")}, + TArgs: []types.Type{types.Typ[types.Int8]}, + }, + Instance{ + Object: lookupObj("entry3.method.y"), + TNest: []types.Type{lookupType("C")}, + TArgs: []types.Type{lookupType("C")}, + }, ), }, { descr: "generic type declaration", resolver: NewResolver( types.NewContext(), - ToSlice(lookupType("entry3").(*types.Named).TypeParams()), + lookupType("entry3").(*types.Named).TypeParams(), []types.Type{lookupType("D")}, + nil, ), node: lookupDecl("entry3"), want: instancesInType(lookupType("D")), @@ -256,6 +313,11 @@ func TestVisitor(t *testing.T) { resolver: test.resolver, info: info, } + if test.resolver != nil { + // Since we know all the tests are for functions and methods, + // set the nested type to the type parameter from the resolver. + v.tNest = test.resolver.tArgs + } ast.Walk(&v, test.node) got := v.instances.Pkg(pkg).Values() if diff := cmp.Diff(test.want, got, instanceOpts()); diff != "" { @@ -319,7 +381,7 @@ func TestSeedVisitor(t *testing.T) { } got := sv.instances.Pkg(pkg).Values() if diff := cmp.Diff(want, got, instanceOpts()); diff != "" { - t.Errorf("Instances from initialSeeder contain diff (-want,+got):\n%s", diff) + t.Errorf("Instances from seedVisitor contain diff (-want,+got):\n%s", diff) } } @@ -353,28 +415,333 @@ func TestCollector(t *testing.T) { } c.Scan(pkg, file) - inst := func(name string, tArg types.Type) Instance { + inst := func(name, tNest, tArg string) Instance { return Instance{ Object: srctesting.LookupObj(pkg, name), - TArgs: []types.Type{tArg}, + TNest: evalTypeArgs(t, f.FileSet, pkg, tNest), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), } } want := []Instance{ - inst("typ", types.Typ[types.Int]), - inst("typ.method", types.Typ[types.Int]), - inst("fun", types.Typ[types.Int8]), - inst("fun.nested", types.Typ[types.Int8]), - inst("typ", types.Typ[types.Int16]), - inst("typ.method", types.Typ[types.Int16]), - inst("typ", types.Typ[types.Int32]), - inst("typ.method", types.Typ[types.Int32]), - inst("fun", types.Typ[types.Int64]), - inst("fun.nested", types.Typ[types.Int64]), + inst(`typ`, ``, `int`), + inst(`typ.method`, ``, `int`), + inst(`fun`, ``, `int8`), + inst(`fun.nested`, `int8`, `int8`), + inst(`typ`, ``, `int16`), + inst(`typ.method`, ``, `int16`), + inst(`typ`, ``, `int32`), + inst(`typ.method`, ``, `int32`), + inst(`fun`, ``, `int64`), + inst(`fun.nested`, `int64`, `int64`), } got := c.Instances.Pkg(pkg).Values() if diff := cmp.Diff(want, got, instanceOpts()); diff != "" { - t.Errorf("Instances from initialSeeder contain diff (-want,+got):\n%s", diff) + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_MoreNesting(t *testing.T) { + src := `package test + + func fun[T any]() { + type nestedCon struct{ X T } + _ = nestedCon{} + + type nestedGen[U any] struct{ Y T; Z U } + _ = nestedGen[T]{} + _ = nestedGen[int8]{} + + type nestedCover[T any] struct{ W T } + _ = nestedCover[T]{} + _ = nestedCover[int16]{} + } + + func a() { + fun[int32]() + fun[int64]() + } + ` + + f := srctesting.New(t) + file := f.Parse(`test.go`, src) + info, pkg := f.Check(`pkg/test`, file) + + c := Collector{ + TContext: types.NewContext(), + Info: info, + Instances: &PackageInstanceSets{}, + } + c.Scan(pkg, file) + + inst := func(name, tNest, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TNest: evalTypeArgs(t, f.FileSet, pkg, tNest), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } + want := []Instance{ + inst(`fun`, ``, `int32`), + inst(`fun`, ``, `int64`), + + inst(`fun.nestedCon`, `int32`, ``), + inst(`fun.nestedCon`, `int64`, ``), + + inst(`fun.nestedGen`, `int32`, `int32`), + inst(`fun.nestedGen`, `int32`, `int8`), + inst(`fun.nestedGen`, `int64`, `int64`), + inst(`fun.nestedGen`, `int64`, `int8`), + + inst(`fun.nestedCover`, `int32`, `int32`), + inst(`fun.nestedCover`, `int32`, `int16`), + inst(`fun.nestedCover`, `int64`, `int64`), + inst(`fun.nestedCover`, `int64`, `int16`), + } + got := c.Instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_NestingWithVars(t *testing.T) { + // This is loosely based off of go1.19.13/test/typeparam/issue47740b.go + // I was getting an error where `Q.print[int;]` was showing up when + // `Q.print` is not in a nesting context with `int` and this helped debug + // it. The problem was that `q` was being treated like a type not a var. + src := `package test + + type Q struct{ v any } + func (q Q) print() { + println(q.v) + } + + func newQ(v any) Q { + return Q{v} + } + + type S[T any] struct{ x T } + func (s S[T]) echo() { + q := newQ(s.x) + q.print() + } + + func a() { + s := S[int]{x: 0} + s.echo() + } + ` + + f := srctesting.New(t) + file := f.Parse(`test.go`, src) + info, pkg := f.Check(`pkg/test`, file) + + c := Collector{ + TContext: types.NewContext(), + Info: info, + Instances: &PackageInstanceSets{}, + } + c.Scan(pkg, file) + + inst := func(name, tNest, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TNest: evalTypeArgs(t, f.FileSet, pkg, tNest), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } + want := []Instance{ + inst(`S`, ``, `int`), + inst(`S.echo`, ``, `int`), + } + got := c.Instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_RecursiveTypeParams(t *testing.T) { + // This is based off of part of go1.19.13/test/typeparam/nested.go + src := `package test + func F[A any]() {} + func main() { + type U[_ any] int + type X[A any] U[X[A]] + F[X[int]]() + } + ` + + f := srctesting.New(t) + file := f.Parse(`test.go`, src) + info, pkg := f.Check(`test`, file) + + c := Collector{ + TContext: types.NewContext(), + Info: info, + Instances: &PackageInstanceSets{}, + } + c.Scan(pkg, file) + + tInt := types.Typ[types.Int] + xAny := srctesting.LookupObj(pkg, `main.X`) + xInt, err := types.Instantiate(types.NewContext(), xAny.Type(), []types.Type{tInt}, true) + if err != nil { + t.Fatalf("Failed to instantiate X[int]: %v", err) + } + + want := []Instance{ + { + Object: srctesting.LookupObj(pkg, `F`), + TArgs: []types.Type{xInt}, + }, { + Object: srctesting.LookupObj(pkg, `main.U`), + TArgs: []types.Type{xInt}, + }, { + Object: xAny, + TArgs: []types.Type{tInt}, + }, + } + got := c.Instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_NestedRecursiveTypeParams(t *testing.T) { + t.Skip(`Skipping test due to known issue with nested recursive type parameters.`) + // TODO(grantnelson-wf): This test is failing because the type parameters + // inside of U are not being resolved to concrete types. This is because + // when instantiating X in the collector, we are not resolving the + // nested type of U that is X's type argument. This leave the A in U + // as a type parameter instead of resolving it to string. + + // This is based off of part of go1.19.13/test/typeparam/nested.go + src := `package test + func F[A any]() any { + type U[_ any] struct{ x A } + type X[B any] U[X[B]] + return X[int]{} + } + func main() { + print(F[string]()) + } + ` + + f := srctesting.New(t) + file := f.Parse(`test.go`, src) + info, pkg := f.Check(`test`, file) + + c := Collector{ + TContext: types.NewContext(), + Info: info, + Instances: &PackageInstanceSets{}, + } + c.Scan(pkg, file) + + xAny := srctesting.LookupObj(pkg, `F.X`) + xInt, err := types.Instantiate(types.NewContext(), xAny.Type(), []types.Type{types.Typ[types.Int]}, true) + if err != nil { + t.Fatalf("Failed to instantiate X[int]: %v", err) + } + // TODO(grantnelson-wf): Need to instantiate xInt to replace `A` with `int` in the struct. + if isGeneric(xInt) { + t.Errorf("Expected uInt to be non-generic, got %v", xInt.Underlying()) + } + + want := []Instance{ + { + Object: srctesting.LookupObj(pkg, `F`), + TArgs: []types.Type{types.Typ[types.String]}, + }, { + Object: srctesting.LookupObj(pkg, `F.U`), + TNest: []types.Type{types.Typ[types.String]}, + TArgs: []types.Type{xInt}, + }, { + Object: xAny, + TNest: []types.Type{types.Typ[types.String]}, + TArgs: []types.Type{types.Typ[types.Int]}, + }, + } + got := c.Instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_NestedTypeParams(t *testing.T) { + t.Skip(`Skipping test due to known issue with nested recursive type parameters.`) + // TODO(grantnelson-wf): This test is failing because the type parameters + // inside of U are not being resolved to concrete types. This is because + // when instantiating X in the collector, we are not resolving the + // nested type of U that is X's type argument. This leave the A in U + // as a type parameter instead of resolving it to string. + + // This is based off of part of go1.19.13/test/typeparam/nested.go + src := `package test + func F[A any]() any { + type T[B any] struct{} + type U[_ any] struct{ X A } + return T[U[A]]{} + } + func main() { + print(F[int]()) + } + ` + + f := srctesting.New(t) + file := f.Parse(`test.go`, src) + info, pkg := f.Check(`test`, file) + + c := Collector{ + TContext: types.NewContext(), + Info: info, + Instances: &PackageInstanceSets{}, + } + c.Scan(pkg, file) + + uAny := srctesting.LookupObj(pkg, `F.U`) + uInt, err := types.Instantiate(types.NewContext(), uAny.Type(), []types.Type{types.Typ[types.Int]}, true) + if err != nil { + t.Fatalf("Failed to instantiate U[int]: %v", err) + } + //TODO(grantnelson-wf): Need to instantiate uInt to replace `A` with `int` in the struct. + if isGeneric(uInt) { + t.Errorf("Expected uInt to be non-generic, got %v", uInt.Underlying()) + } + + want := []Instance{ + { + Object: srctesting.LookupObj(pkg, `F`), + TArgs: []types.Type{types.Typ[types.Int]}, + }, { + Object: srctesting.LookupObj(pkg, `F.U`), + TNest: []types.Type{types.Typ[types.Int]}, + TArgs: []types.Type{types.Typ[types.Int]}, + }, { + Object: srctesting.LookupObj(pkg, `F.T`), + TNest: []types.Type{types.Typ[types.Int]}, + TArgs: []types.Type{uInt}, + }, + } + got := c.Instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func evalTypeArgs(t *testing.T, fSet *token.FileSet, pkg *types.Package, expr string) []types.Type { + if len(expr) == 0 { + return nil + } + args := strings.Split(expr, ",") + targs := make([]types.Type, 0, len(args)) + for _, astr := range args { + tv, err := types.Eval(fSet, pkg, 0, astr) + if err != nil { + t.Fatalf("Eval(%s) failed: %v", astr, err) + } + targs = append(targs, tv.Type) } + return targs } func TestCollector_CrossPackage(t *testing.T) { @@ -492,7 +859,7 @@ func TestResolver_SubstituteSelection(t *testing.T) { info, pkg := f.Check("pkg/test", file) method := srctesting.LookupObj(pkg, "g.Method").(*types.Func).Type().(*types.Signature) - resolver := NewResolver(nil, ToSlice(method.RecvTypeParams()), []types.Type{srctesting.LookupObj(pkg, "x").Type()}) + resolver := NewResolver(nil, method.RecvTypeParams(), []types.Type{srctesting.LookupObj(pkg, "x").Type()}, nil) if l := len(info.Selections); l != 1 { t.Fatalf("Got: %d selections. Want: 1", l) diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index 3e4c04d2c..64c67b4b5 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -3,6 +3,7 @@ package typeparams import ( "fmt" "go/types" + "strings" "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/gopherjs/gopherjs/compiler/typesutil" @@ -15,39 +16,82 @@ import ( type Instance struct { Object types.Object // Object to be instantiated. TArgs typesutil.TypeList // Type params to instantiate with. + + // TNest is the type params of the function this object was nested with-in. + // e.g. In `func A[X any]() { type B[Y any] struct {} }` the `X` + // from `A` is the context of `B[Y]` thus creating `B[X;Y]`. + TNest typesutil.TypeList } // String returns a string representation of the Instance. // // Two semantically different instances may have the same string representation // if the instantiated object or its type arguments shadow other types. -func (i *Instance) String() string { - sym := symbol.New(i.Object).String() - if len(i.TArgs) == 0 { - return sym +func (i Instance) String() string { + return i.symbolicName() + i.TypeParamsString(`<`, `>`) +} + +// TypeString returns a Go type string representing the instance (suitable for %T verb). +func (i Instance) TypeString() string { + return i.qualifiedName() + i.TypeParamsString(`[`, `]`) +} + +// symbolicName returns a string representation of the instance's name +// including the package name and pointer indicators but +// excluding the type parameters. +func (i Instance) symbolicName() string { + if i.Object == nil { + return `` } + return symbol.New(i.Object).String() +} - return fmt.Sprintf("%s<%s>", sym, i.TArgs) +// qualifiedName returns a string representation of the instance's name +// including the package name but +// excluding the type parameters and pointer indicators. +func (i Instance) qualifiedName() string { + if i.Object == nil { + return `` + } + if i.Object.Pkg() == nil { + return i.Object.Name() + } + return fmt.Sprintf("%s.%s", i.Object.Pkg().Name(), i.Object.Name()) } -// TypeString returns a Go type string representing the instance (suitable for %T verb). -func (i *Instance) TypeString() string { - tArgs := "" - if len(i.TArgs) > 0 { - tArgs = "[" + i.TArgs.String() + "]" +// TypeParamsString returns part of a Go type string that represents the type +// parameters of the instance including the nesting type parameters, e.g. [X;Y,Z]. +func (i Instance) TypeParamsString(open, close string) string { + hasNest := len(i.TNest) > 0 + hasArgs := len(i.TArgs) > 0 + buf := strings.Builder{} + if hasNest || hasArgs { + buf.WriteString(open) + if hasNest { + buf.WriteString(i.TNest.String()) + buf.WriteRune(';') + if hasArgs { + buf.WriteRune(' ') + } + } + if hasArgs { + buf.WriteString(i.TArgs.String()) + } + buf.WriteString(close) } - return fmt.Sprintf("%s.%s%s", i.Object.Pkg().Name(), i.Object.Name(), tArgs) + return buf.String() } -// IsTrivial returns true if this is an instance of a non-generic object. -func (i *Instance) IsTrivial() bool { - return len(i.TArgs) == 0 +// IsTrivial returns true if this is an instance of a non-generic object +// and it is not nested in a generic function. +func (i Instance) IsTrivial() bool { + return len(i.TArgs) == 0 && len(i.TNest) == 0 } // Recv returns an instance of the receiver type of a method. // // Returns zero value if not a method. -func (i *Instance) Recv() Instance { +func (i Instance) Recv() Instance { sig, ok := i.Object.Type().(*types.Signature) if !ok { return Instance{} @@ -159,6 +203,17 @@ func (iset *InstanceSet) ForObj(obj types.Object) []Instance { return result } +// ObjHasInstances returns true if there are any instances (either trivial +// or non-trivial) that belong to the given object type, otherwise false. +func (iset *InstanceSet) ObjHasInstances(obj types.Object) bool { + for _, inst := range iset.values { + if inst.Object == obj { + return true + } + } + return false +} + // PackageInstanceSets stores an InstanceSet for each package in a program, keyed // by import path. type PackageInstanceSets map[string]*InstanceSet diff --git a/compiler/internal/typeparams/map.go b/compiler/internal/typeparams/map.go index dbe07a54e..7edbdc016 100644 --- a/compiler/internal/typeparams/map.go +++ b/compiler/internal/typeparams/map.go @@ -38,9 +38,9 @@ type InstanceMap[V any] struct { // If the given key isn't found, an empty bucket and -1 are returned. func (im *InstanceMap[V]) findIndex(key Instance) (mapBucket[V], int) { if im != nil && im.data != nil { - bucket := im.data[key.Object][typeHash(im.hasher, key.TArgs...)] + bucket := im.data[key.Object][typeHash(im.hasher, key.TNest, key.TArgs)] for i, candidate := range bucket { - if candidate != nil && candidate.key.TArgs.Equal(key.TArgs) { + if candidateArgsMatch(key, candidate) { return bucket, i } } @@ -82,7 +82,7 @@ func (im *InstanceMap[V]) Set(key Instance, value V) V { if _, ok := im.data[key.Object]; !ok { im.data[key.Object] = mapBuckets[V]{} } - bucketID := typeHash(im.hasher, key.TArgs...) + bucketID := typeHash(im.hasher, key.TNest, key.TArgs) // If there is already an identical key in the map, override the entry value. hole := -1 @@ -90,7 +90,7 @@ func (im *InstanceMap[V]) Set(key Instance, value V) V { for i, candidate := range bucket { if candidate == nil { hole = i - } else if candidate.key.TArgs.Equal(key.TArgs) { + } else if candidateArgsMatch(key, candidate) { old := candidate.value candidate.value = value return old @@ -180,13 +180,24 @@ func (im *InstanceMap[V]) String() string { return `{` + strings.Join(entries, `, `) + `}` } +// candidateArgsMatch checks if the candidate entry has the same type +// arguments as the given key. +func candidateArgsMatch[V any](key Instance, candidate *mapEntry[V]) bool { + return candidate != nil && + candidate.key.TNest.Equal(key.TNest) && + candidate.key.TArgs.Equal(key.TArgs) +} + // typeHash returns a combined hash of several types. // // Provided hasher is used to compute hashes of individual types, which are // xor'ed together. Xor preserves bit distribution property, so the combined // hash should be as good for bucketing, as the original. -func typeHash(hasher typeutil.Hasher, types ...types.Type) uint32 { +func typeHash(hasher typeutil.Hasher, nestTypes, types []types.Type) uint32 { var hash uint32 + for _, typ := range nestTypes { + hash ^= hasher.Hash(typ) + } for _, typ := range types { hash ^= hasher.Hash(typ) } diff --git a/compiler/internal/typeparams/map_test.go b/compiler/internal/typeparams/map_test.go index baa31f64a..d67a1884d 100644 --- a/compiler/internal/typeparams/map_test.go +++ b/compiler/internal/typeparams/map_test.go @@ -7,8 +7,10 @@ import ( ) func TestInstanceMap(t *testing.T) { + pkg := types.NewPackage(`testPkg`, `testPkg`) + i1 := Instance{ - Object: types.NewTypeName(token.NoPos, nil, "i1", nil), + Object: types.NewTypeName(token.NoPos, pkg, "i1", nil), TArgs: []types.Type{ types.Typ[types.Int], types.Typ[types.Int8], @@ -23,7 +25,7 @@ func TestInstanceMap(t *testing.T) { } i2 := Instance{ - Object: types.NewTypeName(token.NoPos, nil, "i2", nil), // Different pointer. + Object: types.NewTypeName(token.NoPos, pkg, "i2", nil), // Different pointer. TArgs: []types.Type{ types.Typ[types.Int], types.Typ[types.Int8], @@ -70,7 +72,7 @@ func TestInstanceMap(t *testing.T) { if got := m.Len(); got != 1 { t.Errorf("Got: map length %d. Want: 1.", got) } - if got, want := m.String(), `{{type i1 int, int8}:abc}`; got != want { + if got, want := m.String(), `{testPkg.i1:abc}`; got != want { t.Errorf("Got: map string %q. Want: map string %q.", got, want) } if got, want := m.Keys(), []Instance{i1}; !keysMatch(got, want) { @@ -95,7 +97,7 @@ func TestInstanceMap(t *testing.T) { if got := m.Get(i1clone); got != "def" { t.Errorf(`Got: getting set key returned %q. Want: "def"`, got) } - if got, want := m.String(), `{{type i1 int, int8}:def}`; got != want { + if got, want := m.String(), `{testPkg.i1:def}`; got != want { t.Errorf("Got: map string %q. Want: map string %q.", got, want) } if got, want := m.Keys(), []Instance{i1}; !keysMatch(got, want) { @@ -165,7 +167,7 @@ func TestInstanceMap(t *testing.T) { if got := m.Len(); got != 5 { t.Errorf("Got: map length %d. Want: 5.", got) } - if got, want := m.String(), `{{type i1 int, int8}:def, {type i1 int, int}:456, {type i1 string, string}:789, {type i1 }:ghi, {type i2 int, int8}:123}`; got != want { + if got, want := m.String(), `{testPkg.i1:ghi, testPkg.i1:def, testPkg.i1:456, testPkg.i1:789, testPkg.i2:123}`; got != want { t.Errorf("Got: map string %q. Want: map string %q.", got, want) } if got, want := m.Keys(), []Instance{i1, i2, i3, i4, i5}; !keysMatch(got, want) { diff --git a/compiler/internal/typeparams/utils.go b/compiler/internal/typeparams/utils.go index 6930fbf23..ea528314e 100644 --- a/compiler/internal/typeparams/utils.go +++ b/compiler/internal/typeparams/utils.go @@ -3,6 +3,7 @@ package typeparams import ( "errors" "fmt" + "go/token" "go/types" ) @@ -19,6 +20,31 @@ func SignatureTypeParams(sig *types.Signature) *types.TypeParamList { } } +// FindNestingFunc returns the function or method that the given object +// is nested in, or nil if the object was defined at the package level. +func FindNestingFunc(obj types.Object) *types.Func { + objPos := obj.Pos() + if objPos == token.NoPos { + return nil + } + + scope := obj.Parent() + for scope != nil { + // Iterate over all declarations in the scope. + for _, name := range scope.Names() { + decl := scope.Lookup(name) + if fn, ok := decl.(*types.Func); ok { + // Check if the object's position is within the function's scope. + if objPos >= fn.Pos() && objPos <= fn.Scope().End() { + return fn + } + } + } + scope = scope.Parent() + } + return nil +} + var ( errInstantiatesGenerics = errors.New("instantiates generic type or function") errDefinesGenerics = errors.New("defines generic type or function") @@ -58,3 +84,58 @@ func RequiresGenericsSupport(info *types.Info) error { return nil } + +// isGeneric will search all the given types and their subtypes for a +// *types.TypeParam. This will not check if a type could be generic, +// but if each instantiation is not completely concrete yet. +// +// This is useful to check for generics types like `X[B[T]]`, where +// `X` appears concrete because it is instantiated with the type argument `B[T]`, +// however the `T` inside `B[T]` is a type parameter making `X[B[T]]` a generic +// type since it required instantiation to a concrete type, e.g. `X[B[int]]`. +func isGeneric(typ ...types.Type) bool { + var containsTypeParam func(t types.Type) bool + + foreach := func(count int, getter func(index int) types.Type) bool { + for i := 0; i < count; i++ { + if containsTypeParam(getter(i)) { + return true + } + } + return false + } + + seen := make(map[types.Type]struct{}) + containsTypeParam = func(t types.Type) bool { + if _, ok := seen[t]; ok { + return false + } + seen[t] = struct{}{} + + switch t := t.(type) { + case *types.TypeParam: + return true + case *types.Named: + return t.TypeParams().Len() != t.TypeArgs().Len() || + foreach(t.TypeArgs().Len(), func(i int) types.Type { return t.TypeArgs().At(i) }) || + containsTypeParam(t.Underlying()) + case *types.Struct: + return foreach(t.NumFields(), func(i int) types.Type { return t.Field(i).Type() }) + case *types.Interface: + return foreach(t.NumMethods(), func(i int) types.Type { return t.Method(i).Type() }) + case *types.Signature: + return foreach(t.Params().Len(), func(i int) types.Type { return t.Params().At(i).Type() }) || + foreach(t.Results().Len(), func(i int) types.Type { return t.Results().At(i).Type() }) + case *types.Map: + return containsTypeParam(t.Key()) || containsTypeParam(t.Elem()) + case interface{ Elem() types.Type }: + // Handles *types.Pointer, *types.Slice, *types.Array, *types.Chan + return containsTypeParam(t.Elem()) + default: + // Other types (e.g., basic types) do not contain type parameters. + return false + } + } + + return foreach(len(typ), func(i int) types.Type { return typ[i] }) +} diff --git a/compiler/package.go b/compiler/package.go index 8f336130d..bb94962da 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -323,11 +323,17 @@ func (fc *funcContext) initArgs(ty types.Type) string { if !field.Exported() { pkgPath = field.Pkg().Path() } - fields[i] = fmt.Sprintf(`{prop: "%s", name: %s, embedded: %t, exported: %t, typ: %s, tag: %s}`, fieldName(t, i), encodeString(field.Name()), field.Anonymous(), field.Exported(), fc.typeName(field.Type()), encodeString(t.Tag(i))) + ft := fc.fieldType(t, i) + fields[i] = fmt.Sprintf(`{prop: "%s", name: %s, embedded: %t, exported: %t, typ: %s, tag: %s}`, + fieldName(t, i), encodeString(field.Name()), field.Anonymous(), field.Exported(), fc.typeName(ft), encodeString(t.Tag(i))) } return fmt.Sprintf(`"%s", [%s]`, pkgPath, strings.Join(fields, ", ")) case *types.TypeParam: - err := bailout(fmt.Errorf(`%v has unexpected generic type parameter %T`, ty, ty)) + tr := fc.typeResolver.Substitute(ty) + if tr != ty { + return fc.initArgs(tr) + } + err := bailout(fmt.Errorf(`"%v" has unexpected generic type parameter %T`, ty, ty)) panic(err) default: err := bailout(fmt.Errorf("%v has unexpected type %T", ty, ty)) diff --git a/compiler/utils.go b/compiler/utils.go index 83b826ce2..7d286f447 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -198,7 +198,7 @@ func (fc *funcContext) translateSelection(sel typesutil.Selection, pos token.Pos jsFieldName := s.Field(index).Name() for { fields = append(fields, fieldName(s, 0)) - ft := s.Field(0).Type() + ft := fc.fieldType(s, 0) if typesutil.IsJsObject(ft) { return fields, jsTag } @@ -215,7 +215,7 @@ func (fc *funcContext) translateSelection(sel typesutil.Selection, pos token.Pos } } fields = append(fields, fieldName(s, index)) - t = s.Field(index).Type() + t = fc.fieldType(s, index) } return fields, "" } @@ -441,13 +441,16 @@ func (fc *funcContext) objectName(o types.Object) string { // knownInstances returns a list of known instantiations of the object. // -// For objects without type params always returns a single trivial instance. +// For objects without type params and not nested in a generic function or +// method, this always returns a single trivial instance. +// If the object is generic, or in a generic function or method, but there are +// no instances, then the object is unused and an empty list is returned. func (fc *funcContext) knownInstances(o types.Object) []typeparams.Instance { - if !typeparams.HasTypeParams(o.Type()) { + instances := fc.pkgCtx.instanceSet.Pkg(o.Pkg()).ForObj(o) + if len(instances) == 0 && !typeparams.HasTypeParams(o.Type()) { return []typeparams.Instance{{Object: o}} } - - return fc.pkgCtx.instanceSet.Pkg(o.Pkg()).ForObj(o) + return instances } // instName returns a JS expression that refers to the provided instance of a @@ -459,7 +462,8 @@ func (fc *funcContext) instName(inst typeparams.Instance) string { return objName } fc.pkgCtx.DeclareDCEDep(inst.Object, inst.TArgs...) - return fmt.Sprintf("%s[%d /* %v */]", objName, fc.pkgCtx.instanceSet.ID(inst), inst.TArgs) + label := inst.TypeParamsString(` /* `, ` */`) + return fmt.Sprintf("%s[%d%s]", objName, fc.pkgCtx.instanceSet.ID(inst), label) } // methodName returns a JS identifier (specifically, object property name) @@ -504,14 +508,31 @@ func (fc *funcContext) typeName(ty types.Type) string { return "$error" } inst := typeparams.Instance{Object: t.Obj()} + + // Get type arguments for the type if there are any. for i := 0; i < t.TypeArgs().Len(); i++ { inst.TArgs = append(inst.TArgs, t.TypeArgs().At(i)) } + + // Get the nesting type arguments if there are any. + if fn := typeparams.FindNestingFunc(t.Obj()); fn != nil { + if fn.Scope().Contains(t.Obj().Pos()) { + tp := typeparams.SignatureTypeParams(fn.Type().(*types.Signature)) + tNest := make([]types.Type, tp.Len()) + for i := 0; i < tp.Len(); i++ { + tNest[i] = fc.typeResolver.Substitute(tp.At(i)) + } + inst.TNest = typesutil.TypeList(tNest) + } + } + return fc.instName(inst) case *types.Interface: if t.Empty() { return "$emptyInterface" } + case *types.TypeParam: + panic(fmt.Errorf("unexpected type parameter: %v", t)) } // For anonymous composite types, generate a synthetic package-level type @@ -575,6 +596,12 @@ func (fc *funcContext) typeOf(expr ast.Expr) types.Type { return fc.typeResolver.Substitute(typ) } +// fieldType returns the type of the i-th field of the given struct +// after substituting type parameters with concrete types for nested context. +func (fc *funcContext) fieldType(t *types.Struct, i int) types.Type { + return fc.typeResolver.Substitute(t.Field(i).Type()) +} + func (fc *funcContext) selectionOf(e *ast.SelectorExpr) (typesutil.Selection, bool) { if sel, ok := fc.pkgCtx.Selections[e]; ok { return fc.typeResolver.SubstituteSelection(sel), true diff --git a/internal/govendor/subst/export.go b/internal/govendor/subst/export.go index 38e394bda..00a77ca49 100644 --- a/internal/govendor/subst/export.go +++ b/internal/govendor/subst/export.go @@ -4,6 +4,7 @@ package subst import ( + "fmt" "go/types" ) @@ -17,33 +18,33 @@ type Subster struct { } // New creates a new Subster with a given list of type parameters and matching args. -func New(tc *types.Context, tParams []*types.TypeParam, tArgs []types.Type) *Subster { - assert(len(tParams) == len(tArgs), "New() argument count must match") +func New(tc *types.Context, tParams *types.TypeParamList, tArgs []types.Type) *Subster { + if tParams.Len() != len(tArgs) { + panic(fmt.Errorf("number of type parameters and arguments must match: %d => %d", tParams.Len(), len(tArgs))) + } - if len(tParams) == 0 { + if tParams.Len() == 0 && len(tArgs) == 0 { return nil } - subst := &subster{ - replacements: make(map[*types.TypeParam]types.Type, len(tParams)), - cache: make(map[types.Type]types.Type), - ctxt: tc, - scope: nil, - debug: false, - } - for i := 0; i < len(tParams); i++ { - subst.replacements[tParams[i]] = tArgs[i] - } - return &Subster{ - impl: subst, - } + subst := makeSubster(tc, nil, tParams, tArgs, false) + return &Subster{impl: subst} } -// Type returns a version of typ with all references to type parameters replaced -// with the corresponding type arguments. +// Type returns a version of typ with all references to type parameters +// replaced with the corresponding type arguments. func (s *Subster) Type(typ types.Type) types.Type { if s == nil { return typ } return s.impl.typ(typ) } + +// Types returns a version of ts with all references to type parameters +// replaced with the corresponding type arguments. +func (s *Subster) Types(ts []types.Type) []types.Type { + if s == nil { + return ts + } + return s.impl.types(ts) +} diff --git a/internal/govendor/subst/subst.go b/internal/govendor/subst/subst.go index 9020e94f9..825e3c7f1 100644 --- a/internal/govendor/subst/subst.go +++ b/internal/govendor/subst/subst.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Copy of https://cs.opensource.google/go/x/tools/+/refs/tags/v0.17.0:go/ssa/subst.go +// Any changes to this copy are labelled with GOPHERJS. package subst import ( @@ -81,9 +83,12 @@ func (subst *subster) typ(t types.Type) (res types.Type) { // fall through if result r will be identical to t, types.Identical(r, t). switch t := t.(type) { case *types.TypeParam: - r := subst.replacements[t] - assert(r != nil, "type param without replacement encountered") - return r + // GOPHERJS: Replaced an assert that was causing a panic for nested types with code from + // https://cs.opensource.google/go/x/tools/+/refs/tags/v0.33.0:go/ssa/subst.go;l=92 + if r := subst.replacements[t]; r != nil { + return r + } + return t case *types.Basic: return t diff --git a/internal/govendor/subst/subst_test.go b/internal/govendor/subst/subst_test.go index 53fadbcf0..832f0ebd4 100644 --- a/internal/govendor/subst/subst_test.go +++ b/internal/govendor/subst/subst_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Copy of https://cs.opensource.google/go/x/tools/+/refs/tags/v0.17.0:go/ssa/subst_test.go package subst import ( diff --git a/internal/govendor/subst/util.go b/internal/govendor/subst/util.go index 22072e39f..5b55c0310 100644 --- a/internal/govendor/subst/util.go +++ b/internal/govendor/subst/util.go @@ -6,18 +6,16 @@ package subst import "go/types" -// This file defines a number of miscellaneous utility functions. - -//// Sanity checking utilities - // assert panics with the mesage msg if p is false. // Avoid combining with expensive string formatting. +// From https://cs.opensource.google/go/x/tools/+/refs/tags/v0.17.0:go/ssa/util.go;l=27 func assert(p bool, msg string) { if !p { panic(msg) } } +// From https://cs.opensource.google/go/x/tools/+/refs/tags/v0.33.0:go/ssa/wrappers.go;l=262 func changeRecv(s *types.Signature, recv *types.Var) *types.Signature { return types.NewSignatureType(recv, nil, nil, s.Params(), s.Results(), s.Variadic()) } diff --git a/internal/srctesting/srctesting.go b/internal/srctesting/srctesting.go index bf74bce51..e4242991c 100644 --- a/internal/srctesting/srctesting.go +++ b/internal/srctesting/srctesting.go @@ -158,6 +158,9 @@ func LookupObj(pkg *types.Package, name string) types.Object { for len(path) > 0 { obj = scope.Lookup(path[0]) + if obj == nil { + panic(fmt.Sprintf("failed to find %q in %q", path[0], name)) + } path = path[1:] if fun, ok := obj.(*types.Func); ok { @@ -170,6 +173,9 @@ func LookupObj(pkg *types.Package, name string) types.Object { if len(path) > 0 { obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), path[0]) path = path[1:] + if fun, ok := obj.(*types.Func); ok { + scope = fun.Scope() + } } } return obj From f5ca11fb8baadbdeb39c7c5cb7c1d88bb82824be Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Wed, 28 May 2025 11:24:42 -0600 Subject: [PATCH 46/58] Handling deep nested types --- compiler/decls.go | 13 +- compiler/expressions.go | 2 +- compiler/functions.go | 10 +- compiler/internal/analysis/info.go | 6 +- compiler/internal/typeparams/collect.go | 179 +++------------ compiler/internal/typeparams/collect_test.go | 190 ++++++++-------- compiler/internal/typeparams/instance.go | 27 +++ compiler/internal/typeparams/resolver.go | 215 +++++++++++++++++++ compiler/internal/typeparams/utils.go | 44 +++- internal/govendor/subst/export.go | 19 +- tests/gorepo/run.go | 10 +- 11 files changed, 424 insertions(+), 291 deletions(-) create mode 100644 compiler/internal/typeparams/resolver.go diff --git a/compiler/decls.go b/compiler/decls.go index eb95cd2f7..5c6291ba6 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -451,13 +451,7 @@ func (fc *funcContext) newNamedTypeVarDecl(obj *types.TypeName) *Decl { func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, error) { originType := inst.Object.Type().(*types.Named) - var nestResolver *typeparams.Resolver - if len(inst.TNest) > 0 { - fn := typeparams.FindNestingFunc(inst.Object) - tp := typeparams.SignatureTypeParams(fn.Type().(*types.Signature)) - nestResolver = typeparams.NewResolver(fc.pkgCtx.typesCtx, tp, inst.TNest, nil) - } - fc.typeResolver = typeparams.NewResolver(fc.pkgCtx.typesCtx, originType.TypeParams(), inst.TArgs, nestResolver) + fc.typeResolver = typeparams.NewResolver(fc.pkgCtx.typesCtx, inst) defer func() { fc.typeResolver = nil }() instanceType := originType @@ -469,10 +463,7 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er } instanceType = instantiated.(*types.Named) } - if len(inst.TNest) > 0 { - instantiated := nestResolver.Substitute(instanceType) - instanceType = instantiated.(*types.Named) - } + instanceType = fc.typeResolver.Substitute(instanceType).(*types.Named) } underlying := instanceType.Underlying() diff --git a/compiler/expressions.go b/compiler/expressions.go index 781a37a3e..455549a1d 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -531,7 +531,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case *types.Signature: return fc.formatExpr("%s", fc.instName(fc.instanceOf(e.X.(*ast.Ident)))) default: - panic(fmt.Errorf(`unhandled IndexExpr: %T`, t)) + panic(fmt.Errorf(`unhandled IndexExpr: %T in %T`, t, fc.typeOf(e.X))) } case *ast.IndexListExpr: switch t := fc.typeOf(e.X).Underlying().(type) { diff --git a/compiler/functions.go b/compiler/functions.go index 361c92f0f..d4484101b 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -48,13 +48,9 @@ func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, inst typep c.allVars[k] = v } - if sig.TypeParams().Len() > 0 { - c.typeResolver = typeparams.NewResolver(c.pkgCtx.typesCtx, sig.TypeParams(), inst.TArgs, nil) - } else if sig.RecvTypeParams().Len() > 0 { - c.typeResolver = typeparams.NewResolver(c.pkgCtx.typesCtx, sig.RecvTypeParams(), inst.TArgs, nil) - } - if c.objectNames == nil { - c.objectNames = map[types.Object]string{} + // Use the parent function's resolver unless the function has it's own type arguments. + if !inst.IsTrivial() { + c.typeResolver = typeparams.NewResolver(fc.pkgCtx.typesCtx, inst) } // Synthesize an identifier by which the function may reference itself. Since diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go index e400c870c..e55b66245 100644 --- a/compiler/internal/analysis/info.go +++ b/compiler/internal/analysis/info.go @@ -124,11 +124,7 @@ func (info *Info) newFuncInfoInstances(fd *ast.FuncDecl) []*FuncInfo { funcInfos := make([]*FuncInfo, 0, len(instances)) for _, inst := range instances { - var resolver *typeparams.Resolver - if sig, ok := obj.Type().(*types.Signature); ok { - tp := typeparams.SignatureTypeParams(sig) - resolver = typeparams.NewResolver(info.typeCtx, tp, inst.TArgs, nil) - } + resolver := typeparams.NewResolver(info.typeCtx, inst) fi := info.newFuncInfo(fd, inst.Object, inst.TArgs, resolver) funcInfos = append(funcInfos, fi) } diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index 940690e83..f18e6eb20 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -4,143 +4,8 @@ import ( "fmt" "go/ast" "go/types" - "strings" - - "github.com/gopherjs/gopherjs/compiler/typesutil" - "github.com/gopherjs/gopherjs/internal/govendor/subst" ) -// Resolver translates types defined in terms of type parameters into concrete -// types, given a mapping from type params to type arguments. -type Resolver struct { - tParams *types.TypeParamList - tArgs []types.Type - parent *Resolver - - // subster is the substitution helper that will perform the actual - // substitutions. This maybe nil when there are no substitutions but - // will still usable when nil. - subster *subst.Subster - selMemo map[typesutil.Selection]typesutil.Selection -} - -// NewResolver creates a new Resolver with tParams entries mapping to tArgs -// entries with the same index. -func NewResolver(tc *types.Context, tParams *types.TypeParamList, tArgs []types.Type, parent *Resolver) *Resolver { - r := &Resolver{ - tParams: tParams, - tArgs: tArgs, - parent: parent, - subster: subst.New(tc, tParams, tArgs), - selMemo: map[typesutil.Selection]typesutil.Selection{}, - } - return r -} - -// TypeParams is the list of type parameters that this resolver -// (not any parent) will substitute. -func (r *Resolver) TypeParams() *types.TypeParamList { - if r == nil { - return nil - } - return r.tParams -} - -// TypeArgs is the list of type arguments that this resolver -// (not any parent) will resolve to. -func (r *Resolver) TypeArgs() []types.Type { - if r == nil { - return nil - } - return r.tArgs -} - -// Parent is the resolver for the function or method that this resolver -// is nested in. This may be nil if the context for this resolver is not -// nested in another generic function or method. -func (r *Resolver) Parent() *Resolver { - if r == nil { - return nil - } - return r.parent -} - -// Substitute replaces references to type params in the provided type definition -// with the corresponding concrete types. -func (r *Resolver) Substitute(typ types.Type) types.Type { - if r == nil || typ == nil { - return typ // No substitutions to be made. - } - typ = r.subster.Type(typ) - typ = r.parent.Substitute(typ) - return typ -} - -// SubstituteAll same as Substitute, but accepts a TypeList are returns -// substitution results as a slice in the same order. -func (r *Resolver) SubstituteAll(list *types.TypeList) []types.Type { - result := make([]types.Type, list.Len()) - for i := range result { - result[i] = r.Substitute(list.At(i)) - } - return result -} - -// SubstituteSelection replaces a method of field selection on a generic type -// defined in terms of type parameters with a method selection on a concrete -// instantiation of the type. -func (r *Resolver) SubstituteSelection(sel typesutil.Selection) typesutil.Selection { - if r == nil || sel == nil { - return sel // No substitutions to be made. - } - if concrete, ok := r.selMemo[sel]; ok { - return concrete - } - - switch sel.Kind() { - case types.MethodExpr, types.MethodVal, types.FieldVal: - recv := r.Substitute(sel.Recv()) - if types.Identical(recv, sel.Recv()) { - return sel // Non-generic receiver, no substitution necessary. - } - - // Look up the method on the instantiated receiver. - pkg := sel.Obj().Pkg() - obj, index, _ := types.LookupFieldOrMethod(recv, true, pkg, sel.Obj().Name()) - if obj == nil { - panic(fmt.Errorf("failed to lookup field %q in type %v", sel.Obj().Name(), recv)) - } - typ := obj.Type() - - if sel.Kind() == types.MethodExpr { - typ = typesutil.RecvAsFirstArg(typ.(*types.Signature)) - } - concrete := typesutil.NewSelection(sel.Kind(), recv, index, obj, typ) - r.selMemo[sel] = concrete - return concrete - default: - panic(fmt.Errorf("unexpected selection kind %v: %v", sel.Kind(), sel)) - } -} - -// String gets a strings representation of the resolver for debugging. -func (r *Resolver) String() string { - if r == nil { - return `{}` - } - - parts := make([]string, 0, len(r.tArgs)) - for i, ta := range r.tArgs { - parts = append(parts, fmt.Sprintf("%s->%s", r.tParams.At(i), ta)) - } - - nestStr := `` - if r.parent != nil { - nestStr = r.parent.String() + `:` - } - return nestStr + `{` + strings.Join(parts, `, `) + `}` -} - // visitor implements ast.Visitor and collects instances of generic types and // functions into an InstanceSet. // @@ -151,7 +16,9 @@ type visitor struct { instances *PackageInstanceSets resolver *Resolver info *types.Info - tNest []types.Type // The type arguments for a nested context. + + nestTParams *types.TypeParamList // The type parameters for a nested context. + nestTArgs []types.Type // The type arguments for a nested context. } var _ ast.Visitor = &visitor{} @@ -195,12 +62,14 @@ func (c *visitor) visitInstance(ident *ast.Ident, inst types.Instance) { // If the object is defined in the same scope as the instance, // then we apply the current nested type arguments. - var tNest []types.Type + var nestTParams *types.TypeParamList + var nestTArgs []types.Type if obj.Parent().Contains(ident.Pos()) { - tNest = c.tNest + nestTParams = c.nestTParams + nestTArgs = c.nestTArgs } - c.addInstance(obj, tArgs, tNest) + c.addInstance(obj, tArgs, nestTParams, nestTArgs) } func (c *visitor) visitNestedType(obj types.Object) { @@ -222,12 +91,12 @@ func (c *visitor) visitNestedType(obj types.Object) { return } - c.addInstance(obj, nil, c.resolver.TypeArgs()) + c.addInstance(obj, nil, c.resolver.TypeParams(), c.resolver.TypeArgs()) } -func (c *visitor) addInstance(obj types.Object, tArgList *types.TypeList, tNest []types.Type) { +func (c *visitor) addInstance(obj types.Object, tArgList *types.TypeList, nestTParams *types.TypeParamList, nestTArgs []types.Type) { tArgs := c.resolver.SubstituteAll(tArgList) - if isGeneric(tArgs...) { + if isGeneric(nestTParams, tArgs) { // Skip any instances that still have type parameters in them after // substitution. This occurs when a type is defined while nested // in a generic context and is not fully instantiated yet. @@ -238,7 +107,7 @@ func (c *visitor) addInstance(obj types.Object, tArgList *types.TypeList, tNest c.instances.Add(Instance{ Object: obj, TArgs: tArgs, - TNest: tNest, + TNest: nestTArgs, }) if t, ok := obj.Type().(*types.Named); ok { @@ -247,7 +116,7 @@ func (c *visitor) addInstance(obj types.Object, tArgList *types.TypeList, tNest c.instances.Add(Instance{ Object: method.Origin(), TArgs: tArgs, - TNest: tNest, + TNest: nestTArgs, }) } } @@ -358,12 +227,13 @@ func (c *Collector) Scan(pkg *types.Package, files ...*ast.File) { } func (c *Collector) scanSignature(inst Instance, typ *types.Signature, objMap map[types.Object]ast.Node) { - tParams := SignatureTypeParams(typ) v := visitor{ instances: c.Instances, - resolver: NewResolver(c.TContext, tParams, inst.TArgs, nil), + resolver: NewResolver(c.TContext, inst), info: c.Info, - tNest: inst.TArgs, + + nestTParams: SignatureTypeParams(typ), + nestTArgs: inst.TArgs, } ast.Walk(&v, objMap[inst.Object]) } @@ -377,18 +247,19 @@ func (c *Collector) scanNamed(inst Instance, typ *types.Named, objMap map[types. return } - var nestResolver *Resolver - if len(inst.TNest) > 0 { - fn := FindNestingFunc(inst.Object) - tp := SignatureTypeParams(fn.Type().(*types.Signature)) - nestResolver = NewResolver(c.TContext, tp, inst.TNest, nil) + var nestTParams *types.TypeParamList + nest := FindNestingFunc(obj) + if nest != nil { + nestTParams = SignatureTypeParams(nest.Type().(*types.Signature)) } v := visitor{ instances: c.Instances, - resolver: NewResolver(c.TContext, typ.TypeParams(), inst.TArgs, nestResolver), + resolver: NewResolver(c.TContext, inst), info: c.Info, - tNest: inst.TNest, + + nestTParams: nestTParams, + nestTArgs: inst.TNest, } ast.Walk(&v, node) } diff --git a/compiler/internal/typeparams/collect_test.go b/compiler/internal/typeparams/collect_test.go index 6864e5ead..26791ea59 100644 --- a/compiler/internal/typeparams/collect_test.go +++ b/compiler/internal/typeparams/collect_test.go @@ -218,10 +218,10 @@ func TestVisitor(t *testing.T) { descr: "generic function", resolver: NewResolver( types.NewContext(), - lookupType("entry2").(*types.Signature).TypeParams(), - []types.Type{lookupType("B")}, - nil, - ), + Instance{ + Object: lookupObj("entry2"), + TArgs: []types.Type{lookupType("B")}, + }), node: lookupDecl("entry2"), want: append( instancesInFunc(lookupType("B")), @@ -244,10 +244,10 @@ func TestVisitor(t *testing.T) { descr: "generic method", resolver: NewResolver( types.NewContext(), - lookupType("entry3.method").(*types.Signature).RecvTypeParams(), - []types.Type{lookupType("C")}, - nil, - ), + Instance{ + Object: lookupObj("entry3.method"), + TArgs: []types.Type{lookupType("C")}, + }), node: lookupDecl("entry3.method"), want: append( instancesInFunc(lookupType("C")), @@ -278,10 +278,10 @@ func TestVisitor(t *testing.T) { descr: "generic type declaration", resolver: NewResolver( types.NewContext(), - lookupType("entry3").(*types.Named).TypeParams(), - []types.Type{lookupType("D")}, - nil, - ), + Instance{ + Object: lookupObj("entry3"), + TArgs: []types.Type{lookupType("D")}, + }), node: lookupDecl("entry3"), want: instancesInType(lookupType("D")), }, { @@ -316,7 +316,8 @@ func TestVisitor(t *testing.T) { if test.resolver != nil { // Since we know all the tests are for functions and methods, // set the nested type to the type parameter from the resolver. - v.tNest = test.resolver.tArgs + v.nestTParams = test.resolver.tParams + v.nestTArgs = test.resolver.tArgs } ast.Walk(&v, test.node) got := v.instances.Pkg(pkg).Values() @@ -534,6 +535,12 @@ func TestCollector_NestingWithVars(t *testing.T) { f := srctesting.New(t) file := f.Parse(`test.go`, src) info, pkg := f.Check(`pkg/test`, file) + inst := func(name, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } c := Collector{ TContext: types.NewContext(), @@ -542,16 +549,9 @@ func TestCollector_NestingWithVars(t *testing.T) { } c.Scan(pkg, file) - inst := func(name, tNest, tArg string) Instance { - return Instance{ - Object: srctesting.LookupObj(pkg, name), - TNest: evalTypeArgs(t, f.FileSet, pkg, tNest), - TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), - } - } want := []Instance{ - inst(`S`, ``, `int`), - inst(`S.echo`, ``, `int`), + inst(`S`, `int`), + inst(`S.echo`, `int`), } got := c.Instances.Pkg(pkg).Values() if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { @@ -571,33 +571,34 @@ func TestCollector_RecursiveTypeParams(t *testing.T) { ` f := srctesting.New(t) + tc := types.NewContext() file := f.Parse(`test.go`, src) info, pkg := f.Check(`test`, file) + inst := func(name, tNest, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TNest: evalTypeArgs(t, f.FileSet, pkg, tNest), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } c := Collector{ - TContext: types.NewContext(), + TContext: tc, Info: info, Instances: &PackageInstanceSets{}, } c.Scan(pkg, file) - tInt := types.Typ[types.Int] - xAny := srctesting.LookupObj(pkg, `main.X`) - xInt, err := types.Instantiate(types.NewContext(), xAny.Type(), []types.Type{tInt}, true) - if err != nil { - t.Fatalf("Failed to instantiate X[int]: %v", err) - } - + xInst := inst(`main.X`, ``, `int`) + xInt := xInst.Resolve(tc) want := []Instance{ + xInst, { Object: srctesting.LookupObj(pkg, `F`), TArgs: []types.Type{xInt}, }, { Object: srctesting.LookupObj(pkg, `main.U`), TArgs: []types.Type{xInt}, - }, { - Object: xAny, - TArgs: []types.Type{tInt}, }, } got := c.Instances.Pkg(pkg).Values() @@ -607,13 +608,6 @@ func TestCollector_RecursiveTypeParams(t *testing.T) { } func TestCollector_NestedRecursiveTypeParams(t *testing.T) { - t.Skip(`Skipping test due to known issue with nested recursive type parameters.`) - // TODO(grantnelson-wf): This test is failing because the type parameters - // inside of U are not being resolved to concrete types. This is because - // when instantiating X in the collector, we are not resolving the - // nested type of U that is X's type argument. This leave the A in U - // as a type parameter instead of resolving it to string. - // This is based off of part of go1.19.13/test/typeparam/nested.go src := `package test func F[A any]() any { @@ -627,38 +621,72 @@ func TestCollector_NestedRecursiveTypeParams(t *testing.T) { ` f := srctesting.New(t) + tc := types.NewContext() file := f.Parse(`test.go`, src) info, pkg := f.Check(`test`, file) + inst := func(name, tNest, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TNest: evalTypeArgs(t, f.FileSet, pkg, tNest), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } c := Collector{ - TContext: types.NewContext(), + TContext: tc, Info: info, Instances: &PackageInstanceSets{}, } c.Scan(pkg, file) - xAny := srctesting.LookupObj(pkg, `F.X`) - xInt, err := types.Instantiate(types.NewContext(), xAny.Type(), []types.Type{types.Typ[types.Int]}, true) - if err != nil { - t.Fatalf("Failed to instantiate X[int]: %v", err) + xInst := inst(`F.X`, `string`, `int`) + xInt := xInst.Resolve(tc) + want := []Instance{ + inst(`F`, ``, `string`), + xInst, + { + Object: srctesting.LookupObj(pkg, `F.U`), + TNest: []types.Type{types.Typ[types.String]}, + TArgs: []types.Type{xInt}, + }, + } + got := c.Instances.Pkg(pkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) } - // TODO(grantnelson-wf): Need to instantiate xInt to replace `A` with `int` in the struct. - if isGeneric(xInt) { - t.Errorf("Expected uInt to be non-generic, got %v", xInt.Underlying()) +} +func TestCollector_LooselyRecursiveTypeParams(t *testing.T) { + // This is based off of part of go1.19.13/test/typeparam/nested.go + src := `package test + func main() { + type U[B any] struct{ y *B } + type X[C any] struct{ p U[X[C]] } + print(X[int]{}) } + ` + f := srctesting.New(t) + tc := types.NewContext() + file := f.Parse(`test.go`, src) + info, pkg := f.Check(`test`, file) + + c := Collector{ + TContext: tc, + Info: info, + Instances: &PackageInstanceSets{}, + } + c.Scan(pkg, file) + + xInst := Instance{ + Object: srctesting.LookupObj(pkg, `main.X`), + TArgs: []types.Type{types.Typ[types.Int]}, + } + xInt := xInst.Resolve(tc) want := []Instance{ + xInst, { - Object: srctesting.LookupObj(pkg, `F`), - TArgs: []types.Type{types.Typ[types.String]}, - }, { - Object: srctesting.LookupObj(pkg, `F.U`), - TNest: []types.Type{types.Typ[types.String]}, + Object: srctesting.LookupObj(pkg, `main.U`), TArgs: []types.Type{xInt}, - }, { - Object: xAny, - TNest: []types.Type{types.Typ[types.String]}, - TArgs: []types.Type{types.Typ[types.Int]}, }, } got := c.Instances.Pkg(pkg).Values() @@ -668,19 +696,12 @@ func TestCollector_NestedRecursiveTypeParams(t *testing.T) { } func TestCollector_NestedTypeParams(t *testing.T) { - t.Skip(`Skipping test due to known issue with nested recursive type parameters.`) - // TODO(grantnelson-wf): This test is failing because the type parameters - // inside of U are not being resolved to concrete types. This is because - // when instantiating X in the collector, we are not resolving the - // nested type of U that is X's type argument. This leave the A in U - // as a type parameter instead of resolving it to string. - // This is based off of part of go1.19.13/test/typeparam/nested.go src := `package test func F[A any]() any { type T[B any] struct{} type U[_ any] struct{ X A } - return T[U[A]]{} + return T[U[bool]]{} } func main() { print(F[int]()) @@ -688,38 +709,33 @@ func TestCollector_NestedTypeParams(t *testing.T) { ` f := srctesting.New(t) + tc := types.NewContext() file := f.Parse(`test.go`, src) info, pkg := f.Check(`test`, file) + inst := func(name, tNest, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TNest: evalTypeArgs(t, f.FileSet, pkg, tNest), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } c := Collector{ - TContext: types.NewContext(), + TContext: tc, Info: info, Instances: &PackageInstanceSets{}, } c.Scan(pkg, file) - uAny := srctesting.LookupObj(pkg, `F.U`) - uInt, err := types.Instantiate(types.NewContext(), uAny.Type(), []types.Type{types.Typ[types.Int]}, true) - if err != nil { - t.Fatalf("Failed to instantiate U[int]: %v", err) - } - //TODO(grantnelson-wf): Need to instantiate uInt to replace `A` with `int` in the struct. - if isGeneric(uInt) { - t.Errorf("Expected uInt to be non-generic, got %v", uInt.Underlying()) - } - + uInst := inst(`F.U`, `int`, `bool`) + uIntBool := uInst.Resolve(tc) want := []Instance{ + inst(`F`, ``, `int`), + inst(`F.U`, `int`, `bool`), { - Object: srctesting.LookupObj(pkg, `F`), - TArgs: []types.Type{types.Typ[types.Int]}, - }, { - Object: srctesting.LookupObj(pkg, `F.U`), - TNest: []types.Type{types.Typ[types.Int]}, - TArgs: []types.Type{types.Typ[types.Int]}, - }, { Object: srctesting.LookupObj(pkg, `F.T`), TNest: []types.Type{types.Typ[types.Int]}, - TArgs: []types.Type{uInt}, + TArgs: []types.Type{uIntBool}, }, } got := c.Instances.Pkg(pkg).Values() @@ -858,8 +874,10 @@ func TestResolver_SubstituteSelection(t *testing.T) { file := f.Parse("test.go", test.src) info, pkg := f.Check("pkg/test", file) - method := srctesting.LookupObj(pkg, "g.Method").(*types.Func).Type().(*types.Signature) - resolver := NewResolver(nil, method.RecvTypeParams(), []types.Type{srctesting.LookupObj(pkg, "x").Type()}, nil) + resolver := NewResolver(nil, Instance{ + Object: srctesting.LookupObj(pkg, "g.Method"), + TArgs: []types.Type{srctesting.LookupObj(pkg, "x").Type()}, + }) if l := len(info.Selections); l != 1 { t.Fatalf("Got: %d selections. Want: 1", l) diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index 64c67b4b5..10e0df69f 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -106,6 +106,33 @@ func (i Instance) Recv() Instance { } } +// Resolve instantiates and performs a substitution of the instance +// to get the concrete type or function. +// This will panic if the instance is not valid, e.g. if there are a different +// number of type arguments than the type parameters. +// +// If `tc` is non-nil, it de-duplicates the instance against previous +// instances with the same identity. See types.Instantiate for more info. +// +// Instances of named types may be lazily substituted, meaning the underlying +// type may not be fully substituted with the type arguments when returned. +// +// This is useful for quickly resolving an instance for a test or for debugging +// but this uses a temporary Resolver that will not be reused. +// When resolving several instances in the same context, it is more efficient +// to use NewResolver to take advantage of caching. +func (i Instance) Resolve(tc *types.Context) types.Type { + instType := i.Object.Type() + if len(i.TArgs) > 0 { + var err error + instType, err = types.Instantiate(tc, instType, i.TArgs, true) + if err != nil { + panic(fmt.Errorf("failed to instantiate %v: %w", i, err)) + } + } + return NewResolver(tc, i).Substitute(instType) +} + // InstanceSet allows collecting and processing unique Instances. // // Each Instance may be added to the set any number of times, but it will be diff --git a/compiler/internal/typeparams/resolver.go b/compiler/internal/typeparams/resolver.go new file mode 100644 index 000000000..5718c364d --- /dev/null +++ b/compiler/internal/typeparams/resolver.go @@ -0,0 +1,215 @@ +package typeparams + +import ( + "fmt" + "go/types" + "sort" + "strings" + + "github.com/gopherjs/gopherjs/compiler/typesutil" + "github.com/gopherjs/gopherjs/internal/govendor/subst" +) + +// Resolver translates types defined in terms of type parameters into concrete +// types, given a root instance. The root instance provides context for mapping +// from type parameters to type arguments so that the resolver can substitute +// any type parameters used in types to the corresponding type arguments. +// +// In some cases, a generic type may not be able to be fully instantiated. +// Generic named types that have no type arguments applied will have the +// type parameters substituted, however the type arguments will not be +// applied to instantiate the named type. +// +// For example, given `func Foo[T any]() { type Bar[U *T] struct { x T; y U } }`, +// and if `Foo[int]` is used as the root for the resolver, then `Bar[U *T]` will +// be substituted to create the generic `Bar[U *int] struct { x int; y U }`. +// Alternatively, the instantiated but still generic because of the `T`, +// `Bar[bool] struct { x T; y bool}` will be substituted for `Foo[int]` to +// create the concrete `Bar[bool] struct { x int; y bool }`. +// +// Typically the instantiated type from `info.Instances` should be substituted +// to resolve the implicit nesting types and create a concrete type. +// See internal/govendor/subst/subst.go for more details. +type Resolver struct { + tParams *types.TypeParamList + tArgs []types.Type + nest *types.Func + nestTParams *types.TypeParamList + nestTArgs []types.Type + replacements map[*types.TypeParam]types.Type + root Instance + + // subster is the substitution helper that will perform the actual + // substitutions. This maybe nil when there are no substitutions but + // will still be usable when nil. + subster *subst.Subster + selMemo map[typesutil.Selection]typesutil.Selection +} + +// NewResolver creates a new Resolver that will substitute type parameters +// with the type arguments as defined in the provided Instance. +func NewResolver(tc *types.Context, root Instance) *Resolver { + var ( + nest *types.Func + nestTParams *types.TypeParamList + tParams *types.TypeParamList + replacements = map[*types.TypeParam]types.Type{} + ) + + switch typ := root.Object.Type().(type) { + case *types.Signature: + nest = root.Object.(*types.Func) + tParams = SignatureTypeParams(typ) + case *types.Named: + tParams = typ.TypeParams() + nest = FindNestingFunc(root.Object) + if nest != nil { + nestTParams = SignatureTypeParams(nest.Type().(*types.Signature)) + } + default: + panic(fmt.Errorf("unexpected type %T for object %s", typ, root.Object)) + } + + // Check the root's implicit nesting type parameters and arguments match, + // then add them to the replacements. + if nestTParams.Len() != len(root.TNest) { + panic(fmt.Errorf(`number of nesting type parameters and arguments must match: %d => %d`, nestTParams.Len(), len(root.TNest))) + } + for i := 0; i < nestTParams.Len(); i++ { + replacements[nestTParams.At(i)] = root.TNest[i] + } + + // Check the root's type parameters and arguments match, + // then add them to the replacements. + if tParams.Len() != len(root.TArgs) { + panic(fmt.Errorf(`number of type parameters and arguments must match: %d => %d`, tParams.Len(), len(root.TArgs))) + } + for i := 0; i < tParams.Len(); i++ { + replacements[tParams.At(i)] = root.TArgs[i] + } + + return &Resolver{ + tParams: tParams, + tArgs: root.TArgs, + nest: nest, + nestTParams: nestTParams, + nestTArgs: root.TNest, + replacements: replacements, + root: root, + subster: subst.New(tc, replacements), + selMemo: map[typesutil.Selection]typesutil.Selection{}, + } +} + +// TypeParams is the list of type parameters that this resolver will substitute. +func (r *Resolver) TypeParams() *types.TypeParamList { + if r == nil { + return nil + } + return r.tParams +} + +// TypeArgs is the list of type arguments that this resolver will resolve to. +func (r *Resolver) TypeArgs() []types.Type { + if r == nil { + return nil + } + return r.tArgs +} + +// Nest is the nesting function that this resolver will resolve types with. +// This will be null if the resolver is not for a nested context, +func (r *Resolver) Nest() *types.Func { + if r == nil { + return nil + } + return r.nest +} + +// NestTypeParams is the list of type parameters from the nesting function +// that this resolver will substitute. +func (r *Resolver) NestTypeParams() *types.TypeParamList { + if r == nil { + return nil + } + return r.nestTParams +} + +// NestTypeArgs is the list of type arguments from the nesting function +// that this resolver will resolve to. +func (r *Resolver) NestTypeArgs() []types.Type { + if r == nil { + return nil + } + return r.nestTArgs +} + +// Substitute replaces references to type params in the provided type definition +// with the corresponding concrete types. +func (r *Resolver) Substitute(typ types.Type) types.Type { + if r == nil || typ == nil { + return typ // No substitutions to be made. + } + return r.subster.Type(typ) +} + +// SubstituteAll same as Substitute, but accepts a TypeList are returns +// substitution results as a slice in the same order. +func (r *Resolver) SubstituteAll(list *types.TypeList) []types.Type { + result := make([]types.Type, list.Len()) + for i := range result { + result[i] = r.Substitute(list.At(i)) + } + return result +} + +// SubstituteSelection replaces a method of field selection on a generic type +// defined in terms of type parameters with a method selection on a concrete +// instantiation of the type. +func (r *Resolver) SubstituteSelection(sel typesutil.Selection) typesutil.Selection { + if r == nil || sel == nil { + return sel // No substitutions to be made. + } + if concrete, ok := r.selMemo[sel]; ok { + return concrete + } + + switch sel.Kind() { + case types.MethodExpr, types.MethodVal, types.FieldVal: + recv := r.Substitute(sel.Recv()) + if types.Identical(recv, sel.Recv()) { + return sel // Non-generic receiver, no substitution necessary. + } + + // Look up the method on the instantiated receiver. + pkg := sel.Obj().Pkg() + obj, index, _ := types.LookupFieldOrMethod(recv, true, pkg, sel.Obj().Name()) + if obj == nil { + panic(fmt.Errorf("failed to lookup field %q in type %v", sel.Obj().Name(), recv)) + } + typ := obj.Type() + + if sel.Kind() == types.MethodExpr { + typ = typesutil.RecvAsFirstArg(typ.(*types.Signature)) + } + concrete := typesutil.NewSelection(sel.Kind(), recv, index, obj, typ) + r.selMemo[sel] = concrete + return concrete + default: + panic(fmt.Errorf("unexpected selection kind %v: %v", sel.Kind(), sel)) + } +} + +// String gets a strings representation of the resolver for debugging. +func (r *Resolver) String() string { + if r == nil { + return `{}` + } + + parts := make([]string, 0, len(r.replacements)) + for tp, ta := range r.replacements { + parts = append(parts, fmt.Sprintf("%s->%s", tp, ta)) + } + sort.Strings(parts) + return `{` + strings.Join(parts, `, `) + `}` +} diff --git a/compiler/internal/typeparams/utils.go b/compiler/internal/typeparams/utils.go index ea528314e..9fcf53c36 100644 --- a/compiler/internal/typeparams/utils.go +++ b/compiler/internal/typeparams/utils.go @@ -28,16 +28,16 @@ func FindNestingFunc(obj types.Object) *types.Func { return nil } - scope := obj.Parent() + // We can't use `obj.Parent()` here since some types don't have a set + // parent, such as types created with `types.NewTypeName`. Instead find + // the innermost scope from the package to use as the object's parent scope. + scope := obj.Pkg().Scope().Innermost(objPos) for scope != nil { // Iterate over all declarations in the scope. for _, name := range scope.Names() { decl := scope.Lookup(name) - if fn, ok := decl.(*types.Func); ok { - // Check if the object's position is within the function's scope. - if objPos >= fn.Pos() && objPos <= fn.Scope().End() { - return fn - } + if fn, ok := decl.(*types.Func); ok && fn.Scope().Contains(objPos) { + return fn } } scope = scope.Parent() @@ -85,15 +85,22 @@ func RequiresGenericsSupport(info *types.Info) error { return nil } -// isGeneric will search all the given types and their subtypes for a +// isGeneric will search all the given types in `typ` and their subtypes for a // *types.TypeParam. This will not check if a type could be generic, // but if each instantiation is not completely concrete yet. +// The given `ignore` slice is used to ignore type params that are known not +// to be substituted yet, typically the nest type parameters. +// +// This does allow for named types to have lazily substituted underlying types, +// as returned by methods like `types.Instantiate`, +// meaning that the type `B[T]` may be instantiated to `B[int]` but still have +// the underlying type of `struct { t T }` instead of `struct { t int }`. // // This is useful to check for generics types like `X[B[T]]`, where // `X` appears concrete because it is instantiated with the type argument `B[T]`, // however the `T` inside `B[T]` is a type parameter making `X[B[T]]` a generic // type since it required instantiation to a concrete type, e.g. `X[B[int]]`. -func isGeneric(typ ...types.Type) bool { +func isGeneric(ignore *types.TypeParamList, typ []types.Type) bool { var containsTypeParam func(t types.Type) bool foreach := func(count int, getter func(index int) types.Type) bool { @@ -106,19 +113,34 @@ func isGeneric(typ ...types.Type) bool { } seen := make(map[types.Type]struct{}) + managed := make(map[types.Type]struct{}) + for i := ignore.Len() - 1; i >= 0; i-- { + managed[ignore.At(i)] = struct{}{} + } containsTypeParam = func(t types.Type) bool { if _, ok := seen[t]; ok { return false } seen[t] = struct{}{} + if _, ok := managed[t]; ok { + return false + } + switch t := t.(type) { case *types.TypeParam: return true case *types.Named: - return t.TypeParams().Len() != t.TypeArgs().Len() || - foreach(t.TypeArgs().Len(), func(i int) types.Type { return t.TypeArgs().At(i) }) || - containsTypeParam(t.Underlying()) + if t.TypeParams().Len() != t.TypeArgs().Len() || + foreach(t.TypeArgs().Len(), func(i int) types.Type { return t.TypeArgs().At(i) }) { + return true + } + // Add type parameters to managed so that if they are encountered + // we know that they are just lazy substitutions for the checked type arguments. + for i := t.TypeParams().Len() - 1; i >= 0; i-- { + managed[t.TypeParams().At(i)] = struct{}{} + } + return containsTypeParam(t.Underlying()) case *types.Struct: return foreach(t.NumFields(), func(i int) types.Type { return t.Field(i).Type() }) case *types.Interface: diff --git a/internal/govendor/subst/export.go b/internal/govendor/subst/export.go index 00a77ca49..35e38473c 100644 --- a/internal/govendor/subst/export.go +++ b/internal/govendor/subst/export.go @@ -3,10 +3,7 @@ // type arguments. package subst -import ( - "fmt" - "go/types" -) +import "go/types" // To simplify future updates of the borrowed code, we minimize modifications // to it as much as possible. This file implements an exported interface to the @@ -17,17 +14,15 @@ type Subster struct { impl *subster } -// New creates a new Subster with a given list of type parameters and matching args. -func New(tc *types.Context, tParams *types.TypeParamList, tArgs []types.Type) *Subster { - if tParams.Len() != len(tArgs) { - panic(fmt.Errorf("number of type parameters and arguments must match: %d => %d", tParams.Len(), len(tArgs))) - } - - if tParams.Len() == 0 && len(tArgs) == 0 { +// New creates a new Subster with a given a map from type parameters and the arguments +// that should be used to replace them. If the map is empty, nil is returned. +func New(tc *types.Context, replacements map[*types.TypeParam]types.Type) *Subster { + if len(replacements) == 0 { return nil } - subst := makeSubster(tc, nil, tParams, tArgs, false) + subst := makeSubster(tc, nil, nil, nil, false) + subst.replacements = replacements return &Subster{impl: subst} } diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 6720f50d7..3685e99d6 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -153,10 +153,12 @@ var knownFails = map[string]failReason{ "typeparam/issue51733.go": {category: usesUnsupportedPackage, desc: "unsafe: uintptr to struct pointer conversion is unsupported"}, "typeparam/typeswitch5.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, - // Failures related to the lack of generics support. Ideally, this section - // should be emptied once https://github.com/gopherjs/gopherjs/issues/1013 is - // fixed. - "typeparam/nested.go": {category: usesUnsupportedGenerics, desc: "incomplete support for generic types inside generic functions"}, + // Failures related to the lack of nested type number indicators and deep nested types not printing correctly. + // For example, the following line from the test's outputs (see "typeparam/nested.out") + // `4,7: main.T·2[int;main.U·3[int;int]]` will currently output as `4,7: main.T[int;main.U[int]]` + // in GopherJS because we doesn't currently add the `·2` and `·3` indicators to the type names + // and the nested type arguments to deep nested type, e.g. `U[int;int]` is printed as `U[int]`. + "typeparam/nested.go": {category: usesUnsupportedGenerics, desc: "incomplete support for nested type numbering"}, // These are new tests in Go 1.19 "typeparam/issue51521.go": {category: lowLevelRuntimeDifference, desc: "different panic message when calling a method on nil interface"}, From d4d3cf7d45f0b18ceec791cd1f942bdc18057180 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 29 May 2025 16:25:08 -0600 Subject: [PATCH 47/58] Updated subst to v0.33.0 --- internal/govendor/subst/subst.go | 298 ++++++++++++++++++-------- internal/govendor/subst/subst_test.go | 13 +- internal/govendor/subst/util.go | 33 ++- 3 files changed, 246 insertions(+), 98 deletions(-) diff --git a/internal/govendor/subst/subst.go b/internal/govendor/subst/subst.go index 825e3c7f1..1ac705625 100644 --- a/internal/govendor/subst/subst.go +++ b/internal/govendor/subst/subst.go @@ -2,71 +2,85 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Copy of https://cs.opensource.google/go/x/tools/+/refs/tags/v0.17.0:go/ssa/subst.go +// Copy of https://cs.opensource.google/go/x/tools/+/refs/tags/v0.33.0:go/ssa/subst.go // Any changes to this copy are labelled with GOPHERJS. package subst import ( "go/types" + + "golang.org/x/tools/go/types/typeutil" ) -// Type substituter for a fixed set of replacement types. +// subster defines a type substitution operation of a set of type parameters +// to type parameter free replacement types. Substitution is done within +// the context of a package-level function instantiation. *Named types +// declared in the function are unique to the instantiation. +// +// For example, given a parameterized function F +// +// func F[S, T any]() any { +// type X struct{ s S; next *X } +// var p *X +// return p +// } // -// A nil *subster is an valid, empty substitution map. It always acts as +// calling the instantiation F[string, int]() returns an interface +// value (*X[string,int], nil) where the underlying value of +// X[string,int] is a struct{s string; next *X[string,int]}. +// +// A nil *subster is a valid, empty substitution map. It always acts as // the identity function. This allows for treating parameterized and // non-parameterized functions identically while compiling to ssa. // // Not concurrency-safe. +// +// Note: Some may find it helpful to think through some of the most +// complex substitution cases using lambda calculus inspired notation. +// subst.typ() solves evaluating a type expression E +// within the body of a function Fn[m] with the type parameters m +// once we have applied the type arguments N. +// We can succinctly write this as a function application: +// +// ((λm. E) N) +// +// go/types does not provide this interface directly. +// So what subster provides is a type substitution operation +// +// E[m:=N] type subster struct { replacements map[*types.TypeParam]types.Type // values should contain no type params cache map[types.Type]types.Type // cache of subst results - ctxt *types.Context // cache for instantiation - scope *types.Scope // *types.Named declared within this scope can be substituted (optional) - debug bool // perform extra debugging checks + origin *types.Func // types.Objects declared within this origin function are unique within this context + ctxt *types.Context // speeds up repeated instantiations + uniqueness typeutil.Map // determines the uniqueness of the instantiations within the function // TODO(taking): consider adding Pos - // TODO(zpavlinovic): replacements can contain type params - // when generating instances inside of a generic function body. } // Returns a subster that replaces tparams[i] with targs[i]. Uses ctxt as a cache. // targs should not contain any types in tparams. -// scope is the (optional) lexical block of the generic function for which we are substituting. -func makeSubster(ctxt *types.Context, scope *types.Scope, tparams *types.TypeParamList, targs []types.Type, debug bool) *subster { +// fn is the generic function for which we are substituting. +func makeSubster(ctxt *types.Context, fn *types.Func, tparams *types.TypeParamList, targs []types.Type, debug bool) *subster { assert(tparams.Len() == len(targs), "makeSubster argument count must match") + // GOPHERJS: Made `fn` optional so that we can use this on package level types too. + var origin *types.Func + if fn != nil { + origin = fn.Origin() + } + subst := &subster{ replacements: make(map[*types.TypeParam]types.Type, tparams.Len()), cache: make(map[types.Type]types.Type), + origin: origin, ctxt: ctxt, - scope: scope, - debug: debug, } for i := 0; i < tparams.Len(); i++ { subst.replacements[tparams.At(i)] = targs[i] } - if subst.debug { - subst.wellFormed() - } return subst } -// wellFormed asserts that subst was properly initialized. -func (subst *subster) wellFormed() { - if subst == nil { - return - } - // Check that all of the type params do not appear in the arguments. - s := make(map[types.Type]bool, len(subst.replacements)) - for tparam := range subst.replacements { - s[tparam] = true - } - for _, r := range subst.replacements { - if reaches(r, s) { - panic(subst) - } - } -} - // typ returns the type of t with the type parameter tparams[i] substituted // for the type targs[i] where subst was created using tparams and targs. func (subst *subster) typ(t types.Type) (res types.Type) { @@ -80,11 +94,8 @@ func (subst *subster) typ(t types.Type) (res types.Type) { subst.cache[t] = res }() - // fall through if result r will be identical to t, types.Identical(r, t). switch t := t.(type) { case *types.TypeParam: - // GOPHERJS: Replaced an assert that was causing a panic for nested types with code from - // https://cs.opensource.google/go/x/tools/+/refs/tags/v0.33.0:go/ssa/subst.go;l=92 if r := subst.replacements[t]; r != nil { return r } @@ -140,9 +151,17 @@ func (subst *subster) typ(t types.Type) (res types.Type) { case *types.Interface: return subst.interface_(t) + // GOPHERJS: Removed following case since types.Alias is not supported until go1.22. + //case *types.Alias: + // return subst.alias(t) + case *types.Named: return subst.named(t) + // GOPHERJS: Removed following case since the opaque type is specific to the SSA builder. + //case *opaqueType: + // return t // opaque types are never substituted + default: panic("unreachable") } @@ -192,7 +211,7 @@ func (subst *subster) struct_(t *types.Struct) *types.Struct { return t } -// varlist reutrns subst(in[i]) or return nils if subst(v[i]) == v[i] for all i. +// varlist returns subst(in[i]) or return nils if subst(v[i]) == v[i] for all i. func (subst *subster) varlist(in varlist) []*types.Var { var out []*types.Var // nil => no updates for i, n := 0, in.Len(); i < n; i++ { @@ -217,7 +236,7 @@ func (subst *subster) var_(v *types.Var) *types.Var { if v.IsField() { return types.NewField(v.Pos(), v.Pkg(), v.Name(), typ, v.Embedded()) } - return types.NewVar(v.Pos(), v.Pkg(), v.Name(), typ) + return types.NewParam(v.Pos(), v.Pkg(), v.Name(), typ) } } return v @@ -256,6 +275,8 @@ func (subst *subster) interface_(iface *types.Interface) *types.Interface { var methods []*types.Func initMethods := func(n int) { // copy first n explicit methods methods = make([]*types.Func, iface.NumExplicitMethods()) + // GOPHERJS: Replaced a range over count since that's not supported in go1.22 + //for i := range n { for i := 0; i < n; i++ { f := iface.ExplicitMethod(i) norecv := changeRecv(f.Type().(*types.Signature), nil) @@ -280,6 +301,8 @@ func (subst *subster) interface_(iface *types.Interface) *types.Interface { var embeds []types.Type initEmbeds := func(n int) { // copy first n embedded types embeds = make([]types.Type, iface.NumEmbeddeds()) + // GOPHERJS: Replaced a range over count since that's not supported in go1.22 + //for i := range n { for i := 0; i < n; i++ { embeds[i] = iface.EmbeddedType(i) } @@ -307,72 +330,157 @@ func (subst *subster) interface_(iface *types.Interface) *types.Interface { return types.NewInterfaceType(methods, embeds).Complete() } +// GOPHERJS: removed alias substitution since types.Alias is not supported until go1.22 +//func (subst *subster) alias(t *types.Alias) types.Type { ... } + func (subst *subster) named(t *types.Named) types.Type { - // A named type may be: - // (1) ordinary named type (non-local scope, no type parameters, no type arguments), - // (2) locally scoped type, - // (3) generic (type parameters but no type arguments), or - // (4) instantiated (type parameters and type arguments). - tparams := t.TypeParams() - if tparams.Len() == 0 { - if subst.scope != nil && !subst.scope.Contains(t.Obj().Pos()) { - // Outside the current function scope? - return t // case (1) ordinary + // A Named type is a user defined type. + // Ignoring generics, Named types are canonical: they are identical if + // and only if they have the same defining symbol. + // Generics complicate things, both if the type definition itself is + // parameterized, and if the type is defined within the scope of a + // parameterized function. In this case, two named types are identical if + // and only if their identifying symbols are identical, and all type + // arguments bindings in scope of the named type definition (including the + // type parameters of the definition itself) are equivalent. + // + // Notably: + // 1. For type definition type T[P1 any] struct{}, T[A] and T[B] are identical + // only if A and B are identical. + // 2. Inside the generic func Fn[m any]() any { type T struct{}; return T{} }, + // the result of Fn[A] and Fn[B] have identical type if and only if A and + // B are identical. + // 3. Both 1 and 2 could apply, such as in + // func F[m any]() any { type T[x any] struct{}; return T{} } + // + // A subster replaces type parameters within a function scope, and therefore must + // also replace free type parameters in the definitions of local types. + // + // Note: There are some detailed notes sprinkled throughout that borrow from + // lambda calculus notation. These contain some over simplifying math. + // + // LC: One way to think about subster is that it is a way of evaluating + // ((λm. E) N) as E[m:=N]. + // Each Named type t has an object *TypeName within a scope S that binds an + // underlying type expression U. U can refer to symbols within S (+ S's ancestors). + // Let x = t.TypeParams() and A = t.TypeArgs(). + // Each Named type t is then either: + // U where len(x) == 0 && len(A) == 0 + // λx. U where len(x) != 0 && len(A) == 0 + // ((λx. U) A) where len(x) == len(A) + // In each case, we will evaluate t[m:=N]. + tparams := t.TypeParams() // x + targs := t.TypeArgs() // A + + if !declaredWithin(t.Obj(), subst.origin) { + // t is declared outside of Fn[m]. + // + // In this case, we can skip substituting t.Underlying(). + // The underlying type cannot refer to the type parameters. + // + // LC: Let free(E) be the set of free type parameters in an expression E. + // Then whenever m ∉ free(E), then E = E[m:=N]. + // t ∉ Scope(fn) so therefore m ∉ free(U) and m ∩ x = ∅. + if targs.Len() == 0 { + // t has no type arguments. So it does not need to be instantiated. + // + // This is the normal case in real Go code, where t is not parameterized, + // declared at some package scope, and m is a TypeParam from a parameterized + // function F[m] or method. + // + // LC: m ∉ free(A) lets us conclude m ∉ free(t). So t=t[m:=N]. + return t } - // case (2) locally scoped type. - // Create a new named type to represent this instantiation. - // We assume that local types of distinct instantiations of a - // generic function are distinct, even if they don't refer to - // type parameters, but the spec is unclear; see golang/go#58573. + // t is declared outside of Fn[m] and has type arguments. + // The type arguments may contain type parameters m so + // substitute the type arguments, and instantiate the substituted + // type arguments. + // + // LC: Evaluate this as ((λx. U) A') where A' = A[m := N]. + newTArgs := subst.typelist(targs) + return subst.instantiate(t.Origin(), newTArgs) + } + + // t is declared within Fn[m]. + + if targs.Len() == 0 { // no type arguments? + assert(t == t.Origin(), "local parameterized type abstraction must be an origin type") + + // t has no type arguments. + // The underlying type of t may contain the function's type parameters, + // replace these, and create a new type. // // Subtle: We short circuit substitution and use a newly created type in - // subst, i.e. cache[t]=n, to pre-emptively replace t with n in recursive - // types during traversal. This both breaks infinite cycles and allows for - // constructing types with the replacement applied in subst.typ(under). + // subst, i.e. cache[t]=fresh, to preemptively replace t with fresh + // in recursive types during traversal. This both breaks infinite cycles + // and allows for constructing types with the replacement applied in + // subst.typ(U). // - // Example: - // func foo[T any]() { - // type linkedlist struct { - // next *linkedlist - // val T - // } - // } + // A new copy of the Named and Typename (and constraints) per function + // instantiation matches the semantics of Go, which treats all function + // instantiations F[N] as having distinct local types. // - // When the field `next *linkedlist` is visited during subst.typ(under), - // we want the substituted type for the field `next` to be `*n`. - n := types.NewNamed(t.Obj(), nil, nil) - subst.cache[t] = n - subst.cache[n] = n - n.SetUnderlying(subst.typ(t.Underlying())) - return n + // LC: x.Len()=0 can be thought of as a special case of λx. U. + // LC: Evaluate (λx. U)[m:=N] as (λx'. U') where U'=U[x:=x',m:=N]. + tname := t.Obj() + obj := types.NewTypeName(tname.Pos(), tname.Pkg(), tname.Name(), nil) + fresh := types.NewNamed(obj, nil, nil) + var newTParams []*types.TypeParam + for i := 0; i < tparams.Len(); i++ { + cur := tparams.At(i) + cobj := cur.Obj() + cname := types.NewTypeName(cobj.Pos(), cobj.Pkg(), cobj.Name(), nil) + ntp := types.NewTypeParam(cname, nil) + // GOPHERJS: The following cache was removed because it causes a + // problem for recursive types, e.g. `type X[T any] Q[X[T]]`. + // When it sees the `X[T]` in `Q[X[T]]`, it creates a `subOrigin` + // (seen below) which caches the old `T` to the new `T'`. + // Then when creating `subTArgs` (also below), it will return + // `T'` via the cache instead of substituting `T` with the + // correct type argument. + //subst.cache[cur] = ntp + newTParams = append(newTParams, ntp) + } + fresh.SetTypeParams(newTParams) + subst.cache[t] = fresh + subst.cache[fresh] = fresh + fresh.SetUnderlying(subst.typ(t.Underlying())) + // Substitute into all of the constraints after they are created. + for i, ntp := range newTParams { + bound := tparams.At(i).Constraint() + ntp.SetConstraint(subst.typ(bound)) + } + return fresh + } + + // t is defined within Fn[m] and t has type arguments (an instantiation). + // We reduce this to the two cases above: + // (1) substitute the function's type parameters into t.Origin(). + // (2) substitute t's type arguments A and instantiate the updated t.Origin() with these. + // + // LC: Evaluate ((λx. U) A)[m:=N] as (t' A') where t' = (λx. U)[m:=N] and A'=A [m:=N] + subOrigin := subst.typ(t.Origin()) + subTArgs := subst.typelist(targs) + return subst.instantiate(subOrigin, subTArgs) +} + +func (subst *subster) instantiate(orig types.Type, targs []types.Type) types.Type { + i, err := types.Instantiate(subst.ctxt, orig, targs, false) + assert(err == nil, "failed to Instantiate named (Named or Alias) type") + if c, _ := subst.uniqueness.At(i).(types.Type); c != nil { + return c.(types.Type) } - targs := t.TypeArgs() - - // insts are arguments to instantiate using. - insts := make([]types.Type, tparams.Len()) - - // case (3) generic ==> targs.Len() == 0 - // Instantiating a generic with no type arguments should be unreachable. - // Please report a bug if you encounter this. - assert(targs.Len() != 0, "substition into a generic Named type is currently unsupported") - - // case (4) instantiated. - // Substitute into the type arguments and instantiate the replacements/ - // Example: - // type N[A any] func() A - // func Foo[T](g N[T]) {} - // To instantiate Foo[string], one goes through {T->string}. To get the type of g - // one subsitutes T with string in {N with typeargs == {T} and typeparams == {A} } - // to get {N with TypeArgs == {string} and typeparams == {A} }. - assert(targs.Len() == tparams.Len(), "typeargs.Len() must match typeparams.Len() if present") - for i, n := 0, targs.Len(); i < n; i++ { - inst := subst.typ(targs.At(i)) // TODO(generic): Check with rfindley for mutual recursion - insts[i] = inst + subst.uniqueness.Set(i, i) + return i +} + +func (subst *subster) typelist(l *types.TypeList) []types.Type { + res := make([]types.Type, l.Len()) + for i := 0; i < l.Len(); i++ { + res[i] = subst.typ(l.At(i)) } - r, err := types.Instantiate(subst.ctxt, t.Origin(), insts, false) - assert(err == nil, "failed to Instantiate Named type") - return r + return res } func (subst *subster) signature(t *types.Signature) types.Type { @@ -471,6 +579,8 @@ func reaches(t types.Type, c map[types.Type]bool) (res bool) { return true } } + // GOPHERJS: Removed types.Alias from following case since it's not supported until go1.22. + //case *types.Named, *types.Alias: case *types.Named: return reaches(t.Underlying(), c) default: diff --git a/internal/govendor/subst/subst_test.go b/internal/govendor/subst/subst_test.go index 832f0ebd4..8f2f629a1 100644 --- a/internal/govendor/subst/subst_test.go +++ b/internal/govendor/subst/subst_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Copy of https://cs.opensource.google/go/x/tools/+/refs/tags/v0.17.0:go/ssa/subst_test.go +// Copy of https://cs.opensource.google/go/x/tools/+/refs/tags/v0.33.0:go/ssa/subst_test.go package subst import ( @@ -17,6 +17,10 @@ func TestSubst(t *testing.T) { const source = ` package P +func within(){ + // Pretend that the instantiation happens within this function. +} + type t0 int func (t0) f() type t1 interface{ f() } @@ -56,6 +60,11 @@ var _ L[int] = Fn0[L[int]](nil) t.Fatal(err) } + within, _ := pkg.Scope().Lookup("within").(*types.Func) + if within == nil { + t.Fatal("Failed to find the function within()") + } + for _, test := range []struct { expr string // type expression of Named parameterized type args []string // type expressions of args for named @@ -95,7 +104,7 @@ var _ L[int] = Fn0[L[int]](nil) T := tv.Type.(*types.Named) - subst := makeSubster(types.NewContext(), nil, T.TypeParams(), targs, true) + subst := makeSubster(types.NewContext(), within, T.TypeParams(), targs, true) sub := subst.typ(T.Underlying()) if got := sub.String(); got != test.want { t.Errorf("subst{%v->%v}.typ(%s) = %v, want %v", test.expr, test.args, T.Underlying(), got, test.want) diff --git a/internal/govendor/subst/util.go b/internal/govendor/subst/util.go index 5b55c0310..edfa513ba 100644 --- a/internal/govendor/subst/util.go +++ b/internal/govendor/subst/util.go @@ -4,11 +4,14 @@ package subst -import "go/types" +import ( + "go/token" + "go/types" +) // assert panics with the mesage msg if p is false. // Avoid combining with expensive string formatting. -// From https://cs.opensource.google/go/x/tools/+/refs/tags/v0.17.0:go/ssa/util.go;l=27 +// From https://cs.opensource.google/go/x/tools/+/refs/tags/v0.33.0:go/ssa/util.go;l=28 func assert(p bool, msg string) { if !p { panic(msg) @@ -19,3 +22,29 @@ func assert(p bool, msg string) { func changeRecv(s *types.Signature, recv *types.Var) *types.Signature { return types.NewSignatureType(recv, nil, nil, s.Params(), s.Results(), s.Variadic()) } + +// declaredWithin reports whether an object is declared within a function. +// +// obj must not be a method or a field. +// From https://cs.opensource.google/go/x/tools/+/refs/tags/v0.33.0:go/ssa/util.go;l=145 +func declaredWithin(obj types.Object, fn *types.Func) bool { + // GOPHERJS: Made `fn` optional so that we can use this on package level types too. + if fn == nil { + return false + } + + if obj.Pos() != token.NoPos { + return fn.Scope().Contains(obj.Pos()) // trust the positions if they exist. + } + if fn.Pkg() != obj.Pkg() { + return false // fast path for different packages + } + + // Traverse Parent() scopes for fn.Scope(). + for p := obj.Parent(); p != nil; p = p.Parent() { + if p == fn.Scope() { + return true + } + } + return false +} From 5bcfc68d50b29dfe4c7f6ba96bfd20769b15734a Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Thu, 5 Jun 2025 11:19:22 -0600 Subject: [PATCH 48/58] Updating DCE to support nested types --- compiler/compiler_test.go | 173 ++++++++++++++++ compiler/declNames.go | 6 +- compiler/decls.go | 11 +- compiler/expressions.go | 4 +- compiler/internal/dce/README.md | 37 ++-- compiler/internal/dce/collector.go | 4 +- compiler/internal/dce/dce_test.go | 178 +++++++++++++---- compiler/internal/dce/filters.go | 219 +++++++++++++++------ compiler/internal/dce/info.go | 8 +- compiler/internal/typeparams/utils.go | 63 +++++- compiler/internal/typeparams/utils_test.go | 165 ++++++++++++++++ compiler/statements.go | 2 +- compiler/utils.go | 13 +- 13 files changed, 749 insertions(+), 134 deletions(-) diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 88d8e525e..ef3b8aaa9 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -364,6 +364,179 @@ func TestDeclSelection_RemoveUnusedTypeConstraint(t *testing.T) { sel.IsDead(`var:command-line-arguments.ghost`) } +func TestDeclSelection_RemoveUnusedNestedTypesInFunction(t *testing.T) { + src := ` + package main + func Foo[T any](u T) any { + type Bar struct { v T } + return Bar{v: u} + } + func deadCode() { + println(Foo[int](42)) + } + func main() { + println(Foo[string]("cat")) + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + sel.IsAlive(`func:command-line-arguments.main`) + + sel.IsAlive(`funcVar:command-line-arguments.Foo`) + sel.IsAlive(`func:command-line-arguments.Foo`) + sel.IsDead(`func:command-line-arguments.Foo`) + + sel.IsAlive(`typeVar:command-line-arguments.Bar`) + sel.IsAlive(`type:command-line-arguments.Bar`) + sel.IsDead(`type:command-line-arguments.Bar`) + + sel.IsDead(`funcVar:command-line-arguments.deadCode`) + sel.IsDead(`func:command-line-arguments.deadCode`) +} + +func TestDeclSelection_RemoveUnusedNestedTypesInMethod(t *testing.T) { + src := ` + package main + type Baz[T any] struct{} + func (b *Baz[T]) Foo(u T) any { + type Bar struct { v T } + return Bar{v: u} + } + func deadCode() { + b := Baz[int]{} + println(b.Foo(42)) + } + func main() { + b := Baz[string]{} + println(b.Foo("cat")) + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + sel.IsAlive(`func:command-line-arguments.main`) + + sel.IsAlive(`typeVar:command-line-arguments.Baz`) + sel.IsDead(`type:command-line-arguments.Baz`) + sel.IsAlive(`type:command-line-arguments.Baz`) + + sel.IsDead(`func:command-line-arguments.(*Baz).Foo`) + sel.IsAlive(`func:command-line-arguments.(*Baz).Foo`) + + sel.IsAlive(`typeVar:command-line-arguments.Bar`) + sel.IsDead(`type:command-line-arguments.Bar`) + sel.IsAlive(`type:command-line-arguments.Bar`) + + sel.IsDead(`funcVar:command-line-arguments.deadCode`) + sel.IsDead(`func:command-line-arguments.deadCode`) +} + +func TestDeclSelection_RemoveAllUnusedNestedTypes(t *testing.T) { + src := ` + package main + func Foo[T any](u T) any { + type Bar struct { v T } + return Bar{v: u} + } + func deadCode() { + println(Foo[int](42)) + println(Foo[string]("cat")) + } + func main() {}` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + sel.IsAlive(`func:command-line-arguments.main`) + + sel.IsDead(`funcVar:command-line-arguments.Foo`) + sel.IsDead(`func:command-line-arguments.Foo`) + sel.IsDead(`func:command-line-arguments.Foo`) + + sel.IsDead(`typeVar:command-line-arguments.Bar`) + sel.IsDead(`type:command-line-arguments.Bar`) + sel.IsDead(`type:command-line-arguments.Bar`) + + sel.IsDead(`funcVar:command-line-arguments.deadCode`) + sel.IsDead(`func:command-line-arguments.deadCode`) +} + +func TestDeclSelection_CompletelyRemoveNestedType(t *testing.T) { + src := ` + package main + func Foo[T any](u T) any { + type Bar struct { v T } + return Bar{v: u} + } + func deadCode() { + println(Foo[int](42)) + } + func main() {}` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.IsAlive(`func:command-line-arguments.main`) + + sel.IsDead(`funcVar:command-line-arguments.Foo`) + sel.IsDead(`func:command-line-arguments.Foo`) + + sel.IsDead(`typeVar:command-line-arguments.Bar`) + sel.IsDead(`type:command-line-arguments.Bar`) + + sel.IsDead(`funcVar:command-line-arguments.deadCode`) + sel.IsDead(`func:command-line-arguments.deadCode`) +} + +func TestDeclSelection_RemoveAnonNestedTypes(t *testing.T) { + // Based on test/fixedbugs/issue53635.go + // This checks that if an anon type (e.g. []T) is used in a function + // that is not used, the type is removed, otherwise it is kept. + + src := ` + package main + func Foo[T any](u T) any { + return []T(nil) + } + func deadCode() { + println(Foo[string]("cat")) + } + func main() { + println(Foo[int](42)) + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + sel.IsDead(`anonType:command-line-arguments.sliceType`) // []string + sel.IsAlive(`anonType:command-line-arguments.sliceType$1`) // []int +} + +func TestDeclSelection_NoNestAppliedToFuncCallInMethod(t *testing.T) { + // Checks that a function call to a non-local function isn't + // being labeled as a nested function call. + src := ` + package main + func foo(a any) { + println(a) + } + type Bar[T any] struct { u T } + func (b *Bar[T]) Baz() { + foo(b.u) + } + func main() { + b := &Bar[int]{u: 42} + b.Baz() + }` + + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} + sel := declSelection(t, srcFiles, nil) + sel.IsAlive(`init:main`) + + sel.IsAlive(`typeVar:command-line-arguments.Bar`) + sel.IsAlive(`type:command-line-arguments.Bar`) + sel.IsAlive(`func:command-line-arguments.(*Bar).Baz`) + + sel.IsAlive(`func:command-line-arguments.foo`) +} + func TestLengthParenthesizingIssue841(t *testing.T) { // See issue https://github.com/gopherjs/gopherjs/issues/841 // diff --git a/compiler/declNames.go b/compiler/declNames.go index 4ba59e289..57d6ac255 100644 --- a/compiler/declNames.go +++ b/compiler/declNames.go @@ -49,7 +49,7 @@ func funcDeclFullName(inst typeparams.Instance) string { } // typeVarDeclFullName returns a unique name for a package-level variable -// that is used for a named type declaration. +// that is used for a named type declaration or a named nested type declaration. // If the type is generic, this declaration name is also for the list // of instantiations of the type. func typeVarDeclFullName(o *types.TypeName) string { @@ -57,7 +57,9 @@ func typeVarDeclFullName(o *types.TypeName) string { } // typeDeclFullName returns a unique name for a package-level type declaration -// for the given instance of a type. Names are only unique per package. +// for the given instance of a type. Names are only unique per scope package +// unless the type is a nested type then the name is only unique per the +// function or method it is declared in. func typeDeclFullName(inst typeparams.Instance) string { return `type:` + inst.String() } diff --git a/compiler/decls.go b/compiler/decls.go index 5c6291ba6..004973217 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -264,7 +264,7 @@ func (fc *funcContext) newVarDecl(init *types.Initializer) *Decl { fc.localVars = nil // Clean up after ourselves. }) - d.Dce().SetName(init.Lhs[0]) + d.Dce().SetName(init.Lhs[0], nil, nil) if len(init.Lhs) != 1 || analysis.HasSideEffect(init.Rhs, fc.pkgCtx.Info.Info) { d.Dce().SetAsAlive() } @@ -291,7 +291,7 @@ func (fc *funcContext) funcDecls(functions []*ast.FuncDecl) ([]*Decl, error) { FullName: funcVarDeclFullName(o), Vars: []string{objName}, } - varDecl.Dce().SetName(o) + varDecl.Dce().SetName(o, nil, nil) if o.Type().(*types.Signature).TypeParams().Len() != 0 { varDecl.DeclCode = fc.CatchOutput(0, func() { fc.Printf("%s = {};", objName) @@ -331,7 +331,7 @@ func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance) Blocking: fc.pkgCtx.IsBlocking(inst), LinkingName: symbol.New(o), } - d.Dce().SetName(o, inst.TArgs...) + d.Dce().SetName(o, inst.TNest, inst.TArgs) if typesutil.IsMethod(o) { recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj() @@ -433,6 +433,7 @@ func (fc *funcContext) newNamedTypeVarDecl(obj *types.TypeName) *Decl { FullName: typeVarDeclFullName(obj), Vars: []string{name}, } + varDecl.Dce().SetName(obj, nil, nil) if fc.pkgCtx.instanceSet.Pkg(obj.Pkg()).ObjHasInstances(obj) { varDecl.DeclCode = fc.CatchOutput(0, func() { fc.Printf("%s = {};", name) @@ -470,7 +471,7 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er d := &Decl{ FullName: typeDeclFullName(inst), } - d.Dce().SetName(inst.Object, inst.TArgs...) + d.Dce().SetName(inst.Object, inst.TNest, inst.TArgs) fc.pkgCtx.CollectDCEDeps(d, func() { // Code that declares a JS type (i.e. prototype) for each Go type. d.DeclCode = fc.CatchOutput(0, func() { @@ -595,7 +596,7 @@ func (fc *funcContext) anonTypeDecls(anonTypes []*types.TypeName) []*Decl { FullName: anonTypeDeclFullName(t), Vars: []string{t.Name()}, } - d.Dce().SetName(t) + d.Dce().SetName(t, nil, nil) fc.pkgCtx.CollectDCEDeps(d, func() { d.DeclCode = []byte(fmt.Sprintf("\t%s = $%sType(%s);\n", t.Name(), strings.ToLower(typeKind(t.Type())[5:]), fc.initArgs(t.Type()))) }) diff --git a/compiler/expressions.go b/compiler/expressions.go index 455549a1d..6f8ab6e2b 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -591,7 +591,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case types.MethodVal: return fc.formatExpr(`$methodVal(%s, "%s")`, fc.makeReceiver(e), sel.Obj().(*types.Func).Name()) case types.MethodExpr: - fc.pkgCtx.DeclareDCEDep(sel.Obj(), inst.TArgs...) + fc.pkgCtx.DeclareDCEDep(sel.Obj(), inst.TNest, inst.TArgs) if _, ok := sel.Recv().Underlying().(*types.Interface); ok { return fc.formatExpr(`$ifaceMethodExpr("%s")`, sel.Obj().(*types.Func).Name()) } @@ -906,7 +906,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression { sel, _ := fc.selectionOf(e) if !sel.Obj().Exported() { - fc.pkgCtx.DeclareDCEDep(sel.Obj()) + fc.pkgCtx.DeclareDCEDep(sel.Obj(), nil, nil) } x := e.X diff --git a/compiler/internal/dce/README.md b/compiler/internal/dce/README.md index 01ec1e8c6..209b51d68 100644 --- a/compiler/internal/dce/README.md +++ b/compiler/internal/dce/README.md @@ -265,17 +265,32 @@ of either will indicate a use of both. This will cause slightly more unexported methods to be alive while reducing the complication of type checking which object or type of object is performing the call. -| Declaration | exported | unexported | non-generic | generic | object name | method name | -|:------------|:--------:|:----------:|:-----------:|:-------:|:------------|:------------| -| variables | █ | █ | █ | n/a | `.` | | -| functions | █ | █ | █ | | `.` | | -| functions | █ | █ | | █ | `.[]` | | -| named type | █ | █ | █ | | `.` | | -| named type | █ | █ | | █ | `.[]` | | -| method | █ | | █ | | `.` | | -| method | █ | | | █ | `.[]` | | -| method | | █ | █ | | `.` | `.()()` | -| method | | █ | | █ | `.[]` | `.()()` | +| Declaration | exported | unexported | generic | object name | method name | +|:------------|:--------:|:----------:|:-------:|:------------|:------------| +| variables | █ | █ | | `.` | | +| functions | █ | █ | | `.` | | +| functions | █ | █ | █ | `.[]` | | +| named type | █ | █ | | `.` | | +| named type | █ | █ | █ | `.[]` | | +| method | █ | | | `.` | | +| method | █ | | █ | `.[]` | | +| method | | █ | | `.` | `.()()` | +| method | | █ | █ | `.[]` | `.()()` | + +For nested types, the nest (the function or method in which the type is +declared) needs to be taken into consideration to differentiate types with +the same name but different nests. + +| Nest type | generic nest | generic object | object name | +|:----------|:------------:|:--------------:|:------------| +| function | | | `.:` | +| function | █ | | `.:[;]` | +| function | | █ | `.:[]` | +| function | █ | █ | `.:[;]` | +| method | | | `.::` | +| method | █ | | `.::[;]` | +| method | | █ | `.::[]` | +| method | █ | █ | `.::[;]` | #### Name Specifics diff --git a/compiler/internal/dce/collector.go b/compiler/internal/dce/collector.go index fea52468d..3ddac5e8b 100644 --- a/compiler/internal/dce/collector.go +++ b/compiler/internal/dce/collector.go @@ -39,8 +39,8 @@ func (c *Collector) CollectDCEDeps(decl Decl, f func()) { // The given optional type arguments are used to when the object is a // function with type parameters or anytime the object doesn't carry them. // If not given, this attempts to get the type arguments from the object. -func (c *Collector) DeclareDCEDep(o types.Object, tArgs ...types.Type) { +func (c *Collector) DeclareDCEDep(o types.Object, tNest, tArgs []types.Type) { if c.dce != nil { - c.dce.addDep(o, tArgs) + c.dce.addDep(o, tNest, tArgs) } } diff --git a/compiler/internal/dce/dce_test.go b/compiler/internal/dce/dce_test.go index 3ddeac848..849596a37 100644 --- a/compiler/internal/dce/dce_test.go +++ b/compiler/internal/dce/dce_test.go @@ -43,34 +43,34 @@ func Test_Collector_Collecting(t *testing.T) { decl2 := quickTestDecl(obj2) var c Collector - c.DeclareDCEDep(obj1) // no effect since a collection isn't running. + c.DeclareDCEDep(obj1, nil, nil) // no effect since a collection isn't running. depCount(t, decl1, 0) depCount(t, decl2, 0) c.CollectDCEDeps(decl1, func() { - c.DeclareDCEDep(obj2) - c.DeclareDCEDep(obj3) - c.DeclareDCEDep(obj3) // already added so has no effect. + c.DeclareDCEDep(obj2, nil, nil) + c.DeclareDCEDep(obj3, nil, nil) + c.DeclareDCEDep(obj3, nil, nil) // already added so has no effect. }) depCount(t, decl1, 2) depCount(t, decl2, 0) - c.DeclareDCEDep(obj4) // no effect since a collection isn't running. + c.DeclareDCEDep(obj4, nil, nil) // no effect since a collection isn't running. depCount(t, decl1, 2) depCount(t, decl2, 0) c.CollectDCEDeps(decl2, func() { - c.DeclareDCEDep(obj5) - c.DeclareDCEDep(obj6) - c.DeclareDCEDep(obj7) + c.DeclareDCEDep(obj5, nil, nil) + c.DeclareDCEDep(obj6, nil, nil) + c.DeclareDCEDep(obj7, nil, nil) }) depCount(t, decl1, 2) depCount(t, decl2, 3) // The second collection adds to existing dependencies. c.CollectDCEDeps(decl2, func() { - c.DeclareDCEDep(obj4) - c.DeclareDCEDep(obj5) + c.DeclareDCEDep(obj4, nil, nil) + c.DeclareDCEDep(obj5, nil, nil) }) depCount(t, decl1, 2) depCount(t, decl2, 4) @@ -541,7 +541,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { equal(t, d.Dce().String(), `[unnamed] -> []`) t.Log(`object:`, types.ObjectString(tt.obj, nil)) - d.Dce().SetName(tt.obj) + d.Dce().SetName(tt.obj, nil, nil) equal(t, d.Dce().unnamed(), tt.want.unnamed()) equal(t, d.Dce().objectFilter, tt.want.objectFilter) equal(t, d.Dce().methodFilter, tt.want.methodFilter) @@ -568,7 +568,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { c := Collector{} c.CollectDCEDeps(d, func() { - c.DeclareDCEDep(tt.obj) + c.DeclareDCEDep(tt.obj, nil, nil) }) equalSlices(t, d.Dce().getDeps(), wantDeps) }) @@ -582,10 +582,10 @@ func Test_Info_SetNameOnlyOnce(t *testing.T) { obj2 := quickVar(pkg, `Stripe`) decl := &testDecl{} - decl.Dce().SetName(obj1) + decl.Dce().SetName(obj1, nil, nil) err := capturePanic(t, func() { - decl.Dce().SetName(obj2) + decl.Dce().SetName(obj2, nil, nil) }) errorMatches(t, err, `^may only set the name once for path/to/mogwai\.Gizmo .*$`) } @@ -743,7 +743,7 @@ func Test_Info_UsesDeps(t *testing.T) { c := Collector{} c.CollectDCEDeps(d, func() { - c.DeclareDCEDep(uses, tArgs...) + c.DeclareDCEDep(uses, nil, tArgs) }) equalSlices(t, d.Dce().getDeps(), tt.wantDeps) }) @@ -752,10 +752,11 @@ func Test_Info_UsesDeps(t *testing.T) { func Test_Info_SpecificCasesDeps(t *testing.T) { tests := []struct { - name string - obj types.Object - tArgs []types.Type - wantDeps []string + name string + obj types.Object + nestTArgs []types.Type + tArgs []types.Type + wantDeps []string }{ { name: `struct instantiation with generic object`, @@ -808,16 +809,113 @@ func Test_Info_SpecificCasesDeps(t *testing.T) { `astoria.shuffle(string) int`, }, }, + { + name: `a concrete function with a nested concrete type instance`, + obj: parseObject(t, `davi`, + `package astoria + func jake(v int) any { + type davi struct { V int } + return davi{ V: v } + }`), + wantDeps: []string{`astoria.jake:davi`}, + }, + { + name: `a concrete function with a nested generic type instance`, + obj: parseObject(t, `pantoliano`, + `package astoria + func francis(v int) any { + type pantoliano[T any] struct { V int } + return pantoliano[int]{ V: v } + }`), + tArgs: []types.Type{types.Typ[types.Int]}, + wantDeps: []string{`astoria.francis:pantoliano[int]`}, + }, + { + name: `a generic function with a nested concrete type instance`, + obj: parseObject(t, `ramsey`, + `package astoria + func mama[T any](v T) any { + type ramsey struct { V T } + return ramsey{ V: v } + }`), + nestTArgs: []types.Type{types.Typ[types.Int]}, + wantDeps: []string{`astoria.mama:ramsey[int;]`}, + }, + { + name: `a generic function with a nested generic type instance`, + obj: parseObject(t, `matuszak`, + `package astoria + func sloth[T any]() any { + type matuszak[U any] struct { X T; Y U } + return matuszak[bool]{} + }`), + nestTArgs: []types.Type{types.Typ[types.String]}, + tArgs: []types.Type{types.Typ[types.Bool]}, + wantDeps: []string{`astoria.sloth:matuszak[string; bool]`}, + }, + { + name: `a concrete method with a nested concrete type instance`, + obj: parseObject(t, `davi`, + `package astoria + type fratelli struct { V int } + func (m *fratelli) jake() any { + type davi struct { V int } + return davi{ V: m.V } + }`), + wantDeps: []string{`astoria.fratelli:jake:davi`}, + }, + { + name: `a concrete method with a nested generic type instance`, + obj: parseObject(t, `pantoliano`, + `package astoria + type fratelli struct { V int } + func (f *fratelli) francis(v int) any { + type pantoliano[T any] struct { V int } + return pantoliano[int]{ V: v } + }`), + tArgs: []types.Type{types.Typ[types.Int]}, + wantDeps: []string{`astoria.fratelli:francis:pantoliano[int]`}, + }, + { + name: `a generic method with a nested concrete type instance`, + obj: parseObject(t, `ramsey`, + `package astoria + type fratelli[T any] struct { v T } + func (f *fratelli[T]) mama() any { + type ramsey struct { V T } + return ramsey{ V: f.v } + }`), + nestTArgs: []types.Type{types.Typ[types.Int]}, + wantDeps: []string{`astoria.fratelli:mama:ramsey[int;]`}, + }, + { + name: `a generic method with a nested generic type instance`, + obj: parseObject(t, `matuszak`, + `package astoria + type fratelli[T any] struct {} + func (f *fratelli[T]) sloth() any { + type matuszak[U any] struct { X T; Y U } + return matuszak[bool]{} + }`), + nestTArgs: []types.Type{types.Typ[types.String]}, + tArgs: []types.Type{types.Typ[types.Bool]}, + wantDeps: []string{`astoria.fratelli:sloth:matuszak[string; bool]`}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { d := &testDecl{} - t.Logf(`object: %s with [%s]`, types.ObjectString(tt.obj, nil), (typesutil.TypeList)(tt.tArgs).String()) + tail := `` + if len(tt.nestTArgs) > 0 { + tail += (typesutil.TypeList)(tt.nestTArgs).String() + `;` + } + tail += (typesutil.TypeList)(tt.tArgs).String() + t.Logf(`object: %s with [%s]`, types.ObjectString(tt.obj, nil), tail) c := Collector{} c.CollectDCEDeps(d, func() { - c.DeclareDCEDep(tt.obj, tt.tArgs...) + c.DeclareDCEDep(tt.obj, tt.nestTArgs, tt.tArgs) }) equalSlices(t, d.Dce().getDeps(), tt.wantDeps) }) @@ -837,7 +935,7 @@ func Test_Info_SetAsAlive(t *testing.T) { equal(t, decl.Dce().isAlive(), true) // still alive but now explicitly alive equal(t, decl.Dce().String(), `[alive] [unnamed] -> []`) - decl.Dce().SetName(obj) + decl.Dce().SetName(obj, nil, nil) equal(t, decl.Dce().isAlive(), true) // alive because SetAsAlive was called equal(t, decl.Dce().String(), `[alive] path/to/fantasia.Falkor -> []`) }) @@ -848,7 +946,7 @@ func Test_Info_SetAsAlive(t *testing.T) { equal(t, decl.Dce().isAlive(), true) // unnamed is automatically alive equal(t, decl.Dce().String(), `[unnamed] -> []`) - decl.Dce().SetName(obj) + decl.Dce().SetName(obj, nil, nil) equal(t, decl.Dce().isAlive(), false) // named so no longer automatically alive equal(t, decl.Dce().String(), `path/to/fantasia.Artax -> []`) @@ -876,27 +974,27 @@ func Test_Selector_JustVars(t *testing.T) { c := Collector{} c.CollectDCEDeps(frodo, func() { - c.DeclareDCEDep(samwise.obj) - c.DeclareDCEDep(meri.obj) - c.DeclareDCEDep(pippin.obj) + c.DeclareDCEDep(samwise.obj, nil, nil) + c.DeclareDCEDep(meri.obj, nil, nil) + c.DeclareDCEDep(pippin.obj, nil, nil) }) c.CollectDCEDeps(pippin, func() { - c.DeclareDCEDep(meri.obj) + c.DeclareDCEDep(meri.obj, nil, nil) }) c.CollectDCEDeps(aragorn, func() { - c.DeclareDCEDep(boromir.obj) + c.DeclareDCEDep(boromir.obj, nil, nil) }) c.CollectDCEDeps(gimli, func() { - c.DeclareDCEDep(legolas.obj) + c.DeclareDCEDep(legolas.obj, nil, nil) }) c.CollectDCEDeps(legolas, func() { - c.DeclareDCEDep(gimli.obj) + c.DeclareDCEDep(gimli.obj, nil, nil) }) c.CollectDCEDeps(gandalf, func() { - c.DeclareDCEDep(frodo.obj) - c.DeclareDCEDep(aragorn.obj) - c.DeclareDCEDep(gimli.obj) - c.DeclareDCEDep(legolas.obj) + c.DeclareDCEDep(frodo.obj, nil, nil) + c.DeclareDCEDep(aragorn.obj, nil, nil) + c.DeclareDCEDep(gimli.obj, nil, nil) + c.DeclareDCEDep(legolas.obj, nil, nil) }) for _, decl := range fellowship { @@ -1012,16 +1110,16 @@ func Test_Selector_SpecificMethods(t *testing.T) { c := Collector{} c.CollectDCEDeps(rincewindRun, func() { - c.DeclareDCEDep(rincewind.obj) + c.DeclareDCEDep(rincewind.obj, nil, nil) }) c.CollectDCEDeps(rincewindHide, func() { - c.DeclareDCEDep(rincewind.obj) + c.DeclareDCEDep(rincewind.obj, nil, nil) }) c.CollectDCEDeps(vimesRun, func() { - c.DeclareDCEDep(vimes.obj) + c.DeclareDCEDep(vimes.obj, nil, nil) }) c.CollectDCEDeps(vimesRead, func() { - c.DeclareDCEDep(vimes.obj) + c.DeclareDCEDep(vimes.obj, nil, nil) }) vetinari.Dce().SetAsAlive() @@ -1058,7 +1156,7 @@ func Test_Selector_SpecificMethods(t *testing.T) { vetinari.Dce().deps = nil // reset deps c.CollectDCEDeps(vetinari, func() { for _, decl := range tt.deps { - c.DeclareDCEDep(decl.obj) + c.DeclareDCEDep(decl.obj, nil, nil) } }) @@ -1095,7 +1193,7 @@ func testPackage(name string) *types.Package { func quickTestDecl(o types.Object) *testDecl { d := &testDecl{obj: o} - d.Dce().SetName(o) + d.Dce().SetName(o, nil, nil) return d } diff --git a/compiler/internal/dce/filters.go b/compiler/internal/dce/filters.go index 420fd4310..c074fe545 100644 --- a/compiler/internal/dce/filters.go +++ b/compiler/internal/dce/filters.go @@ -5,6 +5,9 @@ import ( "sort" "strconv" "strings" + + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/typesutil" ) // getFilters determines the DCE filters for the given object. @@ -16,7 +19,7 @@ import ( // the object filter will be empty and only the method filter will be set. // The later shouldn't happen when naming a declaration but only when creating // dependencies. -func getFilters(o types.Object, tArgs []types.Type) (objectFilter, methodFilter string) { +func getFilters(o types.Object, nestTArgs, tArgs []types.Type) (objectFilter, methodFilter string) { if f, ok := o.(*types.Func); ok { sig := f.Type().(*types.Signature) if recv := sig.Recv(); recv != nil { @@ -30,7 +33,7 @@ func getFilters(o types.Object, tArgs []types.Type) (objectFilter, methodFilter tArgs = getTypeArgs(typ) } if named, ok := typ.(*types.Named); ok { - objectFilter = getObjectFilter(named.Obj(), tArgs) + objectFilter = getObjectFilter(named.Obj(), nil, tArgs) } // The method is not exported so we only need the method filter. @@ -42,7 +45,7 @@ func getFilters(o types.Object, tArgs []types.Type) (objectFilter, methodFilter } // The object is not a method so we only need the object filter. - objectFilter = getObjectFilter(o, tArgs) + objectFilter = getObjectFilter(o, nestTArgs, tArgs) return } @@ -51,8 +54,8 @@ func getFilters(o types.Object, tArgs []types.Type) (objectFilter, methodFilter // See [naming design] for more information. // // [naming design]: https://github.com/gopherjs/gopherjs/compiler/internal/dce/README.md#naming -func getObjectFilter(o types.Object, tArgs []types.Type) string { - return (&filterGen{argTypeRemap: tArgs}).Object(o, tArgs) +func getObjectFilter(o types.Object, nestTArgs, tArgs []types.Type) string { + return (&filterGen{}).Object(o, nestTArgs, tArgs) } // getMethodFilter returns the method filter that functions as the secondary @@ -62,39 +65,74 @@ func getObjectFilter(o types.Object, tArgs []types.Type) string { // [naming design]: https://github.com/gopherjs/gopherjs/compiler/internal/dce/README.md#naming func getMethodFilter(o types.Object, tArgs []types.Type) string { if sig, ok := o.Type().(*types.Signature); ok { + gen := &filterGen{} + tParams := getTypeParams(o.Type()) if len(tArgs) == 0 { - if recv := sig.Recv(); recv != nil { - tArgs = getTypeArgs(recv.Type()) - } + tArgs = getTypeArgs(sig) + } + if len(tArgs) > 0 { + gen.addReplacements(tParams, tArgs) } - gen := &filterGen{argTypeRemap: tArgs} return objectName(o) + gen.Signature(sig) } return `` } // objectName returns the name part of a filter name, -// including the package path, if available. +// including the package path and nest names, if available. // // This is different from `o.Id` since it always includes the package path // when available and doesn't add "_." when not available. func objectName(o types.Object) string { + prefix := `` if o.Pkg() != nil { - return o.Pkg().Path() + `.` + o.Name() + prefix += o.Pkg().Path() + `.` + } + if nest := typeparams.FindNestingFunc(o); nest != nil && nest != o { + if recv := typesutil.RecvType(nest.Type().(*types.Signature)); recv != nil { + prefix += recv.Obj().Name() + `:` + } + prefix += nest.Name() + `:` + } + return prefix + o.Name() +} + +// getNestTypeParams gets the type parameters for the nesting function +// or nil if the object is not nested in a function or +// the given object is a function itself. +func getNestTypeParams(o types.Object) []types.Type { + fn := typeparams.FindNestingFunc(o) + if fn == nil || fn == o { + return nil + } + + tp := typeparams.SignatureTypeParams(fn.Type().(*types.Signature)) + nestTParams := make([]types.Type, tp.Len()) + for i := 0; i < tp.Len(); i++ { + nestTParams[i] = tp.At(i) } - return o.Name() + return nestTParams } // getTypeArgs gets the type arguments for the given type -// wether they are type arguments or type parameters. +// or nil if the type does not have type arguments. func getTypeArgs(typ types.Type) []types.Type { switch t := typ.(type) { case *types.Pointer: return getTypeArgs(t.Elem()) case *types.Named: - if typeArgs := t.TypeArgs(); typeArgs != nil { - return typeListToSlice(typeArgs) - } + return typeListToSlice(t.TypeArgs()) + } + return nil +} + +// getTypeParams gets the type parameters for the given type +// or nil if the type does not have type parameters. +func getTypeParams(typ types.Type) []types.Type { + switch t := typ.(type) { + case *types.Pointer: + return getTypeParams(t.Elem()) + case *types.Named: if typeParams := t.TypeParams(); typeParams != nil { return typeParamListToSlice(typeParams) } @@ -122,22 +160,28 @@ func typeListToSlice(typeArgs *types.TypeList) []types.Type { func typeParamListToSlice(typeParams *types.TypeParamList) []types.Type { tParams := make([]types.Type, typeParams.Len()) for i := range tParams { - tParams[i] = typeParams.At(i).Constraint() + tParams[i] = typeParams.At(i) } return tParams } type processingGroup struct { - o types.Object - tArgs []types.Type + o types.Object + nestTArgs []types.Type + tArgs []types.Type } -func (p processingGroup) is(o types.Object, tArgs []types.Type) bool { - if len(p.tArgs) != len(tArgs) || p.o != o { +func (p processingGroup) is(o types.Object, nestTArgs, tArgs []types.Type) bool { + if len(p.nestTArgs) != len(nestTArgs) || len(p.tArgs) != len(tArgs) || p.o != o { return false } - for i, tArg := range tArgs { - if p.tArgs[i] != tArg { + for i, ta := range nestTArgs { + if p.nestTArgs[i] != ta { + return false + } + } + for i, ta := range tArgs { + if p.tArgs[i] != ta { return false } } @@ -145,20 +189,79 @@ func (p processingGroup) is(o types.Object, tArgs []types.Type) bool { } type filterGen struct { - // argTypeRemap is the type arguments in the same order as the - // type parameters in the top level object such that the type parameters - // index can be used to get the type argument. - argTypeRemap []types.Type - inProgress []processingGroup + // replacement is used to use another type in place of a given type + // this is typically used for type parameters to type arguments. + replacement map[types.Type]types.Type + inProgress []processingGroup +} + +// addReplacements adds a mapping from one type to another. +// The slices should be the same length but will ignore any extra types. +// Any replacement where the key and value are the same will be ignored. +func (gen *filterGen) addReplacements(from []types.Type, to []types.Type) { + if len(from) == 0 || len(to) == 0 { + return + } + + if gen.replacement == nil { + gen.replacement = map[types.Type]types.Type{} + } + + count := len(from) + if count > len(to) { + count = len(to) + } + for i := 0; i < count; i++ { + if from[i] != to[i] { + gen.replacement[from[i]] = to[i] + } + } } -func (gen *filterGen) startProcessing(o types.Object, tArgs []types.Type) bool { +// pushGenerics prepares the filter generator for processing an object +// by setting any generic information and nesting information needed for it. +// It returns the type arguments for the object and a function to restore +// the previous state of the filter generator. +func (gen *filterGen) pushGenerics(o types.Object, nestTArgs, tArgs []types.Type) ([]types.Type, []types.Type, func()) { + // Create a new replacement map and copy the old one into it. + oldReplacement := gen.replacement + gen.replacement = map[types.Type]types.Type{} + for tp, ta := range oldReplacement { + gen.replacement[tp] = ta + } + + // Prepare the nested context for the object. + nestTParams := getNestTypeParams(o) + if len(nestTArgs) > 0 { + gen.addReplacements(nestTParams, nestTArgs) + } else { + nestTArgs = nestTParams + } + + // Prepare the type arguments for the object. + tParams := getTypeParams(o.Type()) + if len(tArgs) == 0 { + tArgs = getTypeArgs(o.Type()) + } + if len(tArgs) > 0 { + gen.addReplacements(tParams, tArgs) + } else { + tArgs = tParams + } + + // Return a function to restore the old replacement map. + return nestTArgs, tArgs, func() { + gen.replacement = oldReplacement + } +} + +func (gen *filterGen) startProcessing(o types.Object, nestTArgs, tArgs []types.Type) bool { for _, p := range gen.inProgress { - if p.is(o, tArgs) { + if p.is(o, nestTArgs, tArgs) { return false } } - gen.inProgress = append(gen.inProgress, processingGroup{o, tArgs}) + gen.inProgress = append(gen.inProgress, processingGroup{o: o, nestTArgs: nestTArgs, tArgs: tArgs}) return true } @@ -167,19 +270,19 @@ func (gen *filterGen) stopProcessing() { } // Object returns an object filter or filter part for an object. -func (gen *filterGen) Object(o types.Object, tArgs []types.Type) string { +func (gen *filterGen) Object(o types.Object, nestTArgs, tArgs []types.Type) string { filter := objectName(o) // Add additional type information for generics and instances. - if len(tArgs) == 0 { - tArgs = getTypeArgs(o.Type()) - } - if len(tArgs) > 0 { + nestTArgs, tArgs, popGenerics := gen.pushGenerics(o, nestTArgs, tArgs) + defer popGenerics() + + if len(tArgs) > 0 || len(nestTArgs) > 0 { // Avoid infinite recursion in type arguments by // tracking the current object and type arguments being processed // and skipping if already in progress. - if gen.startProcessing(o, tArgs) { - filter += gen.TypeArgs(tArgs) + if gen.startProcessing(o, nestTArgs, tArgs) { + filter += gen.TypeArgs(nestTArgs, tArgs) gen.stopProcessing() } else { filter += `[...]` @@ -205,13 +308,24 @@ func (gen *filterGen) Signature(sig *types.Signature) string { } // TypeArgs returns the filter part containing the type -// arguments, e.g. `[any,int|string]`. -func (gen *filterGen) TypeArgs(tArgs []types.Type) string { - parts := make([]string, len(tArgs)) - for i, tArg := range tArgs { - parts[i] = gen.Type(tArg) +// arguments, e.g. `[bool;any,int|string]`. +func (gen *filterGen) TypeArgs(nestTArgs, tArgs []types.Type) string { + toStr := func(t []types.Type) string { + parts := make([]string, len(t)) + for i, ta := range t { + parts[i] = gen.Type(ta) + } + return strings.Join(parts, `, `) + } + + head := `[` + if len(nestTArgs) > 0 { + head += toStr(nestTArgs) + `;` + if len(tArgs) > 0 { + head += ` ` + } } - return `[` + strings.Join(parts, `, `) + `]` + return head + toStr(tArgs) + `]` } // Tuple returns the filter part containing parameter or result @@ -236,9 +350,6 @@ func (gen *filterGen) Tuple(t *types.Tuple, variadic bool) string { // Type returns the filter part for a single type. func (gen *filterGen) Type(typ types.Type) string { switch t := typ.(type) { - case types.Object: - return gen.Object(t, nil) - case *types.Array: return `[` + strconv.FormatInt(t.Len(), 10) + `]` + gen.Type(t.Elem()) case *types.Chan: @@ -248,8 +359,8 @@ func (gen *filterGen) Type(typ types.Type) string { case *types.Map: return `map[` + gen.Type(t.Key()) + `]` + gen.Type(t.Elem()) case *types.Named: - // Get type args from named instance not generic object - return gen.Object(t.Obj(), getTypeArgs(t)) + // Get type args from named instance not generic object. + return gen.Object(t.Obj(), nil, typeListToSlice(t.TypeArgs())) case *types.Pointer: return `*` + gen.Type(t.Elem()) case *types.Signature: @@ -328,14 +439,10 @@ func (gen *filterGen) Struct(s *types.Struct) string { } // TypeParam returns the filter part for a type parameter. -// If there is an argument remap, it will use the remapped type -// so long as it doesn't map to itself. +// If there is an argument remap, it will use the remapped type. func (gen *filterGen) TypeParam(t *types.TypeParam) string { - index := t.Index() - if index >= 0 && index < len(gen.argTypeRemap) { - if inst := gen.argTypeRemap[index]; inst != t { - return gen.Type(inst) - } + if inst, exists := gen.replacement[t]; exists { + return gen.Type(inst) } if t.Constraint() == nil { return `any` diff --git a/compiler/internal/dce/info.go b/compiler/internal/dce/info.go index 6a45e9ef3..ef310f047 100644 --- a/compiler/internal/dce/info.go +++ b/compiler/internal/dce/info.go @@ -81,19 +81,19 @@ func (d *Info) SetAsAlive() { // The given optional type arguments are used to when the object is a // function with type parameters or anytime the object doesn't carry them. // If not given, this attempts to get the type arguments from the object. -func (d *Info) SetName(o types.Object, tArgs ...types.Type) { +func (d *Info) SetName(o types.Object, tNest, tArgs []types.Type) { if !d.unnamed() { panic(fmt.Errorf(`may only set the name once for %s`, d.String())) } // Determine name(s) for DCE. - d.objectFilter, d.methodFilter = getFilters(o, tArgs) + d.objectFilter, d.methodFilter = getFilters(o, tNest, tArgs) } // addDep add a declaration dependencies used by DCE // for the declaration this DCE info is attached to. -func (d *Info) addDep(o types.Object, tArgs []types.Type) { - objectFilter, methodFilter := getFilters(o, tArgs) +func (d *Info) addDep(o types.Object, tNest, tArgs []types.Type) { + objectFilter, methodFilter := getFilters(o, tNest, tArgs) d.addDepName(objectFilter) d.addDepName(methodFilter) } diff --git a/compiler/internal/typeparams/utils.go b/compiler/internal/typeparams/utils.go index 9fcf53c36..27fab0a89 100644 --- a/compiler/internal/typeparams/utils.go +++ b/compiler/internal/typeparams/utils.go @@ -21,24 +21,71 @@ func SignatureTypeParams(sig *types.Signature) *types.TypeParamList { } // FindNestingFunc returns the function or method that the given object -// is nested in, or nil if the object was defined at the package level. +// is nested in. Returns nil if the object was defined at the package level, +// the position is invalid, or if the object is a function or method. +// +// This may get different results for some specific object types +// (e.g. receivers, type parameters) depending on the Go version. +// In go1.19 and earlier, some objects are not nested in the function +// they are part of the definition of, but in later versions they are. func FindNestingFunc(obj types.Object) *types.Func { + if obj == nil { + return nil + } objPos := obj.Pos() if objPos == token.NoPos { return nil } - // We can't use `obj.Parent()` here since some types don't have a set - // parent, such as types created with `types.NewTypeName`. Instead find - // the innermost scope from the package to use as the object's parent scope. - scope := obj.Pkg().Scope().Innermost(objPos) + if _, ok := obj.(*types.Func); ok { + // Functions and methods are not nested in any other function. + return nil + } + + var pkgScope *types.Scope + if obj.Pkg() != nil { + pkgScope = obj.Pkg().Scope() + } + scope := obj.Parent() + if scope == nil { + // Some types have a nil parent scope, such as methods and field, and + // types created with `types.NewTypeName`. Instead find the innermost + // scope from the package to use as the object's parent scope. + if pkgScope == nil { + return nil + } + scope = pkgScope.Innermost(objPos) + } + + if scope == pkgScope { + // If the object is defined at the package level, + // we can shortcut this check and just return nil. + return nil + } + + // Walk up the scope chain to find the function or method that contains + // the object at the given position. for scope != nil { - // Iterate over all declarations in the scope. + // Iterate over all objects declared in the scope. for _, name := range scope.Names() { - decl := scope.Lookup(name) - if fn, ok := decl.(*types.Func); ok && fn.Scope().Contains(objPos) { + d := scope.Lookup(name) + if fn, ok := d.(*types.Func); ok && fn.Scope() != nil && fn.Scope().Contains(objPos) { return fn } + + if named, ok := d.Type().(*types.Named); ok { + // Iterate over all methods of an object. + for i := 0; i < named.NumMethods(); i++ { + if m := named.Method(i); m != nil && m.Scope() != nil && m.Scope().Contains(objPos) { + return m + } + } + } + } + if scope == pkgScope { + // If we reached the package scope, stop searching. + // We don't need to check the universal scope. + break } scope = scope.Parent() } diff --git a/compiler/internal/typeparams/utils_test.go b/compiler/internal/typeparams/utils_test.go index dda685273..1e10e963a 100644 --- a/compiler/internal/typeparams/utils_test.go +++ b/compiler/internal/typeparams/utils_test.go @@ -2,10 +2,13 @@ package typeparams import ( "errors" + "fmt" + "go/ast" "go/token" "go/types" "testing" + "github.com/google/go-cmp/cmp" "github.com/gopherjs/gopherjs/internal/srctesting" ) @@ -118,3 +121,165 @@ func TestRequiresGenericsSupport(t *testing.T) { } }) } + +func Test_FindNestingFunc(t *testing.T) { + src := `package main + + type bob int + func (a bob) riker() any { + type bill struct{ b int } + return bill{b: int(a)} + } + + type milo[T any] struct{} + func (c *milo[U]) mario() any { + type homer struct{ d U } + return homer{} + } + + func bart() any { + e := []milo[int]{{}} + f := &e[0] + return f.mario() + } + + func jack() any { + type linus interface{ + interface { + marvin() + } + luke() + } + type owen interface { + linus + isaac() + } + return owen(nil) + } + + func bender() any { + charles := func() any { + type arthur struct{ h int } + return arthur{h: 42} + } + return charles() + } + + var ned = func() any { + type elmer struct{ i int } + return elmer{i: 42} + }() + + func garfield(count int) { + calvin: + for j := 0; j < count; j++ { + if j == 5 { + break calvin + } + } + }` + + f := srctesting.New(t) + file := f.Parse("main.go", src) + info, _ := f.Check("test", file) + + // Collect all objects and find nesting functions. + // The results will be ordered by position in the file. + results := []string{} + ast.Inspect(file, func(n ast.Node) bool { + if id, ok := n.(*ast.Ident); ok { + obj := info.ObjectOf(id) + if _, isVar := obj.(*types.Var); isVar { + // Skip variables, some variables (e.g. receivers) are not inside + // a function's scope in go1.19 but in later versions they are. + return true + } + if named, isNamed := obj.(*types.TypeName); isNamed { + if _, isTP := named.Type().(*types.TypeParam); isTP { + // Skip type parameters since they are not inside + // a function's scope in go1.19 but in later versions they are. + return true + } + } + + fn := FindNestingFunc(obj) + fnName := `` + if fn != nil { + fnName = fn.FullName() + } + results = append(results, fmt.Sprintf("%3d) %s => %s", id.Pos(), id.Name, fnName)) + } + return true + }) + + diff := cmp.Diff([]string{ + // package main (nil object) + ` 9) main => `, + + // type bob int + ` 22) bob => `, + ` 26) int => `, // use of basic + + // func (a bob) riker() any { ... } + ` 40) bob => `, + ` 45) riker => `, + ` 53) any => `, + ` 67) bill => (test.bob).riker`, // def + ` 82) int => `, + ` 98) bill => (test.bob).riker`, // use + `106) int => `, + + // type milo[T any] struct {} + `126) milo => `, + `133) any => `, + + // func (c *milo[U]) mario() any { ... } + `158) milo => `, + `167) mario => `, + `175) any => `, + `189) homer => (*test.milo[U]).mario`, // def + `219) homer => (*test.milo[U]).mario`, // use + + // func bart() any { ... } + `239) bart => `, + `246) any => `, + `262) milo => `, // use of non-local defined type + `267) int => `, + `302) mario => `, // use of method on non-local defined type + + // func jack() any { ... } + `322) jack => `, + `329) any => `, + `343) linus => test.jack`, // def + `381) marvin => `, // method def + `400) luke => `, // method def + `420) owen => test.jack`, // def + `441) linus => test.jack`, // use + `451) isaac => `, // method def + `474) owen => test.jack`, // use + `479) nil => `, // use of nil + + // func bender() any { ... } + `496) bender => `, + `505) any => `, + `532) any => `, + `547) arthur => test.bender`, // def inside func lit + `564) int => `, + `581) arthur => test.bender`, // use + + // var ned = func() any { ... } + `646) any => `, + `660) elmer => `, // def inside package-level func lit + `676) int => `, + `692) elmer => `, // use + + // func garfield(count int) { ... } + `719) garfield => `, + `734) int => `, + `744) calvin => `, // local label def + `811) calvin => `, // label break + }, results) + if len(diff) > 0 { + t.Errorf("FindNestingFunc() mismatch (-want +got):\n%s", diff) + } +} diff --git a/compiler/statements.go b/compiler/statements.go index 17ed8b746..5c7ce8769 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -446,7 +446,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { id := spec.(*ast.TypeSpec).Name o := fc.pkgCtx.Defs[id].(*types.TypeName) fc.pkgCtx.typeNames.Add(o) - fc.pkgCtx.DeclareDCEDep(o) + fc.pkgCtx.DeclareDCEDep(o, fc.instance.TArgs, nil) } case token.CONST: // skip, constants are inlined diff --git a/compiler/utils.go b/compiler/utils.go index 7d286f447..34d3f7960 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -415,7 +415,12 @@ func (fc *funcContext) assignedObjectName(o types.Object) (name string, found bo // allocated as needed. func (fc *funcContext) objectName(o types.Object) string { if isPkgLevel(o) { - fc.pkgCtx.DeclareDCEDep(o) + var nestTArgs []types.Type + if typeparams.FindNestingFunc(o) == fc.instance.Object { + // Only set the nest type arguments for objects nested in this funcContext. + nestTArgs = fc.instance.TArgs + } + fc.pkgCtx.DeclareDCEDep(o, nestTArgs, nil) if o.Pkg() != fc.pkgCtx.Pkg || (isVarOrConst(o) && o.Exported()) { return fc.pkgVar(o.Pkg()) + "." + o.Name() @@ -461,7 +466,7 @@ func (fc *funcContext) instName(inst typeparams.Instance) string { if inst.IsTrivial() { return objName } - fc.pkgCtx.DeclareDCEDep(inst.Object, inst.TArgs...) + fc.pkgCtx.DeclareDCEDep(inst.Object, inst.TNest, inst.TArgs) label := inst.TypeParamsString(` /* `, ` */`) return fmt.Sprintf("%s[%d%s]", objName, fc.pkgCtx.instanceSet.ID(inst), label) } @@ -547,7 +552,9 @@ func (fc *funcContext) typeName(ty types.Type) string { fc.pkgCtx.anonTypes = append(fc.pkgCtx.anonTypes, anonType) fc.pkgCtx.anonTypeMap.Set(ty, anonType) } - fc.pkgCtx.DeclareDCEDep(anonType) + // Since anonymous types are always package-level so they can be shared, + // don't pass in the function context (nest type parameters) to the DCE. + fc.pkgCtx.DeclareDCEDep(anonType, nil, nil) return anonType.Name() } From 1ff0ea50bbc97aa77226fbc4c77f9b1991864113 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Tue, 10 Jun 2025 13:05:21 -0600 Subject: [PATCH 49/58] Adding selectors to index handling --- compiler/astutil/astutil.go | 17 ++----- compiler/compiler_test.go | 72 ++++++++++++++++++++++++++++++ compiler/decls.go | 15 +++++-- compiler/expressions.go | 18 +++++++- compiler/functions.go | 2 +- compiler/internal/analysis/info.go | 32 ++++++++++++- tests/misc_test.go | 25 +++++++++++ tests/otherpkg/otherpkg.go | 7 +++ 8 files changed, 166 insertions(+), 22 deletions(-) diff --git a/compiler/astutil/astutil.go b/compiler/astutil/astutil.go index 9ff88a48c..5f259bfff 100644 --- a/compiler/astutil/astutil.go +++ b/compiler/astutil/astutil.go @@ -53,22 +53,11 @@ func IsTypeExpr(expr ast.Expr, info *types.Info) bool { _, ok := info.Uses[e].(*types.TypeName) return ok case *ast.SelectorExpr: - _, ok := info.Uses[e.Sel].(*types.TypeName) - return ok + return IsTypeExpr(e.Sel, info) case *ast.IndexExpr: - ident, ok := e.X.(*ast.Ident) - if !ok { - return false - } - _, ok = info.Uses[ident].(*types.TypeName) - return ok + return IsTypeExpr(e.X, info) case *ast.IndexListExpr: - ident, ok := e.X.(*ast.Ident) - if !ok { - return false - } - _, ok = info.Uses[ident].(*types.TypeName) - return ok + return IsTypeExpr(e.X, info) case *ast.ParenExpr: return IsTypeExpr(e.X, info) default: diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 88d8e525e..e3c7e551f 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -723,6 +723,78 @@ func Test_CrossPackageAnalysis(t *testing.T) { ) } +func Test_IndexedSelectors(t *testing.T) { + src1 := ` + package main + import "github.com/gopherjs/gopherjs/compiler/other" + func main() { + // Instance IndexExpr with a package SelectorExpr for a function call. + other.PrintZero[int]() + other.PrintZero[string]() + + // Instance IndexListExpr with a package SelectorExpr for a function call. + other.PrintZeroZero[int, string]() + + // Index IndexExpr with a struct SelectorExpr for a function call. + f := other.Foo{Ops: []func() { + other.PrintZero[int], + other.PrintZero[string], + other.PrintZeroZero[int, string], + }} + f.Ops[0]() + f.Ops[1]() + + // Index IndexExpr with a package/var SelectorExpr for a function call. + other.Bar.Ops[0]() + other.Baz[0]() + + // IndexExpr with a SelectorExpr for a cast + _ = other.ZHandle[int](other.PrintZero[int]) + + // IndexListExpr with a SelectorExpr for a cast + _ = other.ZZHandle[int, string](other.PrintZeroZero[int, string]) + }` + src2 := ` + package other + func PrintZero[T any]() { + var zero T + println("Zero is ", zero) + } + + func PrintZeroZero[T any, U any]() { + PrintZero[T]() + PrintZero[U]() + } + + type ZHandle[T any] func() + type ZZHandle[T any, U any] func() + + type Foo struct { Ops []func() } + var Bar = Foo{Ops: []func() { + PrintZero[int], + PrintZero[string], + }} + var Baz = Bar.Ops` + + root := srctesting.ParseSources(t, + []srctesting.Source{ + {Name: `main.go`, Contents: []byte(src1)}, + }, + []srctesting.Source{ + {Name: `other/other.go`, Contents: []byte(src2)}, + }) + + archives := compileProject(t, root, false) + // We mostly are checking that the code was turned into decls correctly, + // since the issue was that indexed selectors were not being handled correctly, + // so if it didn't panic by this point, it should be fine. + checkForDeclFullNames(t, archives, + `func:command-line-arguments.main`, + `type:github.com/gopherjs/gopherjs/compiler/other.ZHandle`, + `type:github.com/gopherjs/gopherjs/compiler/other.ZZHandle`, + ) +} + func TestArchiveSelectionAfterSerialization(t *testing.T) { src := ` package main diff --git a/compiler/decls.go b/compiler/decls.go index eb95cd2f7..b3be15b0c 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -282,25 +282,34 @@ func (fc *funcContext) funcDecls(functions []*ast.FuncDecl) ([]*Decl, error) { var mainFunc *types.Func for _, fun := range functions { o := fc.pkgCtx.Defs[fun.Name].(*types.Func) + instances := fc.knownInstances(o) + if len(instances) == 0 { + // No instances of the function, skip it. + continue + } if fun.Recv == nil { // Auxiliary decl shared by all instances of the function that defines - // package-level variable by which they all are referenced. + // package-level variable by which they all are referenced, + // e.g. init functions and instances of generic functions. objName := fc.objectName(o) varDecl := &Decl{ FullName: funcVarDeclFullName(o), Vars: []string{objName}, } varDecl.Dce().SetName(o) - if o.Type().(*types.Signature).TypeParams().Len() != 0 { + if len(instances) > 1 || !instances[0].IsTrivial() { varDecl.DeclCode = fc.CatchOutput(0, func() { fc.Printf("%s = {};", objName) + if o.Exported() { + fc.Printf("$pkg.%s = %s;", encodeIdent(fun.Name.Name), objName) + } }) } funcDecls = append(funcDecls, varDecl) } - for _, inst := range fc.knownInstances(o) { + for _, inst := range instances { funcDecls = append(funcDecls, fc.newFuncDecl(fun, inst)) if o.Name() == "main" { diff --git a/compiler/expressions.go b/compiler/expressions.go index 781a37a3e..772aba30c 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -529,14 +529,28 @@ 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.formatExpr("%s", fc.instName(fc.instanceOf(e.X.(*ast.Ident)))) + switch u := e.X.(type) { + case *ast.Ident: + return fc.formatExpr("%s", fc.instName(fc.instanceOf(u))) + case *ast.SelectorExpr: + return fc.formatExpr("%s", fc.instName(fc.instanceOf(u.Sel))) + default: + panic(fmt.Errorf("unhandled IndexExpr of a Signature: %T in %T", u, fc.typeOf(e.X))) + } default: panic(fmt.Errorf(`unhandled IndexExpr: %T`, t)) } case *ast.IndexListExpr: switch t := fc.typeOf(e.X).Underlying().(type) { case *types.Signature: - return fc.formatExpr("%s", fc.instName(fc.instanceOf(e.X.(*ast.Ident)))) + switch u := e.X.(type) { + case *ast.Ident: + return fc.formatExpr("%s", fc.instName(fc.instanceOf(u))) + case *ast.SelectorExpr: + return fc.formatExpr("%s", fc.instName(fc.instanceOf(u.Sel))) + default: + panic(fmt.Errorf("unhandled IndexListExpr of a Signature: %T in %T", u, fc.typeOf(e.X))) + } default: panic(fmt.Errorf("unhandled IndexListExpr: %T", t)) } diff --git a/compiler/functions.go b/compiler/functions.go index 361c92f0f..230c614b1 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -126,7 +126,7 @@ func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl) []byte { 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() { + if fun.Name.IsExported() && fc.instance.IsTrivial() { fmt.Fprintf(code, "\t$pkg.%s = %s;\n", encodeIdent(fun.Name.Name), lvalue) } return code.Bytes() diff --git a/compiler/internal/analysis/info.go b/compiler/internal/analysis/info.go index e400c870c..75fc8b572 100644 --- a/compiler/internal/analysis/info.go +++ b/compiler/internal/analysis/info.go @@ -574,7 +574,21 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr, deferredCall bool) ast.Visito if astutil.IsTypeExpr(f.Index, fi.pkgInfo.Info) { // This is a call of an instantiation of a generic function, // e.g. `foo[int]` in `func foo[T any]() { ... }; func main() { foo[int]() }` - fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident)), deferredCall) + var inst typeparams.Instance + switch fxt := f.X.(type) { + case *ast.Ident: + inst = fi.instanceForIdent(fxt) + case *ast.SelectorExpr: + if sel := fi.pkgInfo.Selections[fxt]; sel != nil { + inst = fi.instanceForSelection(sel) + } else { + // For qualified identifiers like `pkg.Foo` + inst = fi.instanceForIdent(fxt.Sel) + } + default: + panic(fmt.Errorf(`unexpected type %T for index expression %s`, f.X, f.X)) + } + fi.callToNamedFunc(inst, deferredCall) return fi } // The called function is gotten with an index or key from a map, array, or slice. @@ -596,7 +610,21 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr, deferredCall bool) ast.Visito } // This is a call of an instantiation of a generic function, // e.g. `foo[int, bool]` in `func foo[T1, T2 any]() { ... }; func main() { foo[int, bool]() }` - fi.callToNamedFunc(fi.instanceForIdent(f.X.(*ast.Ident)), deferredCall) + var inst typeparams.Instance + switch fxt := f.X.(type) { + case *ast.Ident: + inst = fi.instanceForIdent(fxt) + case *ast.SelectorExpr: + if sel := fi.pkgInfo.Selections[fxt]; sel != nil { + inst = fi.instanceForSelection(sel) + } else { + // For qualified identifiers like `pkg.Foo` + inst = fi.instanceForIdent(fxt.Sel) + } + default: + panic(fmt.Errorf(`unexpected type %T for index list expression %s`, f.X, f.X)) + } + fi.callToNamedFunc(inst, deferredCall) return fi default: if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { diff --git a/tests/misc_test.go b/tests/misc_test.go index 8dc3be924..4122b8170 100644 --- a/tests/misc_test.go +++ b/tests/misc_test.go @@ -959,3 +959,28 @@ func TestFileSetSize(t *testing.T) { t.Errorf("Got: unsafe.Sizeof(token.FileSet{}) %v, Want: %v", n2, n1) } } + +// TestCrossPackageGenericFuncCalls ensures that generic functions from other +// packages can be called correctly. +func TestCrossPackageGenericFuncCalls(t *testing.T) { + var wantInt int + if got := otherpkg.Zero[int](); got != wantInt { + t.Errorf(`Got: otherpkg.Zero[int]() = %v, Want: %v`, got, wantInt) + } + + var wantStr string + if got := otherpkg.Zero[string](); got != wantStr { + t.Errorf(`Got: otherpkg.Zero[string]() = %q, Want: %q`, got, wantStr) + } +} + +// TestCrossPackageGenericCasting ensures that generic types from other +// packages can be used in a type cast. +// The cast looks like a function call but should be treated as a type conversion. +func TestCrossPackageGenericCasting(t *testing.T) { + fn := otherpkg.GetterHandle[int](otherpkg.Zero[int]) + var wantInt int + if got := fn(); got != wantInt { + t.Errorf(`Got: otherpkg.GetterHandle[int](otherpkg.Zero[int]) = %v, Want: %v`, got, wantInt) + } +} diff --git a/tests/otherpkg/otherpkg.go b/tests/otherpkg/otherpkg.go index 2b413e5f1..6f9499526 100644 --- a/tests/otherpkg/otherpkg.go +++ b/tests/otherpkg/otherpkg.go @@ -1,3 +1,10 @@ package otherpkg var Test float32 + +func Zero[T any]() T { + var zero T + return zero +} + +type GetterHandle[T any] func() T From 6c9aa835446237aaa7985d2c5a0c4b169fbd2770 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Wed, 23 Jul 2025 16:52:04 -0600 Subject: [PATCH 50/58] Grouping by declaration kinds and adding $finishSetup --- .github/workflows/ci.yaml | 1 + compiler/compiler.go | 80 ++++++++++++++--- compiler/compiler_test.go | 14 +-- compiler/decls.go | 83 ++++++++++++------ compiler/functions.go | 22 ++--- compiler/internal/typeparams/collect_test.go | 7 +- compiler/prelude/prelude.js | 4 +- internal/govendor/subst/subst.go | 15 ++-- tests/gencircle_test.go | 85 +++++++++++++++++++ .../gencircle/burninate/burnable/burnable.go | 13 +++ .../gencircle/burninate/dragon/dragon.go | 9 ++ tests/testdata/gencircle/burninate/main.go | 17 ++++ tests/testdata/gencircle/burninate/main.out | 1 + tests/testdata/gencircle/catbox/box/box.go | 17 ++++ tests/testdata/gencircle/catbox/cat/cat.go | 5 ++ .../gencircle/catbox/collections/stack.go | 23 +++++ tests/testdata/gencircle/catbox/main.go | 19 +++++ tests/testdata/gencircle/catbox/main.out | 1 + tests/testdata/gencircle/pingpong/cat/cat.go | 16 ++++ .../gencircle/pingpong/collections/hashes.go | 21 +++++ .../gencircle/pingpong/collections/hashmap.go | 17 ++++ tests/testdata/gencircle/pingpong/main.go | 20 +++++ tests/testdata/gencircle/pingpong/main.out | 1 + tests/testdata/gencircle/simple/bar/bar.go | 5 ++ tests/testdata/gencircle/simple/foo/foo.go | 8 ++ tests/testdata/gencircle/simple/main.go | 21 +++++ tests/testdata/gencircle/simple/main.out | 1 + 27 files changed, 456 insertions(+), 70 deletions(-) create mode 100644 tests/gencircle_test.go create mode 100644 tests/testdata/gencircle/burninate/burnable/burnable.go create mode 100644 tests/testdata/gencircle/burninate/dragon/dragon.go create mode 100644 tests/testdata/gencircle/burninate/main.go create mode 100644 tests/testdata/gencircle/burninate/main.out create mode 100644 tests/testdata/gencircle/catbox/box/box.go create mode 100644 tests/testdata/gencircle/catbox/cat/cat.go create mode 100644 tests/testdata/gencircle/catbox/collections/stack.go create mode 100644 tests/testdata/gencircle/catbox/main.go create mode 100644 tests/testdata/gencircle/catbox/main.out create mode 100644 tests/testdata/gencircle/pingpong/cat/cat.go create mode 100644 tests/testdata/gencircle/pingpong/collections/hashes.go create mode 100644 tests/testdata/gencircle/pingpong/collections/hashmap.go create mode 100644 tests/testdata/gencircle/pingpong/main.go create mode 100644 tests/testdata/gencircle/pingpong/main.out create mode 100644 tests/testdata/gencircle/simple/bar/bar.go create mode 100644 tests/testdata/gencircle/simple/foo/foo.go create mode 100644 tests/testdata/gencircle/simple/main.go create mode 100644 tests/testdata/gencircle/simple/main.out diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 07ff3844a..01e32d5c8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,6 +8,7 @@ on: permissions: contents: read + pull-requests: read concurrency: group: ci-${{ github.ref }} diff --git a/compiler/compiler.go b/compiler/compiler.go index e8264c946..82a51d9cf 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -159,7 +159,7 @@ func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) err } } - if _, err := w.Write([]byte("$synthesizeMethods();\n$initAllLinknames();\nvar $mainPkg = $packages[\"" + string(mainPkg.ImportPath) + "\"];\n$packages[\"runtime\"].$init();\n$go($mainPkg.$init, []);\n$flushConsole();\n\n}).call(this);\n")); err != nil { + if _, err := w.Write([]byte("$callForAllPackages(\"$finishSetup\");\n$synthesizeMethods();\n$callForAllPackages(\"$initLinknames\");\nvar $mainPkg = $packages[\"" + string(mainPkg.ImportPath) + "\"];\n$packages[\"runtime\"].$init();\n$go($mainPkg.$init, []);\n$flushConsole();\n\n}).call(this);\n")); err != nil { return err } return nil @@ -183,13 +183,68 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls linkname.Go filteredDecls = append(filteredDecls, d) } } + // Write variable names if _, err := w.Write(removeWhitespace([]byte(fmt.Sprintf("\tvar %s;\n", strings.Join(vars, ", "))), minify)); err != nil { return err } + // Write imports for _, d := range filteredDecls { - if _, err := w.Write(d.DeclCode); err != nil { + if _, err := w.Write(d.ImportCode); err != nil { return err } + } + // Write named type declarations + for _, d := range filteredDecls { + if _, err := w.Write(d.TypeDeclCode); err != nil { + return err + } + } + // Write exports for named type declarations + for _, d := range filteredDecls { + if _, err := w.Write(d.ExportTypeCode); err != nil { + return err + } + } + + // The following parts have to be run after all packages have been added + // to handle generics that use named types defined in a package that + // is defined after this package has been defined. + if _, err := w.Write(removeWhitespace([]byte("\t$pkg.$finishSetup = function() {\n"), minify)); err != nil { + return err + } + + // Write anonymous type declarations + for _, d := range filteredDecls { + if _, err := w.Write(d.AnonTypeDeclCode); err != nil { + return err + } + } + // Write function declarations + for _, d := range filteredDecls { + if _, err := w.Write(d.FuncDeclCode); err != nil { + return err + } + } + // Write exports for function declarations + for _, d := range filteredDecls { + if _, err := w.Write(d.ExportFuncCode); err != nil { + return err + } + } + // Write reflection metadata for types' methods + for _, d := range filteredDecls { + if _, err := w.Write(d.MethodListCode); err != nil { + return err + } + } + // Write the calls to finish initialization of types + for _, d := range filteredDecls { + if _, err := w.Write(d.TypeInitCode); err != nil { + return err + } + } + + for _, d := range filteredDecls { if gls.IsImplementation(d.LinkingName) { // This decl is referenced by a go:linkname directive, expose it to external // callers via $linkname object (declared in prelude). We are not using @@ -205,16 +260,6 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls linkname.Go } } } - for _, d := range filteredDecls { - if _, err := w.Write(d.MethodListCode); err != nil { - return err - } - } - for _, d := range filteredDecls { - if _, err := w.Write(d.TypeInitCode); err != nil { - return err - } - } { // Set up all functions which package declares, but which implementation @@ -229,16 +274,23 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls linkname.Go if !found { continue // The symbol is not affected by a go:linkname directive. } - lines = append(lines, fmt.Sprintf("\t\t%s = $linknames[%q];\n", d.RefExpr, impl.String())) + lines = append(lines, fmt.Sprintf("\t\t\t%s = $linknames[%q];\n", d.RefExpr, impl.String())) } if len(lines) > 0 { - code := fmt.Sprintf("\t$pkg.$initLinknames = function() {\n%s};\n", strings.Join(lines, "")) + code := fmt.Sprintf("\t\t$pkg.$initLinknames = function() {\n%s};\n", strings.Join(lines, "")) if _, err := w.Write(removeWhitespace([]byte(code), minify)); err != nil { return err } } } + // Write the end of the `$finishSetup` function. + if _, err := w.Write(removeWhitespace([]byte("\t};\n"), minify)); err != nil { + return err + } + + // Write the initialization function that will initialize this package + // (e.g. initialize package-level variable value). if _, err := w.Write(removeWhitespace([]byte("\t$init = function() {\n\t\t$pkg.$init = function() {};\n\t\t/* */ var $f, $c = false, $s = 0, $r; if (this !== undefined && this.$blk !== undefined) { $f = this; $c = true; $s = $f.$s; $r = $f.$r; } s: while (true) { switch ($s) { case 0:\n"), minify)); err != nil { return err } diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 3562b01d4..f06a03c2f 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -569,11 +569,11 @@ func TestLengthParenthesizingIssue841(t *testing.T) { goodRegex := regexp.MustCompile(`\(a\s*\+\s*b\)\.length`) goodFound := false for i, decl := range mainPkg.Declarations { - if badRegex.Match(decl.DeclCode) { + if badRegex.Match(decl.FuncDeclCode) { t.Errorf("found length issue in decl #%d: %s", i, decl.FullName) - t.Logf("decl code:\n%s", string(decl.DeclCode)) + t.Logf("decl code:\n%s", string(decl.FuncDeclCode)) } - if goodRegex.Match(decl.DeclCode) { + if goodRegex.Match(decl.FuncDeclCode) { goodFound = true } } @@ -1121,13 +1121,17 @@ func collectDeclInstances(t *testing.T, pkg *Archive) []string { // Collect all instances of generics (e.g. `Foo[bar] @ 2`) written to the decl code. insts := []string{} - for _, decl := range pkg.Declarations { - if match := rex.FindAllStringSubmatch(string(decl.DeclCode), 1); len(match) > 0 { + checkCode := func(code []byte) { + if match := rex.FindAllStringSubmatch(string(code), 1); len(match) > 0 { instance := match[0][1] + `[` + strings.TrimSpace(match[0][3]) + `]` instance = strings.ReplaceAll(instance, `command-line-arguments`, pkg.Name) insts = append(insts, instance) } } + for _, decl := range pkg.Declarations { + checkCode(decl.TypeDeclCode) + checkCode(decl.FuncDeclCode) + } sort.Strings(insts) return insts } diff --git a/compiler/decls.go b/compiler/decls.go index eb5322130..63c3d5059 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -44,14 +44,30 @@ type Decl struct { RefExpr 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 declares a local variable for an imported package. + ImportCode []byte + // JavaScript code that declares basic information about a named type symbol. + // It configures basic information about the type and its identity. + TypeDeclCode []byte + // JavaScript code that assigns exposed named types to the package. + ExportTypeCode []byte + // JavaScript code that declares basic information about an anonymous types. + // It configures basic information about the type. + // This is added to the finish setup phase to has access to all packages. + AnonTypeDeclCode []byte + // JavaScript code that declares basic information about a function or + // method symbol. This contains the function's or method's compiled body. + // This is added to the finish setup phase to has access to all packages. + FuncDeclCode []byte + // JavaScript code that assigns exposed functions to the package. + // This is added to the finish setup phase to has access to all packages. + ExportFuncCode []byte // JavaScript code that initializes reflection metadata about type's method list. + // This is added to the finish setup phase to has access to all packages. MethodListCode []byte // JavaScript code that initializes the rest of reflection metadata about a type // (e.g. struct fields, array type sizes, element types, etc.). + // This is added to the finish setup phase to has access to all packages. 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). @@ -68,7 +84,12 @@ type Decl struct { // 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.ImportCode = removeWhitespace(d.ImportCode, true) + d.TypeDeclCode = removeWhitespace(d.TypeDeclCode, true) + d.ExportTypeCode = removeWhitespace(d.ExportTypeCode, true) + d.AnonTypeDeclCode = removeWhitespace(d.AnonTypeDeclCode, true) + d.FuncDeclCode = removeWhitespace(d.FuncDeclCode, true) + d.ExportFuncCode = removeWhitespace(d.ExportFuncCode, true) d.MethodListCode = removeWhitespace(d.MethodListCode, true) d.TypeInitCode = removeWhitespace(d.TypeInitCode, true) d.InitCode = removeWhitespace(d.InitCode, true) @@ -164,10 +185,10 @@ func (fc *funcContext) importDecls() (importedPaths []string, importDecls []*Dec func (fc *funcContext) newImportDecl(importedPkg *types.Package) *Decl { pkgVar := fc.importedPkgVar(importedPkg) d := &Decl{ - FullName: importDeclFullName(importedPkg), - 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) }), + FullName: importDeclFullName(importedPkg), + Vars: []string{pkgVar}, + ImportCode: []byte(fmt.Sprintf("\t%s = $packages[\"%s\"];\n", pkgVar, importedPkg.Path())), + InitCode: fc.CatchOutput(1, func() { fc.translateStmt(fc.importInitializer(importedPkg.Path()), nil) }), } d.Dce().SetAsAlive() return d @@ -293,17 +314,19 @@ func (fc *funcContext) funcDecls(functions []*ast.FuncDecl) ([]*Decl, error) { // package-level variable by which they all are referenced, // e.g. init functions and instances of generic functions. objName := fc.objectName(o) + generic := len(instances) > 1 || !instances[0].IsTrivial() + varName := objName + if generic { + varName += ` = []` + } varDecl := &Decl{ FullName: funcVarDeclFullName(o), - Vars: []string{objName}, + Vars: []string{varName}, } varDecl.Dce().SetName(o, nil, nil) - if len(instances) > 1 || !instances[0].IsTrivial() { - varDecl.DeclCode = fc.CatchOutput(0, func() { - fc.Printf("%s = {};", objName) - if o.Exported() { - fc.Printf("$pkg.%s = %s;", encodeIdent(fun.Name.Name), objName) - } + if generic && o.Exported() { + varDecl.ExportFuncCode = fc.CatchOutput(1, func() { + fc.Printf("$pkg.%s = %s;", encodeIdent(fun.Name.Name), objName) }) } funcDecls = append(funcDecls, varDecl) @@ -359,7 +382,7 @@ func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance) } fc.pkgCtx.CollectDCEDeps(d, func() { - d.DeclCode = fc.namedFuncContext(inst).translateTopLevelFunction(fun) + d.FuncDeclCode = fc.namedFuncContext(inst).translateTopLevelFunction(fun) }) return d } @@ -438,18 +461,18 @@ func (fc *funcContext) namedTypeDecls(typeNames typesutil.TypeNames) ([]*Decl, e // the type definition directly. func (fc *funcContext) newNamedTypeVarDecl(obj *types.TypeName) *Decl { name := fc.objectName(obj) + generic := fc.pkgCtx.instanceSet.Pkg(obj.Pkg()).ObjHasInstances(obj) + varName := name + if generic { + varName += ` = []` + } varDecl := &Decl{ FullName: typeVarDeclFullName(obj), - Vars: []string{name}, + Vars: []string{varName}, } varDecl.Dce().SetName(obj, nil, nil) - if fc.pkgCtx.instanceSet.Pkg(obj.Pkg()).ObjHasInstances(obj) { - varDecl.DeclCode = fc.CatchOutput(0, func() { - fc.Printf("%s = {};", name) - }) - } if isPkgLevel(obj) { - varDecl.TypeInitCode = fc.CatchOutput(0, func() { + varDecl.ExportTypeCode = fc.CatchOutput(0, func() { fc.Printf("$pkg.%s = %s;", encodeIdent(obj.Name()), name) }) } @@ -483,7 +506,7 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er d.Dce().SetName(inst.Object, inst.TNest, inst.TArgs) fc.pkgCtx.CollectDCEDeps(d, func() { // Code that declares a JS type (i.e. prototype) for each Go type. - d.DeclCode = fc.CatchOutput(0, func() { + d.TypeDeclCode = fc.CatchOutput(0, func() { size := int64(0) constructor := "null" @@ -505,7 +528,7 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er }) // Reflection metadata about methods the type has. - d.MethodListCode = fc.CatchOutput(0, func() { + d.MethodListCode = fc.CatchOutput(1, func() { if _, ok := underlying.(*types.Interface); ok { return } @@ -531,7 +554,7 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er // initialize themselves. switch t := underlying.(type) { case *types.Array, *types.Chan, *types.Interface, *types.Map, *types.Pointer, *types.Slice, *types.Signature, *types.Struct: - d.TypeInitCode = fc.CatchOutput(0, func() { + d.TypeInitCode = fc.CatchOutput(1, func() { fc.Printf("%s.init(%s);", fc.instName(inst), fc.initArgs(t)) }) } @@ -541,6 +564,10 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er // structConstructor returns JS constructor function for a struct type. func (fc *funcContext) structConstructor(t *types.Struct) string { + if t.NumFields() == 0 { + return `function() { this.$val = this; }` + } + constructor := &strings.Builder{} ctrArgs := make([]string, t.NumFields()) @@ -607,7 +634,7 @@ func (fc *funcContext) anonTypeDecls(anonTypes []*types.TypeName) []*Decl { } d.Dce().SetName(t, nil, nil) fc.pkgCtx.CollectDCEDeps(d, func() { - d.DeclCode = []byte(fmt.Sprintf("\t%s = $%sType(%s);\n", t.Name(), strings.ToLower(typeKind(t.Type())[5:]), fc.initArgs(t.Type()))) + d.AnonTypeDeclCode = []byte(fmt.Sprintf("\t\t%s = $%sType(%s);\n", t.Name(), strings.ToLower(typeKind(t.Type())[5:]), fc.initArgs(t.Type()))) }) decls = append(decls, d) } diff --git a/compiler/functions.go b/compiler/functions.go index 181af6bb8..ef5c57f7d 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -116,14 +116,14 @@ func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl) []byte { lvalue := fc.instName(fc.instance) if fun.Body == nil { - return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fc.unimplementedFunction(o))) + return []byte(fmt.Sprintf("\t\t%s = %s;\n", lvalue, fc.unimplementedFunction(o))) } body := fc.translateFunctionBody(fun.Type, nil, fun.Body) code := bytes.NewBuffer(nil) - fmt.Fprintf(code, "\t%s = %s;\n", lvalue, body) + fmt.Fprintf(code, "\t\t%s = %s;\n", lvalue, body) if fun.Name.IsExported() && fc.instance.IsTrivial() { - fmt.Fprintf(code, "\t$pkg.%s = %s;\n", encodeIdent(fun.Name.Name), lvalue) + fmt.Fprintf(code, "\t\t$pkg.%s = %s;\n", encodeIdent(fun.Name.Name), lvalue) } return code.Bytes() } @@ -140,7 +140,7 @@ func (fc *funcContext) translateMethod(fun *ast.FuncDecl) []byte { // 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))) + return []byte(fmt.Sprintf("\t\t%s = %s;\n", lvalue, fc.unimplementedFunction(o))) } var recv *ast.Ident @@ -148,7 +148,7 @@ func (fc *funcContext) translateMethod(fun *ast.FuncDecl) []byte { 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)) + return []byte(fmt.Sprintf("\t\t%s = %s;\n", lvalue, fun)) } recvInst := fc.instance.Recv() @@ -172,7 +172,7 @@ func (fc *funcContext) translateMethod(fun *ast.FuncDecl) []byte { // 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)) + return []byte(fmt.Sprintf("\t\t%s = %s;\n", lvalue, fun)) } // Structs are a special case: they are represented by JS objects and their @@ -232,7 +232,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, } } - bodyOutput := string(fc.CatchOutput(1, func() { + bodyOutput := string(fc.CatchOutput(2, func() { if fc.IsBlocking() { fc.pkgCtx.Scopes[body] = fc.pkgCtx.Scopes[typ] fc.handleEscapingVars(body) @@ -336,16 +336,16 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, } if prefix != "" { - bodyOutput = fc.Indentation(1) + "/* */" + prefix + "\n" + bodyOutput + bodyOutput = fc.Indentation(2) + "/* */" + prefix + "\n" + bodyOutput } if suffix != "" { - bodyOutput = bodyOutput + fc.Indentation(1) + "/* */" + suffix + "\n" + bodyOutput = bodyOutput + fc.Indentation(2) + "/* */" + suffix + "\n" } if localVarDefs != "" { - bodyOutput = fc.Indentation(1) + localVarDefs + bodyOutput + bodyOutput = fc.Indentation(2) + localVarDefs + bodyOutput } fc.pkgCtx.escapingVars = prevEV - return fmt.Sprintf("function %s(%s) {\n%s%s}", fc.funcRef, strings.Join(args, ", "), bodyOutput, fc.Indentation(0)) + return fmt.Sprintf("function %s(%s) {\n%s%s}", fc.funcRef, strings.Join(args, ", "), bodyOutput, fc.Indentation(1)) } diff --git a/compiler/internal/typeparams/collect_test.go b/compiler/internal/typeparams/collect_test.go index 26791ea59..e122673c5 100644 --- a/compiler/internal/typeparams/collect_test.go +++ b/compiler/internal/typeparams/collect_test.go @@ -7,9 +7,10 @@ import ( "strings" "testing" + "golang.org/x/tools/go/ast/astutil" + "github.com/google/go-cmp/cmp" "github.com/gopherjs/gopherjs/internal/srctesting" - "golang.org/x/tools/go/ast/astutil" ) func TestVisitor(t *testing.T) { @@ -596,7 +597,8 @@ func TestCollector_RecursiveTypeParams(t *testing.T) { { Object: srctesting.LookupObj(pkg, `F`), TArgs: []types.Type{xInt}, - }, { + }, + { Object: srctesting.LookupObj(pkg, `main.U`), TArgs: []types.Type{xInt}, }, @@ -655,6 +657,7 @@ func TestCollector_NestedRecursiveTypeParams(t *testing.T) { t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) } } + func TestCollector_LooselyRecursiveTypeParams(t *testing.T) { // This is based off of part of go1.19.13/test/typeparam/nested.go src := `package test diff --git a/compiler/prelude/prelude.js b/compiler/prelude/prelude.js index d35de6b01..1adcfd7e3 100644 --- a/compiler/prelude/prelude.js +++ b/compiler/prelude/prelude.js @@ -75,10 +75,10 @@ if (($global.process !== undefined) && $global.require) { } var $println = console.log -var $initAllLinknames = () => { +var $callForAllPackages = (methodName) => { var names = $keys($packages); for (var i = 0; i < names.length; i++) { - var f = $packages[names[i]]["$initLinknames"]; + var f = $packages[names[i]][methodName]; if (typeof f == 'function') { f(); } diff --git a/internal/govendor/subst/subst.go b/internal/govendor/subst/subst.go index 1ac705625..2fd29bb81 100644 --- a/internal/govendor/subst/subst.go +++ b/internal/govendor/subst/subst.go @@ -152,14 +152,14 @@ func (subst *subster) typ(t types.Type) (res types.Type) { return subst.interface_(t) // GOPHERJS: Removed following case since types.Alias is not supported until go1.22. - //case *types.Alias: + // case *types.Alias: // return subst.alias(t) case *types.Named: return subst.named(t) // GOPHERJS: Removed following case since the opaque type is specific to the SSA builder. - //case *opaqueType: + // case *opaqueType: // return t // opaque types are never substituted default: @@ -276,7 +276,7 @@ func (subst *subster) interface_(iface *types.Interface) *types.Interface { initMethods := func(n int) { // copy first n explicit methods methods = make([]*types.Func, iface.NumExplicitMethods()) // GOPHERJS: Replaced a range over count since that's not supported in go1.22 - //for i := range n { + // for i := range n { for i := 0; i < n; i++ { f := iface.ExplicitMethod(i) norecv := changeRecv(f.Type().(*types.Signature), nil) @@ -302,7 +302,7 @@ func (subst *subster) interface_(iface *types.Interface) *types.Interface { initEmbeds := func(n int) { // copy first n embedded types embeds = make([]types.Type, iface.NumEmbeddeds()) // GOPHERJS: Replaced a range over count since that's not supported in go1.22 - //for i := range n { + // for i := range n { for i := 0; i < n; i++ { embeds[i] = iface.EmbeddedType(i) } @@ -331,8 +331,7 @@ func (subst *subster) interface_(iface *types.Interface) *types.Interface { } // GOPHERJS: removed alias substitution since types.Alias is not supported until go1.22 -//func (subst *subster) alias(t *types.Alias) types.Type { ... } - +// func (subst *subster) alias(t *types.Alias) types.Type { ... } func (subst *subster) named(t *types.Named) types.Type { // A Named type is a user defined type. // Ignoring generics, Named types are canonical: they are identical if @@ -439,7 +438,7 @@ func (subst *subster) named(t *types.Named) types.Type { // Then when creating `subTArgs` (also below), it will return // `T'` via the cache instead of substituting `T` with the // correct type argument. - //subst.cache[cur] = ntp + // subst.cache[cur] = ntp newTParams = append(newTParams, ntp) } fresh.SetTypeParams(newTParams) @@ -580,7 +579,7 @@ func reaches(t types.Type, c map[types.Type]bool) (res bool) { } } // GOPHERJS: Removed types.Alias from following case since it's not supported until go1.22. - //case *types.Named, *types.Alias: + // case *types.Named, *types.Alias: case *types.Named: return reaches(t.Underlying(), c) default: diff --git a/tests/gencircle_test.go b/tests/gencircle_test.go new file mode 100644 index 000000000..cc326064e --- /dev/null +++ b/tests/gencircle_test.go @@ -0,0 +1,85 @@ +package tests_test + +import ( + "embed" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_GenCircle_Simple(t *testing.T) { runGenCircleTest(t, `simple`) } + +func Test_GenCircle_PingPong(t *testing.T) { runGenCircleTest(t, `pingpong`) } + +func Test_GenCircle_Burninate(t *testing.T) { runGenCircleTest(t, `burninate`) } + +func Test_GenCircle_CatBox(t *testing.T) { + // TODO(grantnelson-wf): This test hits an error similar to + // `panic: info did not have function declaration instance for + // "collections.Push[box.Unboxer[cat.Cat]]"` from `analysis/Info:IsBlocking`. + // + // This is because no instance of `Stack` is used explicitly in code, + // i.e. the code doesn't have an `ast.Ident` for `Stack` found in `types.Info.Instances` + // since the only `Stack` identifiers are the ones for the generic declaration. + // `Stack[box.Unboxer[cat.Cat]]` is implicitly defined via the call + // `collections.NewStack[box.Unboxer[cat.Cat]]()` in main.go. + // + // We need to update the `typeparams.collector` to add these implicit types + // to the `PackageInstanceSets` so that `analysis/info` has the implicit + // instances of `Stack`. + // + // Simply adding `_ = collections.Stack[box.Unboxer[cat.Cat]]{}` is a + // work around for `Stack` issue but the code gets tripped up on `boxImp[T]` + // via `Box[T]` not being defined since again `boxImp` has not been collected. + t.Skip(`Implicit Instance Not Yet Collected`) + runGenCircleTest(t, `catbox`) +} + +// Cache buster: Keeping the tests from using cached results when only +// the test application files are changed. +// +//go:embed testdata/gencircle +var _ embed.FS + +func runGenCircleTest(t *testing.T, testPkg string) { + t.Helper() + if runtime.GOOS == `js` { + t.Skip(`test meant to be run using normal Go compiler (needs os/exec)`) + } + + const ( + basePath = `testdata/gencircle` + mainFile = `main.go` + outFile = `main.out` + ) + + mainPath := filepath.Join(basePath, testPkg, mainFile) + gotBytes, err := exec.Command(`gopherjs`, `run`, mainPath).CombinedOutput() + got := normalizeOut(gotBytes) + if err != nil { + t.Fatalf("error from exec: %v:\n%s", err, got) + } + + outPath := filepath.Join(basePath, testPkg, outFile) + wantBytes, err := os.ReadFile(outPath) + if err != nil { + t.Fatalf(`error reading .out file: %v`, err) + } + want := normalizeOut(wantBytes) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Got diff (-want,+got):\n%s", diff) + } +} + +func normalizeOut(b []byte) string { + s := string(b) + s = strings.ReplaceAll(s, "\r\n", "\n") + s = strings.ReplaceAll(s, "\r", "\n") + return s +} diff --git a/tests/testdata/gencircle/burninate/burnable/burnable.go b/tests/testdata/gencircle/burninate/burnable/burnable.go new file mode 100644 index 000000000..3a6171134 --- /dev/null +++ b/tests/testdata/gencircle/burninate/burnable/burnable.go @@ -0,0 +1,13 @@ +package burnable + +import "github.com/gopherjs/gopherjs/tests/testdata/gencircle/burninate/dragon" + +type Cottages struct{} + +func (c Cottages) String() string { + return `thatched-roof cottages` +} + +func Burn[B dragon.Burnable](d dragon.Trogdor[B], b B) { + d.Burninate(b) +} diff --git a/tests/testdata/gencircle/burninate/dragon/dragon.go b/tests/testdata/gencircle/burninate/dragon/dragon.go new file mode 100644 index 000000000..ddabc6a77 --- /dev/null +++ b/tests/testdata/gencircle/burninate/dragon/dragon.go @@ -0,0 +1,9 @@ +package dragon + +type Burnable interface{ String() string } + +type Trogdor[T Burnable] struct{} + +func (t Trogdor[T]) Burninate(target T) { + println("burninating the " + target.String()) +} diff --git a/tests/testdata/gencircle/burninate/main.go b/tests/testdata/gencircle/burninate/main.go new file mode 100644 index 000000000..594a5a4e9 --- /dev/null +++ b/tests/testdata/gencircle/burninate/main.go @@ -0,0 +1,17 @@ +// Test of instances of generic types inverted dependencies. +// The `burnable` imports `dragons` but the instance of `Trogdor` requires +// `burnable`. This is a simple check that the all packages are loaded before +// the types finish setting up. This is similar to the "simple" gencircle test +// except with generic functions and methods. +package main + +import ( + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/burninate/burnable" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/burninate/dragon" +) + +func main() { + d := dragon.Trogdor[burnable.Cottages]{} + b := burnable.Cottages{} + burnable.Burn(d, b) +} diff --git a/tests/testdata/gencircle/burninate/main.out b/tests/testdata/gencircle/burninate/main.out new file mode 100644 index 000000000..a9bbff9e2 --- /dev/null +++ b/tests/testdata/gencircle/burninate/main.out @@ -0,0 +1 @@ +burninating the thatched-roof cottages diff --git a/tests/testdata/gencircle/catbox/box/box.go b/tests/testdata/gencircle/catbox/box/box.go new file mode 100644 index 000000000..06e909d85 --- /dev/null +++ b/tests/testdata/gencircle/catbox/box/box.go @@ -0,0 +1,17 @@ +package box + +type Unboxer[T any] interface { + Unbox() T +} + +type boxImp[T any] struct { + whatsInTheBox T +} + +func Box[T any](value T) Unboxer[T] { + return &boxImp[T]{whatsInTheBox: value} +} + +func (b *boxImp[T]) Unbox() T { + return b.whatsInTheBox +} diff --git a/tests/testdata/gencircle/catbox/cat/cat.go b/tests/testdata/gencircle/catbox/cat/cat.go new file mode 100644 index 000000000..f85f87751 --- /dev/null +++ b/tests/testdata/gencircle/catbox/cat/cat.go @@ -0,0 +1,5 @@ +package cat + +type Cat struct { + Name string +} diff --git a/tests/testdata/gencircle/catbox/collections/stack.go b/tests/testdata/gencircle/catbox/collections/stack.go new file mode 100644 index 000000000..9e22a24fb --- /dev/null +++ b/tests/testdata/gencircle/catbox/collections/stack.go @@ -0,0 +1,23 @@ +package collections + +type Stack[T any] struct{ values []T } + +func NewStack[T any]() *Stack[T] { + return &Stack[T]{} +} + +func (s *Stack[T]) Count() int { + return len(s.values) +} + +func (s *Stack[T]) Push(value T) { + s.values = append(s.values, value) +} + +func (s *Stack[T]) Pop() (value T) { + if len(s.values) > 0 { + maxIndex := len(s.values) - 1 + s.values, value = s.values[:maxIndex], s.values[maxIndex] + } + return +} diff --git a/tests/testdata/gencircle/catbox/main.go b/tests/testdata/gencircle/catbox/main.go new file mode 100644 index 000000000..97d4166ad --- /dev/null +++ b/tests/testdata/gencircle/catbox/main.go @@ -0,0 +1,19 @@ +// Test of instances of generic types causing dependencies. +// In this tests cat, box, and collections do not import each other directly +// but the main causes instances requiring `collections` to need `box` and +// `box` to need `cat` (thus `collections` indirectly needs `cat`). +// This test is also an attempt at a more realistic scenario. +package main + +import ( + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/catbox/box" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/catbox/cat" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/catbox/collections" +) + +func main() { + s := collections.NewStack[box.Unboxer[cat.Cat]]() + s.Push(box.Box(cat.Cat{Name: "Erwin"})) + s.Push(box.Box(cat.Cat{Name: "Dirac"})) + println(s.Pop().Unbox().Name) +} diff --git a/tests/testdata/gencircle/catbox/main.out b/tests/testdata/gencircle/catbox/main.out new file mode 100644 index 000000000..8415433d3 --- /dev/null +++ b/tests/testdata/gencircle/catbox/main.out @@ -0,0 +1 @@ +Dirac diff --git a/tests/testdata/gencircle/pingpong/cat/cat.go b/tests/testdata/gencircle/pingpong/cat/cat.go new file mode 100644 index 000000000..326b4a736 --- /dev/null +++ b/tests/testdata/gencircle/pingpong/cat/cat.go @@ -0,0 +1,16 @@ +package cat + +import "github.com/gopherjs/gopherjs/tests/testdata/gencircle/pingpong/collections" + +type Cat[H collections.Hasher] struct { + Name string +} + +func (c Cat[H]) Hash() uint { + var zero H + var h collections.Hasher = zero + for _, v := range c.Name { + h = h.Add(uint(v)) + } + return h.Sum() +} diff --git a/tests/testdata/gencircle/pingpong/collections/hashes.go b/tests/testdata/gencircle/pingpong/collections/hashes.go new file mode 100644 index 000000000..1b2c13ab6 --- /dev/null +++ b/tests/testdata/gencircle/pingpong/collections/hashes.go @@ -0,0 +1,21 @@ +package collections + +type Hasher interface { + Add(value uint) Hasher + Sum() uint +} + +type Hashable interface { + Hash() uint +} + +type BadHasher struct{ value uint } + +func (h BadHasher) Add(value uint) Hasher { + h.value += value + return h +} + +func (h BadHasher) Sum() uint { + return h.value +} diff --git a/tests/testdata/gencircle/pingpong/collections/hashmap.go b/tests/testdata/gencircle/pingpong/collections/hashmap.go new file mode 100644 index 000000000..0ab025415 --- /dev/null +++ b/tests/testdata/gencircle/pingpong/collections/hashmap.go @@ -0,0 +1,17 @@ +package collections + +// HashSet keeps a set of non-nil elements that have unique hashes. +type HashSet[E Hashable] struct { + data map[uint]E +} + +func (s *HashSet[E]) Add(e E) { + if s.data == nil { + s.data = map[uint]E{} + } + s.data[e.Hash()] = e +} + +func (s *HashSet[E]) Count() int { + return len(s.data) +} diff --git a/tests/testdata/gencircle/pingpong/main.go b/tests/testdata/gencircle/pingpong/main.go new file mode 100644 index 000000000..6c1bb24e1 --- /dev/null +++ b/tests/testdata/gencircle/pingpong/main.go @@ -0,0 +1,20 @@ +// Test of instances of generic types inverse dependencies. +// This is designed to test when types from package A is used around a type +// from package B, e.g. A.X[B.Y[A.Z]]. The type interfaces bounce back and +// forth between two packages. This means that A can not be simply +// run before B nor after A. The generics have to handle A needing B and +// B needing A to resolve a instances of generic types. +package main + +import ( + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/pingpong/cat" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/pingpong/collections" +) + +func main() { + s := collections.HashSet[cat.Cat[collections.BadHasher]]{} + s.Add(cat.Cat[collections.BadHasher]{Name: "Fluffy"}) + s.Add(cat.Cat[collections.BadHasher]{Name: "Mittens"}) + s.Add(cat.Cat[collections.BadHasher]{Name: "Whiskers"}) + println(s.Count(), "elements") +} diff --git a/tests/testdata/gencircle/pingpong/main.out b/tests/testdata/gencircle/pingpong/main.out new file mode 100644 index 000000000..deb6def3e --- /dev/null +++ b/tests/testdata/gencircle/pingpong/main.out @@ -0,0 +1 @@ +3 elements diff --git a/tests/testdata/gencircle/simple/bar/bar.go b/tests/testdata/gencircle/simple/bar/bar.go new file mode 100644 index 000000000..4ef580194 --- /dev/null +++ b/tests/testdata/gencircle/simple/bar/bar.go @@ -0,0 +1,5 @@ +package bar + +type Bar[G any] struct { + Next *G +} diff --git a/tests/testdata/gencircle/simple/foo/foo.go b/tests/testdata/gencircle/simple/foo/foo.go new file mode 100644 index 000000000..3bb5b2b3c --- /dev/null +++ b/tests/testdata/gencircle/simple/foo/foo.go @@ -0,0 +1,8 @@ +package foo + +import "github.com/gopherjs/gopherjs/tests/testdata/gencircle/simple/bar" + +type Entity struct { + Ref bar.Bar[Entity] + Name string +} diff --git a/tests/testdata/gencircle/simple/main.go b/tests/testdata/gencircle/simple/main.go new file mode 100644 index 000000000..bb180e195 --- /dev/null +++ b/tests/testdata/gencircle/simple/main.go @@ -0,0 +1,21 @@ +// Test of instances of generic types causing inverted dependencies. +// `bar.Bar[Entity]` requires `foo.Entity` but is imported in `foo`, meaning +// that `bar` is added to the package list prior `foo`. The setup of types +// must allow for `foo` to be added before `bar` types to lookup `foo`. +package main + +import ( + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/simple/bar" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/simple/foo" +) + +func main() { + e := foo.Entity{ + Ref: bar.Bar[foo.Entity]{ + Next: &foo.Entity{ + Name: `I am Next`, + }, + }, + } + println(e.Ref.Next.Name) +} diff --git a/tests/testdata/gencircle/simple/main.out b/tests/testdata/gencircle/simple/main.out new file mode 100644 index 000000000..0cd6369bf --- /dev/null +++ b/tests/testdata/gencircle/simple/main.out @@ -0,0 +1 @@ +I am Next From e7ccb4d371b6424fb465de148dab0d717535082d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 16:23:51 +0000 Subject: [PATCH 51/58] Bump brace-expansion from 1.1.11 to 1.1.12 in /node-syscall Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12. - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12) --- updated-dependencies: - dependency-name: brace-expansion dependency-version: 1.1.12 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- node-syscall/package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/node-syscall/package-lock.json b/node-syscall/package-lock.json index df06938c6..fc07161a1 100644 --- a/node-syscall/package-lock.json +++ b/node-syscall/package-lock.json @@ -117,9 +117,10 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" From 96a2970a9fb91a441579517caa0f4572e382abf2 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Wed, 30 Jul 2025 14:37:27 -0600 Subject: [PATCH 52/58] Fixing collecting instances with inverted dependencies --- compiler/decls.go | 14 +- compiler/internal/analysis/info_test.go | 10 +- compiler/internal/typeparams/collect.go | 56 +++++-- compiler/internal/typeparams/collect_test.go | 157 +++++++++++++++++-- compiler/internal/typeparams/instance.go | 15 +- compiler/package.go | 7 +- compiler/prelude/jsmapping.js | 15 +- compiler/sources/sources.go | 16 +- tests/gencircle_test.go | 22 +-- 9 files changed, 227 insertions(+), 85 deletions(-) diff --git a/compiler/decls.go b/compiler/decls.go index 63c3d5059..0979a77ef 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -51,23 +51,23 @@ type Decl struct { TypeDeclCode []byte // JavaScript code that assigns exposed named types to the package. ExportTypeCode []byte - // JavaScript code that declares basic information about an anonymous types. + // JavaScript code that declares basic information about an anonymous type. // It configures basic information about the type. - // This is added to the finish setup phase to has access to all packages. + // This is added to the finish setup phase to have access to all packages. AnonTypeDeclCode []byte // JavaScript code that declares basic information about a function or // method symbol. This contains the function's or method's compiled body. - // This is added to the finish setup phase to has access to all packages. + // This is added to the finish setup phase to have access to all packages. FuncDeclCode []byte // JavaScript code that assigns exposed functions to the package. - // This is added to the finish setup phase to has access to all packages. + // This is added to the finish setup phase to have access to all packages. ExportFuncCode []byte - // JavaScript code that initializes reflection metadata about type's method list. - // This is added to the finish setup phase to has access to all packages. + // JavaScript code that initializes reflection metadata about a type's method list. + // This is added to the finish setup phase to have access to all packages. MethodListCode []byte // JavaScript code that initializes the rest of reflection metadata about a type // (e.g. struct fields, array type sizes, element types, etc.). - // This is added to the finish setup phase to has access to all packages. + // This is added to the finish setup phase to have access to all packages. 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). diff --git a/compiler/internal/analysis/info_test.go b/compiler/internal/analysis/info_test.go index 0df26b0b9..4c18d05d4 100644 --- a/compiler/internal/analysis/info_test.go +++ b/compiler/internal/analysis/info_test.go @@ -1632,13 +1632,13 @@ func newBlockingTest(t *testing.T, src string) *blockingTest { tContext := types.NewContext() tc := typeparams.Collector{ TContext: tContext, - Info: f.Info, Instances: &typeparams.PackageInstanceSets{}, } file := f.Parse(`test.go`, src) testInfo, testPkg := f.Check(`pkg/test`, file) - tc.Scan(testPkg, file) + tc.Scan(testInfo, testPkg, file) + tc.Finish() getImportInfo := func(path string) (*Info, error) { return nil, fmt.Errorf(`getImportInfo should not be called in this test, called with %v`, path) @@ -1658,7 +1658,6 @@ func newBlockingTestWithOtherPackage(t *testing.T, testSrc string, otherSrc stri tContext := types.NewContext() tc := typeparams.Collector{ TContext: tContext, - Info: f.Info, Instances: &typeparams.PackageInstanceSets{}, } @@ -1672,11 +1671,12 @@ func newBlockingTestWithOtherPackage(t *testing.T, testSrc string, otherSrc stri otherFile := f.Parse(`other.go`, otherSrc) _, otherPkg := f.Check(`pkg/other`, otherFile) - tc.Scan(otherPkg, otherFile) + tc.Scan(f.Info, otherPkg, otherFile) testFile := f.Parse(`test.go`, testSrc) _, testPkg := f.Check(`pkg/test`, testFile) - tc.Scan(testPkg, testFile) + tc.Scan(f.Info, testPkg, testFile) + tc.Finish() otherPkgInfo := AnalyzePkg([]*ast.File{otherFile}, f.FileSet, f.Info, tContext, otherPkg, tc.Instances, getImportInfo) pkgInfo[otherPkg.Path()] = otherPkgInfo diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index f18e6eb20..b42c1031d 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -188,16 +188,26 @@ func (c *seedVisitor) Visit(n ast.Node) ast.Visitor { // set whenever their receiver type instance is encountered. type Collector struct { TContext *types.Context - Info *types.Info Instances *PackageInstanceSets + + objMap map[types.Object]ast.Node + infoMap map[string]*types.Info } // Scan package files for generic instances. -func (c *Collector) Scan(pkg *types.Package, files ...*ast.File) { - if c.Info.Instances == nil || c.Info.Defs == nil { +func (c *Collector) Scan(info *types.Info, pkg *types.Package, files ...*ast.File) { + if c.objMap == nil { + c.objMap = map[types.Object]ast.Node{} + } + if c.infoMap == nil { + c.infoMap = map[string]*types.Info{} + } + + // Check the info for this package then record it for later use. + if info.Instances == nil || info.Defs == nil { panic(fmt.Errorf("types.Info must have Instances and Defs populated")) } - objMap := map[types.Object]ast.Node{} + c.infoMap[pkg.Path()] = info // Collect instances of generic objects in non-generic code in the package and // add then to the existing InstanceSet. @@ -205,42 +215,58 @@ func (c *Collector) Scan(pkg *types.Package, files ...*ast.File) { visitor: visitor{ instances: c.Instances, resolver: nil, - info: c.Info, + info: info, }, - objMap: objMap, + objMap: c.objMap, } for _, file := range files { ast.Walk(&sc, file) } +} + +// Finish will finish the collecting instances by propagating instances of +// generic types and functions found in generic code. The generic code is +// rescanned with in an instances context to find internally defined instances. +// +// This should only be called after all the files are scanned. +func (c *Collector) Finish() { + for !c.Instances.allExhausted() { + for pkgPath, instances := range *c.Instances { + c.propagate(pkgPath, instances) + } + } +} - for iset := c.Instances.Pkg(pkg); !iset.exhausted(); { +func (c *Collector) propagate(pkgPath string, instances *InstanceSet) { + info := c.infoMap[pkgPath] + for iset := instances; !iset.exhausted(); { inst, _ := iset.next() switch typ := inst.Object.Type().(type) { case *types.Signature: - c.scanSignature(inst, typ, objMap) + c.scanSignature(inst, typ, info) case *types.Named: - c.scanNamed(inst, typ, objMap) + c.scanNamed(inst, typ, info) } } } -func (c *Collector) scanSignature(inst Instance, typ *types.Signature, objMap map[types.Object]ast.Node) { +func (c *Collector) scanSignature(inst Instance, typ *types.Signature, info *types.Info) { v := visitor{ instances: c.Instances, resolver: NewResolver(c.TContext, inst), - info: c.Info, + info: info, nestTParams: SignatureTypeParams(typ), nestTArgs: inst.TArgs, } - ast.Walk(&v, objMap[inst.Object]) + ast.Walk(&v, c.objMap[inst.Object]) } -func (c *Collector) scanNamed(inst Instance, typ *types.Named, objMap map[types.Object]ast.Node) { +func (c *Collector) scanNamed(inst Instance, typ *types.Named, info *types.Info) { obj := typ.Obj() - node := objMap[obj] + node := c.objMap[obj] if node == nil { // Types without an entry in objMap are concrete types // that are defined in a generic context. Skip them. @@ -256,7 +282,7 @@ func (c *Collector) scanNamed(inst Instance, typ *types.Named, objMap map[types. v := visitor{ instances: c.Instances, resolver: NewResolver(c.TContext, inst), - info: c.Info, + info: info, nestTParams: nestTParams, nestTArgs: inst.TNest, diff --git a/compiler/internal/typeparams/collect_test.go b/compiler/internal/typeparams/collect_test.go index e122673c5..1021e9dad 100644 --- a/compiler/internal/typeparams/collect_test.go +++ b/compiler/internal/typeparams/collect_test.go @@ -412,10 +412,10 @@ func TestCollector(t *testing.T) { c := Collector{ TContext: types.NewContext(), - Info: info, Instances: &PackageInstanceSets{}, } - c.Scan(pkg, file) + c.Scan(info, pkg, file) + c.Finish() inst := func(name, tNest, tArg string) Instance { return Instance{ @@ -470,10 +470,10 @@ func TestCollector_MoreNesting(t *testing.T) { c := Collector{ TContext: types.NewContext(), - Info: info, Instances: &PackageInstanceSets{}, } - c.Scan(pkg, file) + c.Scan(info, pkg, file) + c.Finish() inst := func(name, tNest, tArg string) Instance { return Instance{ @@ -545,10 +545,10 @@ func TestCollector_NestingWithVars(t *testing.T) { c := Collector{ TContext: types.NewContext(), - Info: info, Instances: &PackageInstanceSets{}, } - c.Scan(pkg, file) + c.Scan(info, pkg, file) + c.Finish() want := []Instance{ inst(`S`, `int`), @@ -585,10 +585,10 @@ func TestCollector_RecursiveTypeParams(t *testing.T) { c := Collector{ TContext: tc, - Info: info, Instances: &PackageInstanceSets{}, } - c.Scan(pkg, file) + c.Scan(info, pkg, file) + c.Finish() xInst := inst(`main.X`, ``, `int`) xInt := xInst.Resolve(tc) @@ -636,10 +636,10 @@ func TestCollector_NestedRecursiveTypeParams(t *testing.T) { c := Collector{ TContext: tc, - Info: info, Instances: &PackageInstanceSets{}, } - c.Scan(pkg, file) + c.Scan(info, pkg, file) + c.Finish() xInst := inst(`F.X`, `string`, `int`) xInt := xInst.Resolve(tc) @@ -675,10 +675,10 @@ func TestCollector_LooselyRecursiveTypeParams(t *testing.T) { c := Collector{ TContext: tc, - Info: info, Instances: &PackageInstanceSets{}, } - c.Scan(pkg, file) + c.Scan(info, pkg, file) + c.Finish() xInst := Instance{ Object: srctesting.LookupObj(pkg, `main.X`), @@ -725,10 +725,10 @@ func TestCollector_NestedTypeParams(t *testing.T) { c := Collector{ TContext: tc, - Info: info, Instances: &PackageInstanceSets{}, } - c.Scan(pkg, file) + c.Scan(info, pkg, file) + c.Finish() uInst := inst(`F.U`, `int`, `bool`) uIntBool := uInst.Resolve(tc) @@ -747,6 +747,129 @@ func TestCollector_NestedTypeParams(t *testing.T) { } } +func TestCollector_ImplicitTypeInstance(t *testing.T) { + f := srctesting.New(t) + tc := types.NewContext() + inst := func(pkg *types.Package, name, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } + + fooSrc := `package foo + type Foo[T any] struct{} + func NewFoo[T any]() *Foo[T] { + return &Foo[T]{} + }` + fooFile := f.Parse(`foo/foo.go`, fooSrc) + _, fooPkg := f.Check(`foo`, fooFile) + + mainSrc := `package test + import "foo" + func main() { + print(foo.NewFoo[int]()) + }` + mainFile := f.Parse(`main.go`, mainSrc) + _, mainPkg := f.Check(`test`, mainFile) + + c := Collector{ + TContext: tc, + Instances: &PackageInstanceSets{}, + } + // The issue which caused this test to be written only occurred when + // fooFile was scanned before mainFile, otherwise it would work fine. + // The problem was that `foo.Foo[int]` was not being collected in this + // order because mainFile adds an instance into the instance set for "foo" + // after "foo" was already propagated. This was fixed by performing + // propagation after all the packages are scanned via `Finish`. + c.Scan(f.Info, fooPkg, fooFile) + c.Scan(f.Info, mainPkg, mainFile) + c.Finish() + + want := []Instance{ + inst(fooPkg, `NewFoo`, `int`), + inst(fooPkg, `Foo`, `int`), + } + got := c.Instances.Pkg(fooPkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector_MoreImplicitTypeInstance(t *testing.T) { + f := srctesting.New(t) + tc := types.NewContext() + inst := func(pkg *types.Package, name, tArg string) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, name), + TArgs: evalTypeArgs(t, f.FileSet, pkg, tArg), + } + } + + fooSrc := `package foo + type Foo[T, U any] struct{} + func NewFoo[T, U any]() *Foo[T, U] { + return &Foo[T, U]{} + }` + fooFile := f.Parse(`foo/foo.go`, fooSrc) + _, fooPkg := f.Check(`foo`, fooFile) + + barSrc := `package bar + import "foo" + func Bar[T, U any](f *foo.Foo[T, U]) *foo.Foo[U, T] { + return foo.NewFoo[U, T]() + }` + barFile := f.Parse(`bar/bar.go`, barSrc) + _, barPkg := f.Check(`bar`, barFile) + + mainSrc := `package test + import "foo" + import "bar" + func main() { + f := foo.NewFoo[int, string]() + print(bar.Bar[int, string](f)) + }` + mainFile := f.Parse(`main.go`, mainSrc) + _, mainPkg := f.Check(`test`, mainFile) + + want := []Instance{ + inst(fooPkg, `NewFoo`, `int, string`), + inst(fooPkg, `NewFoo`, `string, int`), + inst(fooPkg, `Foo`, `int, string`), + inst(fooPkg, `Foo`, `string, int`), + } + trial := func(order ...int) { + c := Collector{ + TContext: tc, + Instances: &PackageInstanceSets{}, + } + for _, i := range order { + switch i { + case 1: + c.Scan(f.Info, fooPkg, fooFile) + case 2: + c.Scan(f.Info, barPkg, barFile) + case 3: + c.Scan(f.Info, mainPkg, mainFile) + } + } + c.Finish() + + got := c.Instances.Pkg(fooPkg).Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != `` { + t.Errorf("Instances from Collector trial %v contain diff (-want,+got):\n%s", order, diff) + } + } + + trial(1, 2, 3) + trial(1, 3, 2) + trial(2, 1, 3) + trial(2, 3, 1) + trial(3, 1, 2) + trial(3, 2, 1) +} + func evalTypeArgs(t *testing.T, fSet *token.FileSet, pkg *types.Package, expr string) []types.Type { if len(expr) == 0 { return nil @@ -794,11 +917,11 @@ func TestCollector_CrossPackage(t *testing.T) { c := Collector{ TContext: types.NewContext(), - Info: f.Info, Instances: &PackageInstanceSets{}, } - c.Scan(barPkg, barFile) - c.Scan(fooPkg, fooFile) + c.Scan(f.Info, barPkg, barFile) + c.Scan(f.Info, fooPkg, fooFile) + c.Finish() inst := func(pkg *types.Package, name string, tArg types.BasicKind) Instance { return Instance{ diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index 10e0df69f..438239707 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -202,7 +202,9 @@ func (iset *InstanceSet) next() (Instance, bool) { } // exhausted returns true if there are no unprocessed instances in the set. -func (iset *InstanceSet) exhausted() bool { return len(iset.values) <= iset.unprocessed } +func (iset *InstanceSet) exhausted() bool { + return len(iset.values) <= iset.unprocessed +} // Values returns instances that are currently in the set. Order is not specified. func (iset *InstanceSet) Values() []Instance { @@ -245,6 +247,17 @@ func (iset *InstanceSet) ObjHasInstances(obj types.Object) bool { // by import path. type PackageInstanceSets map[string]*InstanceSet +// allExhausted returns true if there are no unprocessed instances in +// any of the instance sets. +func (i PackageInstanceSets) allExhausted() bool { + for _, iset := range i { + if !iset.exhausted() { + return false + } + } + return true +} + // Pkg returns InstanceSet for objects defined in the given package. func (i PackageInstanceSets) Pkg(pkg *types.Package) *InstanceSet { path := pkg.Path() diff --git a/compiler/package.go b/compiler/package.go index bb94962da..2dcf19d60 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -263,9 +263,14 @@ func PrepareAllSources(allSources []*sources.Sources, importer sources.Importer, // Collect all the generic type instances from all the packages. // This must be done for all sources prior to any analysis. instances := &typeparams.PackageInstanceSets{} + tc := &typeparams.Collector{ + TContext: tContext, + Instances: instances, + } for _, srcs := range allSources { - srcs.CollectInstances(tContext, instances) + srcs.CollectInstances(tc) } + tc.Finish() // Analyze the package to determine type parameters instances, blocking, // and other type information. This will not populate the information. diff --git a/compiler/prelude/jsmapping.js b/compiler/prelude/jsmapping.js index f5317d626..db2c4bc64 100644 --- a/compiler/prelude/jsmapping.js +++ b/compiler/prelude/jsmapping.js @@ -392,16 +392,13 @@ var $internalize = (v, t, recv, seen, makeWrapper) => { } var n = new t.ptr(); for (var i = 0; i < t.fields.length; i++) { - var f = t.fields[i]; - - if (!f.exported) { - continue; - } - var jsProp = v[f.name]; - - n[f.prop] = $internalize(jsProp, f.typ, recv, seen, makeWrapper); + var f = t.fields[i]; + if (!f.exported) { + continue; + } + var jsProp = v[f.name]; + n[f.prop] = $internalize(jsProp, f.typ, recv, seen, makeWrapper); } - return n; } $throwRuntimeError("cannot internalize " + t.string); diff --git a/compiler/sources/sources.go b/compiler/sources/sources.go index 8e2d12946..1255c1926 100644 --- a/compiler/sources/sources.go +++ b/compiler/sources/sources.go @@ -44,11 +44,11 @@ type Sources struct { // JSFiles is the JavaScript files that are part of the package. JSFiles []jsFile.JSFile - // TypeInfo is the type information this package. + // TypeInfo is the type information for this package. // This is nil until set by Analyze. TypeInfo *analysis.Info - // baseInfo is the base type information this package. + // baseInfo is the base type information for this package. // This is nil until set by TypeCheck. baseInfo *types.Info @@ -163,13 +163,11 @@ func (s *Sources) TypeCheck(importer Importer, sizes types.Sizes, tContext *type // // This must be called before Analyze to have the type parameters instances // needed during analysis. -func (s *Sources) CollectInstances(tContext *types.Context, instances *typeparams.PackageInstanceSets) { - tc := typeparams.Collector{ - TContext: tContext, - Info: s.baseInfo, - Instances: instances, - } - tc.Scan(s.Package, s.Files...) +// +// Note that once all the sources are collected, the collector needs to be +// finished to ensure all the instances are collected. +func (s *Sources) CollectInstances(tc *typeparams.Collector) { + tc.Scan(s.baseInfo, s.Package, s.Files...) } // Analyze will determine the type parameters instances, blocking, diff --git a/tests/gencircle_test.go b/tests/gencircle_test.go index cc326064e..0915e4c18 100644 --- a/tests/gencircle_test.go +++ b/tests/gencircle_test.go @@ -18,27 +18,7 @@ func Test_GenCircle_PingPong(t *testing.T) { runGenCircleTest(t, `pingpong`) } func Test_GenCircle_Burninate(t *testing.T) { runGenCircleTest(t, `burninate`) } -func Test_GenCircle_CatBox(t *testing.T) { - // TODO(grantnelson-wf): This test hits an error similar to - // `panic: info did not have function declaration instance for - // "collections.Push[box.Unboxer[cat.Cat]]"` from `analysis/Info:IsBlocking`. - // - // This is because no instance of `Stack` is used explicitly in code, - // i.e. the code doesn't have an `ast.Ident` for `Stack` found in `types.Info.Instances` - // since the only `Stack` identifiers are the ones for the generic declaration. - // `Stack[box.Unboxer[cat.Cat]]` is implicitly defined via the call - // `collections.NewStack[box.Unboxer[cat.Cat]]()` in main.go. - // - // We need to update the `typeparams.collector` to add these implicit types - // to the `PackageInstanceSets` so that `analysis/info` has the implicit - // instances of `Stack`. - // - // Simply adding `_ = collections.Stack[box.Unboxer[cat.Cat]]{}` is a - // work around for `Stack` issue but the code gets tripped up on `boxImp[T]` - // via `Box[T]` not being defined since again `boxImp` has not been collected. - t.Skip(`Implicit Instance Not Yet Collected`) - runGenCircleTest(t, `catbox`) -} +func Test_GenCircle_CatBox(t *testing.T) { runGenCircleTest(t, `catbox`) } // Cache buster: Keeping the tests from using cached results when only // the test application files are changed. From 784cea3e04733ca5109166f8dee92308fb4d25ff Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Wed, 13 Aug 2025 11:39:27 -0600 Subject: [PATCH 53/58] Removing old style +build constraints --- .github/workflows/ci.yaml | 5 +++-- compiler/natives/src/bufio/bufio_test.go | 1 - compiler/natives/src/bytes/bytes.go | 1 - compiler/natives/src/bytes/bytes_test.go | 1 - compiler/natives/src/compress/gzip/example_test.go | 1 - compiler/natives/src/crypto/elliptic/nistec.go | 1 - compiler/natives/src/crypto/internal/boring/bbig/big.go | 1 - compiler/natives/src/crypto/internal/boring/bcache/cache.go | 1 - .../natives/src/crypto/internal/boring/bcache/cache_test.go | 1 - compiler/natives/src/crypto/internal/boring/sig/sig.go | 1 - compiler/natives/src/crypto/internal/nistec/nistec_test.go | 1 - compiler/natives/src/crypto/internal/nistec/wrapper.go | 1 - compiler/natives/src/crypto/internal/subtle/aliasing.go | 1 - compiler/natives/src/crypto/rand/rand.go | 1 - compiler/natives/src/database/sql/driver/driver_test.go | 1 - compiler/natives/src/debug/pe/symbol.go | 1 - compiler/natives/src/embed/embed.go | 1 - compiler/natives/src/encoding/gob/gob_test.go | 1 - compiler/natives/src/encoding/json/stream_test.go | 1 - compiler/natives/src/fmt/fmt_test.go | 1 - compiler/natives/src/go/token/position.go | 1 - compiler/natives/src/go/token/token_test.go | 1 - .../natives/src/golang.org/x/crypto/internal/alias/alias.go | 1 - .../src/golang.org/x/crypto/internal/subtle/aliasing.go | 1 - compiler/natives/src/hash/maphash/maphash.go | 1 - compiler/natives/src/internal/bytealg/bytealg.go | 1 - compiler/natives/src/internal/cpu/cpu.go | 1 - compiler/natives/src/internal/fmtsort/fmtsort_test.go | 1 - compiler/natives/src/internal/poll/semaphore.go | 1 - compiler/natives/src/internal/reflectlite/all_test.go | 1 - compiler/natives/src/internal/reflectlite/export_test.go | 1 - .../natives/src/internal/reflectlite/reflect_mirror_test.go | 1 - compiler/natives/src/internal/reflectlite/reflectlite.go | 1 - compiler/natives/src/internal/reflectlite/swapper.go | 1 - compiler/natives/src/internal/reflectlite/type.go | 1 - compiler/natives/src/internal/reflectlite/utils.go | 1 - compiler/natives/src/internal/reflectlite/value.go | 1 - .../natives/src/internal/unsafeheader/unsafeheader_test.go | 1 - compiler/natives/src/io/io_test.go | 1 - compiler/natives/src/math/big/big.go | 1 - compiler/natives/src/math/big/big_test.go | 1 - compiler/natives/src/math/bits/bits.go | 1 - compiler/natives/src/math/math.go | 1 - compiler/natives/src/math/math_test.go | 1 - compiler/natives/src/math/rand/rand_test.go | 1 - compiler/natives/src/net/fastrand.go | 1 - compiler/natives/src/net/http/clientserver_test.go | 1 - compiler/natives/src/net/http/cookiejar/example_test.go | 1 - compiler/natives/src/net/http/http.go | 1 - compiler/natives/src/net/http/http_wasm_test.go | 1 - compiler/natives/src/net/http/main_test.go | 1 - compiler/natives/src/net/http/server_test.go | 1 - compiler/natives/src/net/http/transport_test.go | 1 - compiler/natives/src/net/netip/export_test.go | 1 - compiler/natives/src/net/netip/fuzz_test.go | 1 - compiler/natives/src/net/netip/netip.go | 1 - compiler/natives/src/net/netip/netip_test.go | 1 - compiler/natives/src/os/file.go | 1 - compiler/natives/src/os/os.go | 1 - compiler/natives/src/os/signal/signal.go | 1 - compiler/natives/src/reflect/example_test.go | 1 - compiler/natives/src/reflect/reflect.go | 1 - compiler/natives/src/reflect/reflect_test.go | 1 - compiler/natives/src/reflect/swapper.go | 1 - compiler/natives/src/regexp/regexp_test.go | 1 - compiler/natives/src/runtime/debug/debug.go | 1 - compiler/natives/src/runtime/fastrand.go | 1 - compiler/natives/src/runtime/pprof/pprof.go | 1 - compiler/natives/src/runtime/runtime.go | 1 - compiler/natives/src/strconv/atoi.go | 1 - compiler/natives/src/strconv/itoa.go | 1 - compiler/natives/src/strings/strings.go | 1 - compiler/natives/src/strings/strings_test.go | 1 - compiler/natives/src/sync/atomic/atomic.go | 1 - compiler/natives/src/sync/atomic/atomic_test.go | 1 - compiler/natives/src/sync/cond.go | 1 - compiler/natives/src/sync/cond_test.go | 1 - compiler/natives/src/sync/map_test.go | 1 - compiler/natives/src/sync/pool.go | 1 - compiler/natives/src/sync/pool_test.go | 1 - compiler/natives/src/sync/sync.go | 1 - compiler/natives/src/sync/waitgroup.go | 1 - compiler/natives/src/syscall/js/export_test.go | 1 - compiler/natives/src/syscall/js/js.go | 1 - compiler/natives/src/syscall/js/js_test.go | 1 - compiler/natives/src/testing/allocs_test.go | 1 - compiler/natives/src/testing/example.go | 1 - compiler/natives/src/testing/helper_test.go | 1 - compiler/natives/src/testing/helperfuncs_test.go | 1 - compiler/natives/src/testing/sub_test.go | 1 - compiler/natives/src/text/template/template.go | 1 - compiler/natives/src/time/time.go | 1 - compiler/natives/src/time/time_test.go | 1 - compiler/natives/src/time/zoneinfo_js.go | 1 - compiler/natives/src/unicode/unicode.go | 1 - .../vendor/golang.org/x/crypto/internal/subtle/aliasing.go | 1 - internal/sysutil/sysutil.go | 1 - js/js_test.go | 1 - tests/gorepo/run.go | 1 - tests/js_test.go | 1 - tests/map_js_test.go | 1 - tests/syscall_test.go | 1 - tests/testdata/legacy_syscall/main.go | 1 - 103 files changed, 3 insertions(+), 104 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 01e32d5c8..27e65c4da 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -115,8 +115,9 @@ jobs: run: go mod tidy && git diff --exit-code - name: Check natives build tags working-directory: ${{ env.GOPHERJS_PATH }} - # All those packages should have // +build js. - run: diff -u <(echo -n) <(go list ./compiler/natives/src/...) + run: | + echo "Any following packages have at least one file that is missing //go:build js" + diff -u <(echo -n) <(go list ./compiler/natives/src/...) go_tests: name: Go Tests diff --git a/compiler/natives/src/bufio/bufio_test.go b/compiler/natives/src/bufio/bufio_test.go index b97fe22f7..f0ddf0534 100644 --- a/compiler/natives/src/bufio/bufio_test.go +++ b/compiler/natives/src/bufio/bufio_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bufio_test diff --git a/compiler/natives/src/bytes/bytes.go b/compiler/natives/src/bytes/bytes.go index 1f74edc4f..15c014301 100644 --- a/compiler/natives/src/bytes/bytes.go +++ b/compiler/natives/src/bytes/bytes.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bytes diff --git a/compiler/natives/src/bytes/bytes_test.go b/compiler/natives/src/bytes/bytes_test.go index e9d0e1690..02aac5ea9 100644 --- a/compiler/natives/src/bytes/bytes_test.go +++ b/compiler/natives/src/bytes/bytes_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bytes_test diff --git a/compiler/natives/src/compress/gzip/example_test.go b/compiler/natives/src/compress/gzip/example_test.go index b3f6fbe43..6b8f703bd 100644 --- a/compiler/natives/src/compress/gzip/example_test.go +++ b/compiler/natives/src/compress/gzip/example_test.go @@ -1,5 +1,4 @@ //go:build js && wasm -// +build js,wasm package gzip_test diff --git a/compiler/natives/src/crypto/elliptic/nistec.go b/compiler/natives/src/crypto/elliptic/nistec.go index 326c602d5..b2cc45e9d 100644 --- a/compiler/natives/src/crypto/elliptic/nistec.go +++ b/compiler/natives/src/crypto/elliptic/nistec.go @@ -1,5 +1,4 @@ //go:build js -// +build js package elliptic diff --git a/compiler/natives/src/crypto/internal/boring/bbig/big.go b/compiler/natives/src/crypto/internal/boring/bbig/big.go index 3a726ba3c..170a6f983 100644 --- a/compiler/natives/src/crypto/internal/boring/bbig/big.go +++ b/compiler/natives/src/crypto/internal/boring/bbig/big.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bbig diff --git a/compiler/natives/src/crypto/internal/boring/bcache/cache.go b/compiler/natives/src/crypto/internal/boring/bcache/cache.go index afff404ce..b33233211 100644 --- a/compiler/natives/src/crypto/internal/boring/bcache/cache.go +++ b/compiler/natives/src/crypto/internal/boring/bcache/cache.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bcache diff --git a/compiler/natives/src/crypto/internal/boring/bcache/cache_test.go b/compiler/natives/src/crypto/internal/boring/bcache/cache_test.go index 12f2c4da4..9f50eb0ff 100644 --- a/compiler/natives/src/crypto/internal/boring/bcache/cache_test.go +++ b/compiler/natives/src/crypto/internal/boring/bcache/cache_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bcache diff --git a/compiler/natives/src/crypto/internal/boring/sig/sig.go b/compiler/natives/src/crypto/internal/boring/sig/sig.go index 3eb2454aa..dab01104e 100644 --- a/compiler/natives/src/crypto/internal/boring/sig/sig.go +++ b/compiler/natives/src/crypto/internal/boring/sig/sig.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sig diff --git a/compiler/natives/src/crypto/internal/nistec/nistec_test.go b/compiler/natives/src/crypto/internal/nistec/nistec_test.go index d755e7ec3..4da8ba9e3 100644 --- a/compiler/natives/src/crypto/internal/nistec/nistec_test.go +++ b/compiler/natives/src/crypto/internal/nistec/nistec_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package nistec_test diff --git a/compiler/natives/src/crypto/internal/nistec/wrapper.go b/compiler/natives/src/crypto/internal/nistec/wrapper.go index 0d6706b52..a55aa4925 100644 --- a/compiler/natives/src/crypto/internal/nistec/wrapper.go +++ b/compiler/natives/src/crypto/internal/nistec/wrapper.go @@ -1,5 +1,4 @@ //go:build js -// +build js package nistec diff --git a/compiler/natives/src/crypto/internal/subtle/aliasing.go b/compiler/natives/src/crypto/internal/subtle/aliasing.go index 145687d59..23c9843c8 100644 --- a/compiler/natives/src/crypto/internal/subtle/aliasing.go +++ b/compiler/natives/src/crypto/internal/subtle/aliasing.go @@ -1,5 +1,4 @@ //go:build js -// +build js package subtle diff --git a/compiler/natives/src/crypto/rand/rand.go b/compiler/natives/src/crypto/rand/rand.go index 1c3631a02..4e4aab565 100644 --- a/compiler/natives/src/crypto/rand/rand.go +++ b/compiler/natives/src/crypto/rand/rand.go @@ -1,5 +1,4 @@ //go:build js -// +build js package rand diff --git a/compiler/natives/src/database/sql/driver/driver_test.go b/compiler/natives/src/database/sql/driver/driver_test.go index 446da47c5..d943d5f0c 100644 --- a/compiler/natives/src/database/sql/driver/driver_test.go +++ b/compiler/natives/src/database/sql/driver/driver_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package driver diff --git a/compiler/natives/src/debug/pe/symbol.go b/compiler/natives/src/debug/pe/symbol.go index 798502ce3..7564b8f77 100644 --- a/compiler/natives/src/debug/pe/symbol.go +++ b/compiler/natives/src/debug/pe/symbol.go @@ -1,5 +1,4 @@ //go:build js -// +build js package pe diff --git a/compiler/natives/src/embed/embed.go b/compiler/natives/src/embed/embed.go index bb9738546..83cf729ac 100644 --- a/compiler/natives/src/embed/embed.go +++ b/compiler/natives/src/embed/embed.go @@ -1,5 +1,4 @@ //go:build js -// +build js package embed diff --git a/compiler/natives/src/encoding/gob/gob_test.go b/compiler/natives/src/encoding/gob/gob_test.go index 823b572ac..1678f462f 100644 --- a/compiler/natives/src/encoding/gob/gob_test.go +++ b/compiler/natives/src/encoding/gob/gob_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package gob diff --git a/compiler/natives/src/encoding/json/stream_test.go b/compiler/natives/src/encoding/json/stream_test.go index adad8e153..484ee5c12 100644 --- a/compiler/natives/src/encoding/json/stream_test.go +++ b/compiler/natives/src/encoding/json/stream_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package json diff --git a/compiler/natives/src/fmt/fmt_test.go b/compiler/natives/src/fmt/fmt_test.go index 70797693b..1a1c3278c 100644 --- a/compiler/natives/src/fmt/fmt_test.go +++ b/compiler/natives/src/fmt/fmt_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package fmt_test diff --git a/compiler/natives/src/go/token/position.go b/compiler/natives/src/go/token/position.go index 6a1ee0c15..8cf111ab2 100644 --- a/compiler/natives/src/go/token/position.go +++ b/compiler/natives/src/go/token/position.go @@ -1,5 +1,4 @@ //go:build js -// +build js package token diff --git a/compiler/natives/src/go/token/token_test.go b/compiler/natives/src/go/token/token_test.go index 335ee0776..1eb777123 100644 --- a/compiler/natives/src/go/token/token_test.go +++ b/compiler/natives/src/go/token/token_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package token diff --git a/compiler/natives/src/golang.org/x/crypto/internal/alias/alias.go b/compiler/natives/src/golang.org/x/crypto/internal/alias/alias.go index a3e1e7f79..9b168a95a 100644 --- a/compiler/natives/src/golang.org/x/crypto/internal/alias/alias.go +++ b/compiler/natives/src/golang.org/x/crypto/internal/alias/alias.go @@ -1,5 +1,4 @@ //go:build js -// +build js package alias diff --git a/compiler/natives/src/golang.org/x/crypto/internal/subtle/aliasing.go b/compiler/natives/src/golang.org/x/crypto/internal/subtle/aliasing.go index 145687d59..23c9843c8 100644 --- a/compiler/natives/src/golang.org/x/crypto/internal/subtle/aliasing.go +++ b/compiler/natives/src/golang.org/x/crypto/internal/subtle/aliasing.go @@ -1,5 +1,4 @@ //go:build js -// +build js package subtle diff --git a/compiler/natives/src/hash/maphash/maphash.go b/compiler/natives/src/hash/maphash/maphash.go index 5c982404f..08ed83760 100644 --- a/compiler/natives/src/hash/maphash/maphash.go +++ b/compiler/natives/src/hash/maphash/maphash.go @@ -1,5 +1,4 @@ //go:build js -// +build js package maphash diff --git a/compiler/natives/src/internal/bytealg/bytealg.go b/compiler/natives/src/internal/bytealg/bytealg.go index dbcc6dec4..15a3c2d70 100644 --- a/compiler/natives/src/internal/bytealg/bytealg.go +++ b/compiler/natives/src/internal/bytealg/bytealg.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bytealg diff --git a/compiler/natives/src/internal/cpu/cpu.go b/compiler/natives/src/internal/cpu/cpu.go index 794a58d6a..a8c25ea8e 100644 --- a/compiler/natives/src/internal/cpu/cpu.go +++ b/compiler/natives/src/internal/cpu/cpu.go @@ -1,5 +1,4 @@ //go:build js -// +build js package cpu diff --git a/compiler/natives/src/internal/fmtsort/fmtsort_test.go b/compiler/natives/src/internal/fmtsort/fmtsort_test.go index f45987d72..50b65bb45 100644 --- a/compiler/natives/src/internal/fmtsort/fmtsort_test.go +++ b/compiler/natives/src/internal/fmtsort/fmtsort_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package fmtsort_test diff --git a/compiler/natives/src/internal/poll/semaphore.go b/compiler/natives/src/internal/poll/semaphore.go index 5e4f5ea8d..70d26b6c5 100644 --- a/compiler/natives/src/internal/poll/semaphore.go +++ b/compiler/natives/src/internal/poll/semaphore.go @@ -1,5 +1,4 @@ //go:build js -// +build js package poll diff --git a/compiler/natives/src/internal/reflectlite/all_test.go b/compiler/natives/src/internal/reflectlite/all_test.go index 4445189a0..7ddee2353 100644 --- a/compiler/natives/src/internal/reflectlite/all_test.go +++ b/compiler/natives/src/internal/reflectlite/all_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflectlite_test diff --git a/compiler/natives/src/internal/reflectlite/export_test.go b/compiler/natives/src/internal/reflectlite/export_test.go index d663e65ba..06e7eb004 100644 --- a/compiler/natives/src/internal/reflectlite/export_test.go +++ b/compiler/natives/src/internal/reflectlite/export_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflectlite diff --git a/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go b/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go index 01504f582..85493a061 100644 --- a/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go +++ b/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflectlite_test diff --git a/compiler/natives/src/internal/reflectlite/reflectlite.go b/compiler/natives/src/internal/reflectlite/reflectlite.go index d48f15987..f55eb4437 100644 --- a/compiler/natives/src/internal/reflectlite/reflectlite.go +++ b/compiler/natives/src/internal/reflectlite/reflectlite.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflectlite diff --git a/compiler/natives/src/internal/reflectlite/swapper.go b/compiler/natives/src/internal/reflectlite/swapper.go index b8827c226..b324eb6e7 100644 --- a/compiler/natives/src/internal/reflectlite/swapper.go +++ b/compiler/natives/src/internal/reflectlite/swapper.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflectlite diff --git a/compiler/natives/src/internal/reflectlite/type.go b/compiler/natives/src/internal/reflectlite/type.go index 0a41e862e..949599725 100644 --- a/compiler/natives/src/internal/reflectlite/type.go +++ b/compiler/natives/src/internal/reflectlite/type.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflectlite diff --git a/compiler/natives/src/internal/reflectlite/utils.go b/compiler/natives/src/internal/reflectlite/utils.go index 1941f0d0e..c5f714fd4 100644 --- a/compiler/natives/src/internal/reflectlite/utils.go +++ b/compiler/natives/src/internal/reflectlite/utils.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflectlite diff --git a/compiler/natives/src/internal/reflectlite/value.go b/compiler/natives/src/internal/reflectlite/value.go index 32d310723..12f8d292f 100644 --- a/compiler/natives/src/internal/reflectlite/value.go +++ b/compiler/natives/src/internal/reflectlite/value.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflectlite diff --git a/compiler/natives/src/internal/unsafeheader/unsafeheader_test.go b/compiler/natives/src/internal/unsafeheader/unsafeheader_test.go index f20cf31fa..abe54639b 100644 --- a/compiler/natives/src/internal/unsafeheader/unsafeheader_test.go +++ b/compiler/natives/src/internal/unsafeheader/unsafeheader_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package unsafeheader_test diff --git a/compiler/natives/src/io/io_test.go b/compiler/natives/src/io/io_test.go index d746b3709..ae0945f22 100644 --- a/compiler/natives/src/io/io_test.go +++ b/compiler/natives/src/io/io_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package io_test diff --git a/compiler/natives/src/math/big/big.go b/compiler/natives/src/math/big/big.go index 25512db31..4c850d165 100644 --- a/compiler/natives/src/math/big/big.go +++ b/compiler/natives/src/math/big/big.go @@ -1,5 +1,4 @@ //go:build js -// +build js package big diff --git a/compiler/natives/src/math/big/big_test.go b/compiler/natives/src/math/big/big_test.go index acad9a043..ae2dd5916 100644 --- a/compiler/natives/src/math/big/big_test.go +++ b/compiler/natives/src/math/big/big_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package big diff --git a/compiler/natives/src/math/bits/bits.go b/compiler/natives/src/math/bits/bits.go index b434603a4..cf4b66df6 100644 --- a/compiler/natives/src/math/bits/bits.go +++ b/compiler/natives/src/math/bits/bits.go @@ -1,5 +1,4 @@ //go:build js -// +build js package bits diff --git a/compiler/natives/src/math/math.go b/compiler/natives/src/math/math.go index b0ed2da0d..8ba472df2 100644 --- a/compiler/natives/src/math/math.go +++ b/compiler/natives/src/math/math.go @@ -1,5 +1,4 @@ //go:build js -// +build js package math diff --git a/compiler/natives/src/math/math_test.go b/compiler/natives/src/math/math_test.go index eb62dd1d1..f4b5311de 100644 --- a/compiler/natives/src/math/math_test.go +++ b/compiler/natives/src/math/math_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package math_test diff --git a/compiler/natives/src/math/rand/rand_test.go b/compiler/natives/src/math/rand/rand_test.go index 2246dfec4..e51d3a321 100644 --- a/compiler/natives/src/math/rand/rand_test.go +++ b/compiler/natives/src/math/rand/rand_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package rand_test diff --git a/compiler/natives/src/net/fastrand.go b/compiler/natives/src/net/fastrand.go index 8feafc78f..153fe13b9 100644 --- a/compiler/natives/src/net/fastrand.go +++ b/compiler/natives/src/net/fastrand.go @@ -1,5 +1,4 @@ //go:build js -// +build js package net diff --git a/compiler/natives/src/net/http/clientserver_test.go b/compiler/natives/src/net/http/clientserver_test.go index 35b44dd4d..e04ba34b2 100644 --- a/compiler/natives/src/net/http/clientserver_test.go +++ b/compiler/natives/src/net/http/clientserver_test.go @@ -1,5 +1,4 @@ //go:build js && wasm -// +build js,wasm package http_test diff --git a/compiler/natives/src/net/http/cookiejar/example_test.go b/compiler/natives/src/net/http/cookiejar/example_test.go index 09de5d0cf..7171867c1 100644 --- a/compiler/natives/src/net/http/cookiejar/example_test.go +++ b/compiler/natives/src/net/http/cookiejar/example_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package cookiejar_test diff --git a/compiler/natives/src/net/http/http.go b/compiler/natives/src/net/http/http.go index 8fd607c4d..fbd7a8927 100644 --- a/compiler/natives/src/net/http/http.go +++ b/compiler/natives/src/net/http/http.go @@ -1,5 +1,4 @@ //go:build js -// +build js package http diff --git a/compiler/natives/src/net/http/http_wasm_test.go b/compiler/natives/src/net/http/http_wasm_test.go index d078c0ea3..4c10efd9a 100644 --- a/compiler/natives/src/net/http/http_wasm_test.go +++ b/compiler/natives/src/net/http/http_wasm_test.go @@ -1,5 +1,4 @@ //go:build js && wasm -// +build js,wasm package http diff --git a/compiler/natives/src/net/http/main_test.go b/compiler/natives/src/net/http/main_test.go index bb747d123..2b2e3ec32 100644 --- a/compiler/natives/src/net/http/main_test.go +++ b/compiler/natives/src/net/http/main_test.go @@ -1,5 +1,4 @@ //go:build js && wasm -// +build js,wasm package http_test diff --git a/compiler/natives/src/net/http/server_test.go b/compiler/natives/src/net/http/server_test.go index f55704dcf..401eeff7c 100644 --- a/compiler/natives/src/net/http/server_test.go +++ b/compiler/natives/src/net/http/server_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package http_test diff --git a/compiler/natives/src/net/http/transport_test.go b/compiler/natives/src/net/http/transport_test.go index a173e47e7..2ece96853 100644 --- a/compiler/natives/src/net/http/transport_test.go +++ b/compiler/natives/src/net/http/transport_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package http_test diff --git a/compiler/natives/src/net/netip/export_test.go b/compiler/natives/src/net/netip/export_test.go index 03b7cbe1b..fca2dc07c 100644 --- a/compiler/natives/src/net/netip/export_test.go +++ b/compiler/natives/src/net/netip/export_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package netip diff --git a/compiler/natives/src/net/netip/fuzz_test.go b/compiler/natives/src/net/netip/fuzz_test.go index f7359c5bb..574201d2d 100644 --- a/compiler/natives/src/net/netip/fuzz_test.go +++ b/compiler/natives/src/net/netip/fuzz_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package netip_test diff --git a/compiler/natives/src/net/netip/netip.go b/compiler/natives/src/net/netip/netip.go index 9d2b8b2d6..61b40fc24 100644 --- a/compiler/natives/src/net/netip/netip.go +++ b/compiler/natives/src/net/netip/netip.go @@ -1,5 +1,4 @@ //go:build js -// +build js package netip diff --git a/compiler/natives/src/net/netip/netip_test.go b/compiler/natives/src/net/netip/netip_test.go index 46b116c00..280929425 100644 --- a/compiler/natives/src/net/netip/netip_test.go +++ b/compiler/natives/src/net/netip/netip_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package netip_test diff --git a/compiler/natives/src/os/file.go b/compiler/natives/src/os/file.go index a3683b8b0..2df42121a 100644 --- a/compiler/natives/src/os/file.go +++ b/compiler/natives/src/os/file.go @@ -1,5 +1,4 @@ //go:build js -// +build js package os diff --git a/compiler/natives/src/os/os.go b/compiler/natives/src/os/os.go index a45e13508..103ccb288 100644 --- a/compiler/natives/src/os/os.go +++ b/compiler/natives/src/os/os.go @@ -1,5 +1,4 @@ //go:build js -// +build js package os diff --git a/compiler/natives/src/os/signal/signal.go b/compiler/natives/src/os/signal/signal.go index fe38d22b2..b39453926 100644 --- a/compiler/natives/src/os/signal/signal.go +++ b/compiler/natives/src/os/signal/signal.go @@ -1,5 +1,4 @@ //go:build js -// +build js package signal diff --git a/compiler/natives/src/reflect/example_test.go b/compiler/natives/src/reflect/example_test.go index 0deab2ed5..65abf3dd6 100644 --- a/compiler/natives/src/reflect/example_test.go +++ b/compiler/natives/src/reflect/example_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflect_test diff --git a/compiler/natives/src/reflect/reflect.go b/compiler/natives/src/reflect/reflect.go index 81f4c7b08..f1f24e559 100644 --- a/compiler/natives/src/reflect/reflect.go +++ b/compiler/natives/src/reflect/reflect.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflect diff --git a/compiler/natives/src/reflect/reflect_test.go b/compiler/natives/src/reflect/reflect_test.go index 4c0bcd0be..9fb413c65 100644 --- a/compiler/natives/src/reflect/reflect_test.go +++ b/compiler/natives/src/reflect/reflect_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflect_test diff --git a/compiler/natives/src/reflect/swapper.go b/compiler/natives/src/reflect/swapper.go index 068f984ed..d951ff668 100644 --- a/compiler/natives/src/reflect/swapper.go +++ b/compiler/natives/src/reflect/swapper.go @@ -1,5 +1,4 @@ //go:build js -// +build js package reflect diff --git a/compiler/natives/src/regexp/regexp_test.go b/compiler/natives/src/regexp/regexp_test.go index 3a2d58d32..3966e876d 100644 --- a/compiler/natives/src/regexp/regexp_test.go +++ b/compiler/natives/src/regexp/regexp_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package regexp diff --git a/compiler/natives/src/runtime/debug/debug.go b/compiler/natives/src/runtime/debug/debug.go index b45da62bb..1d94ab89b 100644 --- a/compiler/natives/src/runtime/debug/debug.go +++ b/compiler/natives/src/runtime/debug/debug.go @@ -1,5 +1,4 @@ //go:build js -// +build js package debug diff --git a/compiler/natives/src/runtime/fastrand.go b/compiler/natives/src/runtime/fastrand.go index a5f2bdbb8..b902a5177 100644 --- a/compiler/natives/src/runtime/fastrand.go +++ b/compiler/natives/src/runtime/fastrand.go @@ -1,5 +1,4 @@ //go:build js -// +build js package runtime diff --git a/compiler/natives/src/runtime/pprof/pprof.go b/compiler/natives/src/runtime/pprof/pprof.go index f398ca21b..512ef1a5b 100644 --- a/compiler/natives/src/runtime/pprof/pprof.go +++ b/compiler/natives/src/runtime/pprof/pprof.go @@ -1,5 +1,4 @@ //go:build js -// +build js package pprof diff --git a/compiler/natives/src/runtime/runtime.go b/compiler/natives/src/runtime/runtime.go index 9f8425af8..24513175c 100644 --- a/compiler/natives/src/runtime/runtime.go +++ b/compiler/natives/src/runtime/runtime.go @@ -1,5 +1,4 @@ //go:build js -// +build js package runtime diff --git a/compiler/natives/src/strconv/atoi.go b/compiler/natives/src/strconv/atoi.go index 63ea9b732..5b4444ee0 100644 --- a/compiler/natives/src/strconv/atoi.go +++ b/compiler/natives/src/strconv/atoi.go @@ -1,5 +1,4 @@ //go:build js -// +build js package strconv diff --git a/compiler/natives/src/strconv/itoa.go b/compiler/natives/src/strconv/itoa.go index c5440c78e..790555318 100644 --- a/compiler/natives/src/strconv/itoa.go +++ b/compiler/natives/src/strconv/itoa.go @@ -1,5 +1,4 @@ //go:build js -// +build js package strconv diff --git a/compiler/natives/src/strings/strings.go b/compiler/natives/src/strings/strings.go index 2867872f6..6d8bedd54 100644 --- a/compiler/natives/src/strings/strings.go +++ b/compiler/natives/src/strings/strings.go @@ -1,5 +1,4 @@ //go:build js -// +build js package strings diff --git a/compiler/natives/src/strings/strings_test.go b/compiler/natives/src/strings/strings_test.go index fb9a4a57a..adc05d6c5 100644 --- a/compiler/natives/src/strings/strings_test.go +++ b/compiler/natives/src/strings/strings_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package strings_test diff --git a/compiler/natives/src/sync/atomic/atomic.go b/compiler/natives/src/sync/atomic/atomic.go index 1cbfe65f9..99aa8559f 100644 --- a/compiler/natives/src/sync/atomic/atomic.go +++ b/compiler/natives/src/sync/atomic/atomic.go @@ -1,5 +1,4 @@ //go:build js -// +build js package atomic diff --git a/compiler/natives/src/sync/atomic/atomic_test.go b/compiler/natives/src/sync/atomic/atomic_test.go index e1ec6086c..8dbd0d027 100644 --- a/compiler/natives/src/sync/atomic/atomic_test.go +++ b/compiler/natives/src/sync/atomic/atomic_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package atomic_test diff --git a/compiler/natives/src/sync/cond.go b/compiler/natives/src/sync/cond.go index 916ace8f7..1f94d5284 100644 --- a/compiler/natives/src/sync/cond.go +++ b/compiler/natives/src/sync/cond.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync diff --git a/compiler/natives/src/sync/cond_test.go b/compiler/natives/src/sync/cond_test.go index 3b286ba49..e530b9625 100644 --- a/compiler/natives/src/sync/cond_test.go +++ b/compiler/natives/src/sync/cond_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync_test diff --git a/compiler/natives/src/sync/map_test.go b/compiler/natives/src/sync/map_test.go index 432096071..5e8460411 100644 --- a/compiler/natives/src/sync/map_test.go +++ b/compiler/natives/src/sync/map_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync_test diff --git a/compiler/natives/src/sync/pool.go b/compiler/natives/src/sync/pool.go index 9d3825e14..c865ca5dc 100644 --- a/compiler/natives/src/sync/pool.go +++ b/compiler/natives/src/sync/pool.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync diff --git a/compiler/natives/src/sync/pool_test.go b/compiler/natives/src/sync/pool_test.go index ea35fd136..09ab79b32 100644 --- a/compiler/natives/src/sync/pool_test.go +++ b/compiler/natives/src/sync/pool_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync_test diff --git a/compiler/natives/src/sync/sync.go b/compiler/natives/src/sync/sync.go index 294b0b109..5ca82694e 100644 --- a/compiler/natives/src/sync/sync.go +++ b/compiler/natives/src/sync/sync.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync diff --git a/compiler/natives/src/sync/waitgroup.go b/compiler/natives/src/sync/waitgroup.go index e1f20eeb6..99cb49ff7 100644 --- a/compiler/natives/src/sync/waitgroup.go +++ b/compiler/natives/src/sync/waitgroup.go @@ -1,5 +1,4 @@ //go:build js -// +build js package sync diff --git a/compiler/natives/src/syscall/js/export_test.go b/compiler/natives/src/syscall/js/export_test.go index 8f030c4d7..fb133a150 100644 --- a/compiler/natives/src/syscall/js/export_test.go +++ b/compiler/natives/src/syscall/js/export_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package js diff --git a/compiler/natives/src/syscall/js/js.go b/compiler/natives/src/syscall/js/js.go index c0c18a614..03a09fde8 100644 --- a/compiler/natives/src/syscall/js/js.go +++ b/compiler/natives/src/syscall/js/js.go @@ -1,5 +1,4 @@ //go:build js -// +build js package js diff --git a/compiler/natives/src/syscall/js/js_test.go b/compiler/natives/src/syscall/js/js_test.go index 999266da2..4c6e091df 100644 --- a/compiler/natives/src/syscall/js/js_test.go +++ b/compiler/natives/src/syscall/js/js_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package js_test diff --git a/compiler/natives/src/testing/allocs_test.go b/compiler/natives/src/testing/allocs_test.go index 54c2f545b..e1f4cffba 100644 --- a/compiler/natives/src/testing/allocs_test.go +++ b/compiler/natives/src/testing/allocs_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package testing_test diff --git a/compiler/natives/src/testing/example.go b/compiler/natives/src/testing/example.go index bf8d06482..8568fec2c 100644 --- a/compiler/natives/src/testing/example.go +++ b/compiler/natives/src/testing/example.go @@ -1,5 +1,4 @@ //go:build js -// +build js package testing diff --git a/compiler/natives/src/testing/helper_test.go b/compiler/natives/src/testing/helper_test.go index 6815fd651..3615da315 100644 --- a/compiler/natives/src/testing/helper_test.go +++ b/compiler/natives/src/testing/helper_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package testing diff --git a/compiler/natives/src/testing/helperfuncs_test.go b/compiler/natives/src/testing/helperfuncs_test.go index 54a1ee737..560c0466a 100644 --- a/compiler/natives/src/testing/helperfuncs_test.go +++ b/compiler/natives/src/testing/helperfuncs_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package testing diff --git a/compiler/natives/src/testing/sub_test.go b/compiler/natives/src/testing/sub_test.go index 1e9a79e47..300045db3 100644 --- a/compiler/natives/src/testing/sub_test.go +++ b/compiler/natives/src/testing/sub_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package testing diff --git a/compiler/natives/src/text/template/template.go b/compiler/natives/src/text/template/template.go index 056fe9c85..d5cf5de56 100644 --- a/compiler/natives/src/text/template/template.go +++ b/compiler/natives/src/text/template/template.go @@ -1,5 +1,4 @@ //go:build js -// +build js package template diff --git a/compiler/natives/src/time/time.go b/compiler/natives/src/time/time.go index 40c596e68..3b0cb0ef4 100644 --- a/compiler/natives/src/time/time.go +++ b/compiler/natives/src/time/time.go @@ -1,5 +1,4 @@ //go:build js -// +build js package time diff --git a/compiler/natives/src/time/time_test.go b/compiler/natives/src/time/time_test.go index 5d4119cc6..7f161fdac 100644 --- a/compiler/natives/src/time/time_test.go +++ b/compiler/natives/src/time/time_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package time_test diff --git a/compiler/natives/src/time/zoneinfo_js.go b/compiler/natives/src/time/zoneinfo_js.go index 0101b95f9..2932b9cf0 100644 --- a/compiler/natives/src/time/zoneinfo_js.go +++ b/compiler/natives/src/time/zoneinfo_js.go @@ -1,5 +1,4 @@ //go:build js -// +build js package time diff --git a/compiler/natives/src/unicode/unicode.go b/compiler/natives/src/unicode/unicode.go index a622c32f1..806f72bdb 100644 --- a/compiler/natives/src/unicode/unicode.go +++ b/compiler/natives/src/unicode/unicode.go @@ -1,5 +1,4 @@ //go:build js -// +build js package unicode diff --git a/compiler/natives/src/vendor/golang.org/x/crypto/internal/subtle/aliasing.go b/compiler/natives/src/vendor/golang.org/x/crypto/internal/subtle/aliasing.go index 104ac82bb..0b515aeee 100644 --- a/compiler/natives/src/vendor/golang.org/x/crypto/internal/subtle/aliasing.go +++ b/compiler/natives/src/vendor/golang.org/x/crypto/internal/subtle/aliasing.go @@ -1,5 +1,4 @@ //go:build js -// +build js package subtle diff --git a/internal/sysutil/sysutil.go b/internal/sysutil/sysutil.go index e19eb02e1..8fc72403d 100644 --- a/internal/sysutil/sysutil.go +++ b/internal/sysutil/sysutil.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows // Package sysutil contains system-specific utilities. package sysutil diff --git a/js/js_test.go b/js/js_test.go index d7a904954..53fee6473 100644 --- a/js/js_test.go +++ b/js/js_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package js_test diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 3685e99d6..0cb3b8143 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -1,5 +1,4 @@ //go:build ignore -// +build ignore // skip diff --git a/tests/js_test.go b/tests/js_test.go index 6f6eaa542..bed64179c 100644 --- a/tests/js_test.go +++ b/tests/js_test.go @@ -1,5 +1,4 @@ //go:build js && !wasm -// +build js,!wasm package tests_test diff --git a/tests/map_js_test.go b/tests/map_js_test.go index c815661ab..04847d3d4 100644 --- a/tests/map_js_test.go +++ b/tests/map_js_test.go @@ -1,5 +1,4 @@ //go:build js && !wasm -// +build js,!wasm package tests diff --git a/tests/syscall_test.go b/tests/syscall_test.go index 104800df7..ea880a218 100644 --- a/tests/syscall_test.go +++ b/tests/syscall_test.go @@ -1,5 +1,4 @@ //go:build js -// +build js package tests diff --git a/tests/testdata/legacy_syscall/main.go b/tests/testdata/legacy_syscall/main.go index 75ba22f6b..359d737af 100644 --- a/tests/testdata/legacy_syscall/main.go +++ b/tests/testdata/legacy_syscall/main.go @@ -1,5 +1,4 @@ //go:build legacy_syscall && gopherjs -// +build legacy_syscall,gopherjs // This program tests GopherJS's ability to perform raw syscalls using the // deprecated node_syscall extension. See TestLegacySyscall. From eded33e67bee3131ed021c3a824cc0d406155dc7 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Wed, 13 Aug 2025 11:46:06 -0600 Subject: [PATCH 54/58] Formatting imports --- compiler/gopherjspkg/fs.go | 4 +--- compiler/natives/src/bytes/bytes_test.go | 4 +--- compiler/natives/src/compress/gzip/example_test.go | 4 +--- compiler/natives/src/go/parser/parser_test.go | 4 +--- compiler/natives/src/go/token/token_test.go | 4 +--- compiler/natives/src/hash/maphash/maphash.go | 4 +--- compiler/natives/src/internal/poll/semaphore.go | 4 +--- compiler/natives/src/internal/reflectlite/export_test.go | 4 +--- .../natives/src/internal/reflectlite/reflect_mirror_test.go | 4 +--- compiler/natives/src/internal/reflectlite/utils.go | 4 +--- compiler/natives/src/io/io_test.go | 4 +--- compiler/natives/src/math/math.go | 4 +--- compiler/natives/src/math/math_test.go | 4 +--- compiler/natives/src/net/fastrand.go | 4 +--- compiler/natives/src/net/http/client_test.go | 4 +--- compiler/natives/src/net/http/clientserver_test.go | 4 +--- compiler/natives/src/regexp/regexp_test.go | 4 +--- compiler/natives/src/runtime/runtime.go | 4 +--- compiler/natives/src/strconv/atoi.go | 4 +--- compiler/natives/src/strconv/itoa.go | 4 +--- compiler/natives/src/syscall/fs_js.go | 4 +--- compiler/natives/src/syscall/legacy.go | 4 +--- compiler/natives/src/syscall/syscall_js_wasm.go | 4 +--- compiler/natives/src/time/time_test.go | 4 +--- tests/alias_test.go | 4 +--- tests/compiler_test.go | 4 +--- 26 files changed, 26 insertions(+), 78 deletions(-) diff --git a/compiler/gopherjspkg/fs.go b/compiler/gopherjspkg/fs.go index 0ec155308..caa07e11c 100644 --- a/compiler/gopherjspkg/fs.go +++ b/compiler/gopherjspkg/fs.go @@ -1,8 +1,6 @@ package gopherjspkg -import ( - "net/http" -) +import "net/http" // FS is a virtual filesystem that contains core GopherJS packages. var FS http.FileSystem diff --git a/compiler/natives/src/bytes/bytes_test.go b/compiler/natives/src/bytes/bytes_test.go index 02aac5ea9..5a2277f59 100644 --- a/compiler/natives/src/bytes/bytes_test.go +++ b/compiler/natives/src/bytes/bytes_test.go @@ -2,9 +2,7 @@ package bytes_test -import ( - "testing" -) +import "testing" func dangerousSlice(t *testing.T) []byte { t.Skip("dangerousSlice relies on syscall.Getpagesize, which GopherJS doesn't implement") diff --git a/compiler/natives/src/compress/gzip/example_test.go b/compiler/natives/src/compress/gzip/example_test.go index 6b8f703bd..37b3e734c 100644 --- a/compiler/natives/src/compress/gzip/example_test.go +++ b/compiler/natives/src/compress/gzip/example_test.go @@ -2,9 +2,7 @@ package gzip_test -import ( - "fmt" -) +import "fmt" // The test relies on a local HTTP server, which is not supported under NodeJS. func Example_compressingReader() { diff --git a/compiler/natives/src/go/parser/parser_test.go b/compiler/natives/src/go/parser/parser_test.go index 7fded29fd..00f533233 100644 --- a/compiler/natives/src/go/parser/parser_test.go +++ b/compiler/natives/src/go/parser/parser_test.go @@ -2,9 +2,7 @@ package parser -import ( - "testing" -) +import "testing" func TestParseDepthLimit(t *testing.T) { t.Skip("causes call stack exhaustion on js/ecmascript") diff --git a/compiler/natives/src/go/token/token_test.go b/compiler/natives/src/go/token/token_test.go index 1eb777123..983c84dee 100644 --- a/compiler/natives/src/go/token/token_test.go +++ b/compiler/natives/src/go/token/token_test.go @@ -2,9 +2,7 @@ package token -import ( - "testing" -) +import "testing" func TestFileSetRace(t *testing.T) { t.Skip() diff --git a/compiler/natives/src/hash/maphash/maphash.go b/compiler/natives/src/hash/maphash/maphash.go index 08ed83760..7c2a68cf6 100644 --- a/compiler/natives/src/hash/maphash/maphash.go +++ b/compiler/natives/src/hash/maphash/maphash.go @@ -2,9 +2,7 @@ package maphash -import ( - _ "unsafe" // for linkname -) +import _ "unsafe" // for linkname // hashkey is similar how it is defined in runtime/alg.go for Go 1.19 // to be used in hash{32,64}.go to seed the hash function as part of diff --git a/compiler/natives/src/internal/poll/semaphore.go b/compiler/natives/src/internal/poll/semaphore.go index 70d26b6c5..112a37768 100644 --- a/compiler/natives/src/internal/poll/semaphore.go +++ b/compiler/natives/src/internal/poll/semaphore.go @@ -2,9 +2,7 @@ package poll -import ( - _ "unsafe" // For go:linkname -) +import _ "unsafe" // For go:linkname //go:linkname runtime_Semacquire sync.runtime_Semacquire func runtime_Semacquire(s *uint32) diff --git a/compiler/natives/src/internal/reflectlite/export_test.go b/compiler/natives/src/internal/reflectlite/export_test.go index 06e7eb004..c80ba65a8 100644 --- a/compiler/natives/src/internal/reflectlite/export_test.go +++ b/compiler/natives/src/internal/reflectlite/export_test.go @@ -2,9 +2,7 @@ package reflectlite -import ( - "unsafe" -) +import "unsafe" // Field returns the i'th field of the struct v. // It panics if v's Kind is not Struct or i is out of range. diff --git a/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go b/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go index 85493a061..4ca0c29c6 100644 --- a/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go +++ b/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go @@ -2,9 +2,7 @@ package reflectlite_test -import ( - "testing" -) +import "testing" func TestMirrorWithReflect(t *testing.T) { t.Skip("TestMirrorWithReflect") diff --git a/compiler/natives/src/internal/reflectlite/utils.go b/compiler/natives/src/internal/reflectlite/utils.go index c5f714fd4..c832bd4cf 100644 --- a/compiler/natives/src/internal/reflectlite/utils.go +++ b/compiler/natives/src/internal/reflectlite/utils.go @@ -2,9 +2,7 @@ package reflectlite -import ( - "unsafe" -) +import "unsafe" type ChanDir int diff --git a/compiler/natives/src/io/io_test.go b/compiler/natives/src/io/io_test.go index ae0945f22..fd8cdaaec 100644 --- a/compiler/natives/src/io/io_test.go +++ b/compiler/natives/src/io/io_test.go @@ -2,9 +2,7 @@ package io_test -import ( - "testing" -) +import "testing" func TestMultiWriter_WriteStringSingleAlloc(t *testing.T) { t.Skip() diff --git a/compiler/natives/src/math/math.go b/compiler/natives/src/math/math.go index 8ba472df2..fb0777b12 100644 --- a/compiler/natives/src/math/math.go +++ b/compiler/natives/src/math/math.go @@ -2,9 +2,7 @@ package math -import ( - "github.com/gopherjs/gopherjs/js" -) +import "github.com/gopherjs/gopherjs/js" var ( math = js.Global.Get("Math") diff --git a/compiler/natives/src/math/math_test.go b/compiler/natives/src/math/math_test.go index f4b5311de..de0f7fd8d 100644 --- a/compiler/natives/src/math/math_test.go +++ b/compiler/natives/src/math/math_test.go @@ -2,9 +2,7 @@ package math_test -import ( - "testing" -) +import "testing" // Slightly higher tolerances than upstream, otherwise TestGamma fails. // TODO: Is there a better way to fix TestGamma? It's weird that only one test diff --git a/compiler/natives/src/net/fastrand.go b/compiler/natives/src/net/fastrand.go index 153fe13b9..878f970a5 100644 --- a/compiler/natives/src/net/fastrand.go +++ b/compiler/natives/src/net/fastrand.go @@ -2,9 +2,7 @@ package net -import ( - _ "unsafe" // For go:linkname -) +import _ "unsafe" // For go:linkname //go:linkname fastrandu runtime.fastrandu func fastrandu() uint diff --git a/compiler/natives/src/net/http/client_test.go b/compiler/natives/src/net/http/client_test.go index 302b800df..f8c3b744e 100644 --- a/compiler/natives/src/net/http/client_test.go +++ b/compiler/natives/src/net/http/client_test.go @@ -2,9 +2,7 @@ package http_test -import ( - "testing" -) +import "testing" func testClientTimeout(t *testing.T, h2 bool) { // The original test expects Client.Timeout error to be returned, but under diff --git a/compiler/natives/src/net/http/clientserver_test.go b/compiler/natives/src/net/http/clientserver_test.go index e04ba34b2..fc4486998 100644 --- a/compiler/natives/src/net/http/clientserver_test.go +++ b/compiler/natives/src/net/http/clientserver_test.go @@ -2,9 +2,7 @@ package http_test -import ( - "testing" -) +import "testing" func testTransportGCRequest(t *testing.T, h2, body bool) { t.Skip("The test relies on runtime.SetFinalizer(), which is not supported by GopherJS.") diff --git a/compiler/natives/src/regexp/regexp_test.go b/compiler/natives/src/regexp/regexp_test.go index 3966e876d..471281fa1 100644 --- a/compiler/natives/src/regexp/regexp_test.go +++ b/compiler/natives/src/regexp/regexp_test.go @@ -2,9 +2,7 @@ package regexp -import ( - "testing" -) +import "testing" //gopherjs:keep-original func TestOnePassCutoff(t *testing.T) { diff --git a/compiler/natives/src/runtime/runtime.go b/compiler/natives/src/runtime/runtime.go index 24513175c..56846b018 100644 --- a/compiler/natives/src/runtime/runtime.go +++ b/compiler/natives/src/runtime/runtime.go @@ -2,9 +2,7 @@ package runtime -import ( - "github.com/gopherjs/gopherjs/js" -) +import "github.com/gopherjs/gopherjs/js" const ( GOOS = "js" diff --git a/compiler/natives/src/strconv/atoi.go b/compiler/natives/src/strconv/atoi.go index 5b4444ee0..1f26293a3 100644 --- a/compiler/natives/src/strconv/atoi.go +++ b/compiler/natives/src/strconv/atoi.go @@ -2,9 +2,7 @@ package strconv -import ( - "github.com/gopherjs/gopherjs/js" -) +import "github.com/gopherjs/gopherjs/js" const ( maxInt32 float64 = 1<<31 - 1 diff --git a/compiler/natives/src/strconv/itoa.go b/compiler/natives/src/strconv/itoa.go index 790555318..04dac0b71 100644 --- a/compiler/natives/src/strconv/itoa.go +++ b/compiler/natives/src/strconv/itoa.go @@ -2,9 +2,7 @@ package strconv -import ( - "github.com/gopherjs/gopherjs/js" -) +import "github.com/gopherjs/gopherjs/js" // Itoa in gopherjs is always a 32bit int so the native toString // always handles it successfully. diff --git a/compiler/natives/src/syscall/fs_js.go b/compiler/natives/src/syscall/fs_js.go index 5a0a5a64d..23c2b2145 100644 --- a/compiler/natives/src/syscall/fs_js.go +++ b/compiler/natives/src/syscall/fs_js.go @@ -2,9 +2,7 @@ package syscall -import ( - "syscall/js" -) +import "syscall/js" // fsCall emulates a file system-related syscall via a corresponding NodeJS fs // API. diff --git a/compiler/natives/src/syscall/legacy.go b/compiler/natives/src/syscall/legacy.go index beb99eb78..ad1df8bda 100644 --- a/compiler/natives/src/syscall/legacy.go +++ b/compiler/natives/src/syscall/legacy.go @@ -2,9 +2,7 @@ package syscall -import ( - "github.com/gopherjs/gopherjs/js" -) +import "github.com/gopherjs/gopherjs/js" var ( syscallModule *js.Object diff --git a/compiler/natives/src/syscall/syscall_js_wasm.go b/compiler/natives/src/syscall/syscall_js_wasm.go index 5bcbdeed4..648c228cb 100644 --- a/compiler/natives/src/syscall/syscall_js_wasm.go +++ b/compiler/natives/src/syscall/syscall_js_wasm.go @@ -1,8 +1,6 @@ package syscall -import ( - "syscall/js" -) +import "syscall/js" func runtime_envs() []string { process := js.Global().Get("process") diff --git a/compiler/natives/src/time/time_test.go b/compiler/natives/src/time/time_test.go index 7f161fdac..e980e8aed 100644 --- a/compiler/natives/src/time/time_test.go +++ b/compiler/natives/src/time/time_test.go @@ -2,9 +2,7 @@ package time_test -import ( - "testing" -) +import "testing" func TestSleep(t *testing.T) { t.Skip("time.Now() is not accurate enough for the test") diff --git a/tests/alias_test.go b/tests/alias_test.go index 9e791856e..ecd911d9d 100644 --- a/tests/alias_test.go +++ b/tests/alias_test.go @@ -1,8 +1,6 @@ package tests -import ( - "testing" -) +import "testing" type foo struct { a int diff --git a/tests/compiler_test.go b/tests/compiler_test.go index 7c72e3535..e33d24efe 100644 --- a/tests/compiler_test.go +++ b/tests/compiler_test.go @@ -1,8 +1,6 @@ package tests -import ( - "testing" -) +import "testing" func TestVariadicNil(t *testing.T) { t.Run("only variadic", func(t *testing.T) { From 437f326f1a28d9702fc4b91d4ca5029c9c8408a8 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Wed, 13 Aug 2025 13:01:28 -0600 Subject: [PATCH 55/58] Updating tests and overrides --- .../src/database/sql/driver/driver_test.go | 4 +- compiler/natives/src/encoding/gob/gob_test.go | 4 ++ compiler/natives/src/go/token/token_test.go | 2 +- .../natives/src/hash/maphash/maphash_test.go | 53 +++++++++++++++++++ .../src/internal/reflectlite/all_test.go | 19 ------- .../reflectlite/reflect_mirror_test.go | 9 ---- compiler/natives/src/io/io_test.go | 2 +- compiler/natives/src/math/big/big.go | 8 --- compiler/natives/src/math/big/big_test.go | 8 --- compiler/natives/src/math/math_test.go | 19 ------- compiler/natives/src/math/rand/rand_test.go | 4 -- .../natives/src/net/http/clientserver_test.go | 1 + compiler/natives/src/net/http/server_test.go | 5 ++ compiler/natives/src/runtime/debug/debug.go | 25 +++++++++ .../natives/src/runtime/debug/debug_test.go | 13 +++++ compiler/natives/src/testing/quick/quick.go | 11 ++-- compiler/natives/src/time/time_test.go | 8 +-- .../natives/src/time/zoneinfo_unix_test.go | 9 ++++ 18 files changed, 123 insertions(+), 81 deletions(-) create mode 100644 compiler/natives/src/hash/maphash/maphash_test.go delete mode 100644 compiler/natives/src/internal/reflectlite/reflect_mirror_test.go delete mode 100644 compiler/natives/src/math/big/big.go delete mode 100644 compiler/natives/src/math/math_test.go create mode 100644 compiler/natives/src/runtime/debug/debug_test.go create mode 100644 compiler/natives/src/time/zoneinfo_unix_test.go diff --git a/compiler/natives/src/database/sql/driver/driver_test.go b/compiler/natives/src/database/sql/driver/driver_test.go index d943d5f0c..59ac867ef 100644 --- a/compiler/natives/src/database/sql/driver/driver_test.go +++ b/compiler/natives/src/database/sql/driver/driver_test.go @@ -23,10 +23,10 @@ var valueConverterTests = []valueConverterTest{ {DefaultParameterConverter, (*int64)(nil), nil, ""}, {DefaultParameterConverter, &answer, answer, ""}, {DefaultParameterConverter, &now, now, ""}, - //{DefaultParameterConverter, i(9), int64(9), ""}, // TODO: Fix. + //{DefaultParameterConverter, i(9), int64(9), ""}, // TODO: Fix. Errors with `driver.defaultConverter(driver.i(9)) = 9 (driver.i); want 9 (int64)` {DefaultParameterConverter, f(0.1), float64(0.1), ""}, {DefaultParameterConverter, b(true), true, ""}, - //{DefaultParameterConverter, bs{1}, []byte{1}, ""}, // TODO: Fix. + //{DefaultParameterConverter, bs{1}, []byte{1}, ""}, // TODO: Fix. Errors with `driver.defaultConverter(driver.bs([1])) = [1] (driver.bs); want [1] ([]uint8)` {DefaultParameterConverter, s("a"), "a", ""}, {DefaultParameterConverter, is{1}, nil, "unsupported type driver.is, a slice of int"}, } diff --git a/compiler/natives/src/encoding/gob/gob_test.go b/compiler/natives/src/encoding/gob/gob_test.go index 1678f462f..32cd38bbd 100644 --- a/compiler/natives/src/encoding/gob/gob_test.go +++ b/compiler/natives/src/encoding/gob/gob_test.go @@ -104,3 +104,7 @@ func TestTypeRace(t *testing.T) { // cannot succeed when nosync is used. t.Skip("using nosync") } + +func TestCountDecodeMallocs(t *testing.T) { + t.Skip("testing.AllocsPerRun not supported in GopherJS") +} diff --git a/compiler/natives/src/go/token/token_test.go b/compiler/natives/src/go/token/token_test.go index 983c84dee..9ef38348a 100644 --- a/compiler/natives/src/go/token/token_test.go +++ b/compiler/natives/src/go/token/token_test.go @@ -5,5 +5,5 @@ package token import "testing" func TestFileSetRace(t *testing.T) { - t.Skip() + t.Skip("Fails with: WaitGroup counter not zero") } diff --git a/compiler/natives/src/hash/maphash/maphash_test.go b/compiler/natives/src/hash/maphash/maphash_test.go new file mode 100644 index 000000000..a1e9f45a9 --- /dev/null +++ b/compiler/natives/src/hash/maphash/maphash_test.go @@ -0,0 +1,53 @@ +//go:build js + +package maphash + +import "testing" + +//gopherjs:keep-original +func TestSmhasherSmallKeys(t *testing.T) { + if !testing.Short() { + t.Skip("Causes a heap overflow in GopherJS when not --short") + // This test adds a lot of uint64 hashes into a map, + // (16,843,008 for long tests, 65,792 for short tests) + // inside `(s *hashSet) add(h uint64)` with `s.m[h] = struct{}{}`. + // This is to check the number of collisions in the hash function. + } + _gopherjs_original_TestSmhasherSmallKeys(t) +} + +//gopherjs:keep-original +func TestSmhasherZeros(t *testing.T) { + if !testing.Short() { + t.Skip("Too slow when not --short") + // This test creates a byte slice with 262,144 bytes for long tests + // and 1,024 for short tests filled by defualt with zeroes. + // Then it adds [:1], [:2], and so on upto the full slice. + } + _gopherjs_original_TestSmhasherZeros(t) +} + +func TestSmhasherTwoNonzero(t *testing.T) { + // The original skips if `runtime.GOARCH == "wasm"` which means we should skip too. + t.Skip("Too slow on wasm and JS") +} + +func TestSmhasherSparse(t *testing.T) { + // The original skips if `runtime.GOARCH == "wasm"` which means we should skip too. + t.Skip("Too slow on wasm and JS") +} + +func TestSmhasherPermutation(t *testing.T) { + // The original skips if `runtime.GOARCH == "wasm"` which means we should skip too. + t.Skip("Too slow on wasm and JS") +} + +func TestSmhasherAvalanche(t *testing.T) { + // The original skips if `runtime.GOARCH == "wasm"` which means we should skip too. + t.Skip("Too slow on wasm and JS") +} + +func TestSmhasherWindowed(t *testing.T) { + // The original skips if `runtime.GOARCH == "wasm"` which means we should skip too. + t.Skip("Too slow on wasm and JS") +} diff --git a/compiler/natives/src/internal/reflectlite/all_test.go b/compiler/natives/src/internal/reflectlite/all_test.go index 7ddee2353..66a7add0a 100644 --- a/compiler/natives/src/internal/reflectlite/all_test.go +++ b/compiler/natives/src/internal/reflectlite/all_test.go @@ -2,25 +2,6 @@ package reflectlite_test -import ( - "testing" - - . "internal/reflectlite" -) - -func TestTypes(t *testing.T) { - for i, tt := range typeTests { - if i == 30 { - continue - } - testReflectType(t, i, Field(ValueOf(tt.i), 0).Type(), tt.s) - } -} - -func TestNameBytesAreAligned(t *testing.T) { - t.Skip("TestNameBytesAreAligned") -} - // `A` is used with `B[T any]` and is otherwise not needed. // //gopherjs:purge for go1.19 without generics diff --git a/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go b/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go deleted file mode 100644 index 4ca0c29c6..000000000 --- a/compiler/natives/src/internal/reflectlite/reflect_mirror_test.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build js - -package reflectlite_test - -import "testing" - -func TestMirrorWithReflect(t *testing.T) { - t.Skip("TestMirrorWithReflect") -} diff --git a/compiler/natives/src/io/io_test.go b/compiler/natives/src/io/io_test.go index fd8cdaaec..36d70e3fa 100644 --- a/compiler/natives/src/io/io_test.go +++ b/compiler/natives/src/io/io_test.go @@ -5,7 +5,7 @@ package io_test import "testing" func TestMultiWriter_WriteStringSingleAlloc(t *testing.T) { - t.Skip() + t.Skip("testing.AllocsPerRun not supported in GopherJS") } func TestMultiReaderFreesExhaustedReaders(t *testing.T) { diff --git a/compiler/natives/src/math/big/big.go b/compiler/natives/src/math/big/big.go deleted file mode 100644 index 4c850d165..000000000 --- a/compiler/natives/src/math/big/big.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build js - -package big - -// TODO: This is a workaround for https://github.com/gopherjs/gopherjs/issues/652. -// -// Remove after that issue is resolved. -type Word uintptr diff --git a/compiler/natives/src/math/big/big_test.go b/compiler/natives/src/math/big/big_test.go index ae2dd5916..92989abe9 100644 --- a/compiler/natives/src/math/big/big_test.go +++ b/compiler/natives/src/math/big/big_test.go @@ -4,14 +4,6 @@ package big import "testing" -func TestBytes(t *testing.T) { - t.Skip("broken") -} - -func TestModSqrt(t *testing.T) { - t.Skip("slow") -} - func TestLinkerGC(t *testing.T) { t.Skip("The test is specific to GC's linker.") } diff --git a/compiler/natives/src/math/math_test.go b/compiler/natives/src/math/math_test.go deleted file mode 100644 index de0f7fd8d..000000000 --- a/compiler/natives/src/math/math_test.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build js - -package math_test - -import "testing" - -// Slightly higher tolerances than upstream, otherwise TestGamma fails. -// TODO: Is there a better way to fix TestGamma? It's weird that only one test -// -// requires increasing tolerances. Perhaps there's a better fix? Maybe we -// should override TestGamma specifically and not the package-wide tolerances, -// because this will cause many other tests to be less accurate. Or maybe this -// is fine? -func close(a, b float64) bool { return tolerance(a, b, 4e-14) } -func veryclose(a, b float64) bool { return tolerance(a, b, 6e-15) } - -func testExp(t *testing.T, Exp func(float64) float64, name string) { - t.Skip("inaccurate") -} diff --git a/compiler/natives/src/math/rand/rand_test.go b/compiler/natives/src/math/rand/rand_test.go index e51d3a321..0073850a0 100644 --- a/compiler/natives/src/math/rand/rand_test.go +++ b/compiler/natives/src/math/rand/rand_test.go @@ -4,10 +4,6 @@ package rand_test import "testing" -func TestFloat32(t *testing.T) { - t.Skip("slow") -} - func TestConcurrent(t *testing.T) { t.Skip("using nosync") } diff --git a/compiler/natives/src/net/http/clientserver_test.go b/compiler/natives/src/net/http/clientserver_test.go index fc4486998..a95a5a0ca 100644 --- a/compiler/natives/src/net/http/clientserver_test.go +++ b/compiler/natives/src/net/http/clientserver_test.go @@ -9,5 +9,6 @@ func testTransportGCRequest(t *testing.T, h2, body bool) { } func testWriteHeaderAfterWrite(t *testing.T, h2, hijack bool) { + // See: https://github.com/gopherjs/gopherjs/issues/1085 t.Skip("GopherJS source maps don't preserve original function names in stack traces, which this test relied on.") } diff --git a/compiler/natives/src/net/http/server_test.go b/compiler/natives/src/net/http/server_test.go index 401eeff7c..7223713d9 100644 --- a/compiler/natives/src/net/http/server_test.go +++ b/compiler/natives/src/net/http/server_test.go @@ -7,3 +7,8 @@ import "testing" func TestTimeoutHandlerSuperfluousLogs(t *testing.T) { t.Skip("https://github.com/gopherjs/gopherjs/issues/1085") } + +func TestHTTP2WriteDeadlineExtendedOnNewRequest(t *testing.T) { + // Test depends on httptest.NewUnstartedServer + t.Skip("Network access not supported by GopherJS.") +} diff --git a/compiler/natives/src/runtime/debug/debug.go b/compiler/natives/src/runtime/debug/debug.go index 1d94ab89b..ed9c0e34f 100644 --- a/compiler/natives/src/runtime/debug/debug.go +++ b/compiler/natives/src/runtime/debug/debug.go @@ -2,6 +2,8 @@ package debug +import "time" + func setGCPercent(int32) int32 { // Not implemented. Return initial setting. return 100 @@ -12,3 +14,26 @@ func setMaxStack(bytes int) int { // The initial setting is 1 GB on 64-bit systems, 250 MB on 32-bit systems. return 250000000 } + +func readGCStats(pauses *[]time.Duration) { + // Not implemented. No GC stats available in this environment. +} + +func freeOSMemory() { + // Not implemented. No OS memory management in this environment. +} + +func setPanicOnFault(bool) bool { + // Not implemented. + return true +} + +func setMaxThreads(int) int { + // Not implemented. + return 10000 +} + +func setMemoryLimit(int64) int64 { + // Not implemented. + return 0 +} diff --git a/compiler/natives/src/runtime/debug/debug_test.go b/compiler/natives/src/runtime/debug/debug_test.go new file mode 100644 index 000000000..7c7bf855f --- /dev/null +++ b/compiler/natives/src/runtime/debug/debug_test.go @@ -0,0 +1,13 @@ +//go:build js + +package debug_test + +import "testing" + +func TestReadGCStats(t *testing.T) { + t.Skip(`This test uses runtime.GC(), which GopherJS doesn't support`) +} + +func TestFreeOSMemory(t *testing.T) { + t.Skip(`This test uses runtime.GC(), which GopherJS doesn't support`) +} diff --git a/compiler/natives/src/testing/quick/quick.go b/compiler/natives/src/testing/quick/quick.go index 51fa843aa..bd9bccf3e 100644 --- a/compiler/natives/src/testing/quick/quick.go +++ b/compiler/natives/src/testing/quick/quick.go @@ -13,12 +13,11 @@ var maxCountCap int = 0 // reasonable amount of time. This is a better compromise than disabling a slow // test entirely. // -// //gopherjs:keep-original -// func TestFoo(t *testing.T) { -// t.Cleanup(quick.GopherJSInternalMaxCountCap(100)) -// _gopherjs_original_TestFoo(t) -// } - +// //gopherjs:keep-original +// func TestFoo(t *testing.T) { +// t.Cleanup(quick.GopherJSInternalMaxCountCap(100)) +// _gopherjs_original_TestFoo(t) +// } func GopherJSInternalMaxCountCap(newCap int) (restore func()) { previousCap := maxCountCap maxCountCap = newCap diff --git a/compiler/natives/src/time/time_test.go b/compiler/natives/src/time/time_test.go index e980e8aed..777fe11fe 100644 --- a/compiler/natives/src/time/time_test.go +++ b/compiler/natives/src/time/time_test.go @@ -4,10 +4,10 @@ package time_test import "testing" -func TestSleep(t *testing.T) { - t.Skip("time.Now() is not accurate enough for the test") +func TestZeroTimer(t *testing.T) { + t.Skip(`This test is very slow (about 19 mins)`) } -func TestEnvTZUsage(t *testing.T) { - t.Skip("TZ environment variable in not applicable in the browser context.") +func TestSleep(t *testing.T) { + t.Skip("time.Now() is not accurate enough for the test") } diff --git a/compiler/natives/src/time/zoneinfo_unix_test.go b/compiler/natives/src/time/zoneinfo_unix_test.go new file mode 100644 index 000000000..8cb20416e --- /dev/null +++ b/compiler/natives/src/time/zoneinfo_unix_test.go @@ -0,0 +1,9 @@ +//go:build js && unix && !ios && !android + +package time_test + +import "testing" + +func TestEnvTZUsage(t *testing.T) { + t.Skip("TZ environment variable in not applicable in the browser context.") +} From 9eb1c70bd522baa8cca1fb48e3642f7df03a5512 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Fri, 15 Aug 2025 11:57:36 -0600 Subject: [PATCH 56/58] enable generics and remove overrides for generics --- .github/workflows/ci.yaml | 1 - compiler/internal/typeparams/utils.go | 27 --- compiler/internal/typeparams/utils_test.go | 58 ------ .../natives/src/crypto/elliptic/nistec.go | 80 -------- .../src/crypto/internal/nistec/nistec_test.go | 88 --------- .../src/crypto/internal/nistec/wrapper.go | 184 ------------------ compiler/natives/src/go/doc/doc_test.go | 37 ---- compiler/natives/src/go/token/position.go | 21 -- .../src/internal/reflectlite/all_test.go | 27 --- compiler/natives/src/net/netip/fuzz_test.go | 10 - compiler/natives/src/reflect/reflect_test.go | 10 - compiler/natives/src/sync/atomic/atomic.go | 27 ++- .../natives/src/sync/atomic/atomic_test.go | 49 ----- compiler/natives/src/testing/helper_test.go | 7 - .../natives/src/testing/helperfuncs_test.go | 12 -- compiler/sources/sources.go | 9 - internal/experiments/experiments.go | 2 +- tests/gorepo/run.go | 3 +- 18 files changed, 27 insertions(+), 625 deletions(-) delete mode 100644 compiler/natives/src/crypto/elliptic/nistec.go delete mode 100644 compiler/natives/src/crypto/internal/nistec/nistec_test.go delete mode 100644 compiler/natives/src/crypto/internal/nistec/wrapper.go delete mode 100644 compiler/natives/src/go/doc/doc_test.go delete mode 100644 compiler/natives/src/go/token/position.go delete mode 100644 compiler/natives/src/internal/reflectlite/all_test.go delete mode 100644 compiler/natives/src/net/netip/fuzz_test.go delete mode 100644 compiler/natives/src/testing/helper_test.go delete mode 100644 compiler/natives/src/testing/helperfuncs_test.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 27e65c4da..169b072cf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,7 +18,6 @@ env: GO_VERSION: 1.19.13 NODE_VERSION: 18 GOLANGCI_VERSION: v1.53.3 - GOPHERJS_EXPERIMENT: generics SOURCE_MAP_SUPPORT: true GOPATH: ${{ github.workspace }}/go GOPHERJS_PATH: ${{ github.workspace }}/go/src/github.com/${{ github.repository }} diff --git a/compiler/internal/typeparams/utils.go b/compiler/internal/typeparams/utils.go index 27fab0a89..0c3d1d60d 100644 --- a/compiler/internal/typeparams/utils.go +++ b/compiler/internal/typeparams/utils.go @@ -1,8 +1,6 @@ package typeparams import ( - "errors" - "fmt" "go/token" "go/types" ) @@ -92,11 +90,6 @@ func FindNestingFunc(obj types.Object) *types.Func { return nil } -var ( - errInstantiatesGenerics = errors.New("instantiates generic type or function") - errDefinesGenerics = errors.New("defines generic type or function") -) - // HasTypeParams returns true if object defines type parameters. // // Note: this function doe not check if the object definition actually uses the @@ -112,26 +105,6 @@ func HasTypeParams(typ types.Type) bool { } } -// RequiresGenericsSupport returns an error if the type-checked code depends on -// generics support. -func RequiresGenericsSupport(info *types.Info) error { - for ident := range info.Instances { - // Any instantiation means dependency on generics. - return fmt.Errorf("%w: %v", errInstantiatesGenerics, info.ObjectOf(ident)) - } - - for _, obj := range info.Defs { - if obj == nil { - continue - } - if HasTypeParams(obj.Type()) { - return fmt.Errorf("%w: %v", errDefinesGenerics, obj) - } - } - - return nil -} - // isGeneric will search all the given types in `typ` and their subtypes for a // *types.TypeParam. This will not check if a type could be generic, // but if each instantiation is not completely concrete yet. diff --git a/compiler/internal/typeparams/utils_test.go b/compiler/internal/typeparams/utils_test.go index 1e10e963a..dc8b30414 100644 --- a/compiler/internal/typeparams/utils_test.go +++ b/compiler/internal/typeparams/utils_test.go @@ -1,7 +1,6 @@ package typeparams import ( - "errors" "fmt" "go/ast" "go/token" @@ -65,63 +64,6 @@ func TestHasTypeParams(t *testing.T) { } } -func TestRequiresGenericsSupport(t *testing.T) { - t.Run("generic func", func(t *testing.T) { - f := srctesting.New(t) - src := `package foo - func foo[T any](t T) {}` - info, _ := f.Check("pkg/foo", f.Parse("foo.go", src)) - - err := RequiresGenericsSupport(info) - if !errors.Is(err, errDefinesGenerics) { - t.Errorf("Got: RequiresGenericsSupport() = %v. Want: %v", err, errDefinesGenerics) - } - }) - - t.Run("generic type", func(t *testing.T) { - f := srctesting.New(t) - src := `package foo - type Foo[T any] struct{t T}` - info, _ := f.Check("pkg/foo", f.Parse("foo.go", src)) - - err := RequiresGenericsSupport(info) - if !errors.Is(err, errDefinesGenerics) { - t.Errorf("Got: RequiresGenericsSupport() = %v. Want: %v", err, errDefinesGenerics) - } - }) - - t.Run("imported generic instance", func(t *testing.T) { - f := srctesting.New(t) - f.Info = nil // Do not combine type checking info from different packages. - src1 := `package foo - type Foo[T any] struct{t T}` - f.Check("pkg/foo", f.Parse("foo.go", src1)) - - src2 := `package bar - import "pkg/foo" - func bar() { _ = foo.Foo[int]{} }` - info, _ := f.Check("pkg/bar", f.Parse("bar.go", src2)) - - err := RequiresGenericsSupport(info) - if !errors.Is(err, errInstantiatesGenerics) { - t.Errorf("Got: RequiresGenericsSupport() = %v. Want: %v", err, errInstantiatesGenerics) - } - }) - - t.Run("no generic usage", func(t *testing.T) { - f := srctesting.New(t) - src := `package foo - type Foo struct{} - func foo() { _ = Foo{} }` - info, _ := f.Check("pkg/foo", f.Parse("foo.go", src)) - - err := RequiresGenericsSupport(info) - if err != nil { - t.Errorf("Got: RequiresGenericsSupport() = %v. Want: nil", err) - } - }) -} - func Test_FindNestingFunc(t *testing.T) { src := `package main diff --git a/compiler/natives/src/crypto/elliptic/nistec.go b/compiler/natives/src/crypto/elliptic/nistec.go deleted file mode 100644 index b2cc45e9d..000000000 --- a/compiler/natives/src/crypto/elliptic/nistec.go +++ /dev/null @@ -1,80 +0,0 @@ -//go:build js - -package elliptic - -import ( - "crypto/internal/nistec" - "math/big" -) - -// nistPoint uses generics so must be removed for generic-less GopherJS. -// All the following code changes in this file are to make p224, p256, -// p521, and p384 still function correctly without this generic struct. -// -//gopherjs:purge for go1.19 without generics -type nistPoint[T any] interface{} - -// nistCurve replaces the generics with a version using the wrappedPoint -// interface, then update all the method signatures to also use wrappedPoint. -type nistCurve struct { - newPoint func() nistec.WrappedPoint - params *CurveParams -} - -//gopherjs:override-signature -func (curve *nistCurve) Params() *CurveParams - -//gopherjs:override-signature -func (curve *nistCurve) IsOnCurve(x, y *big.Int) bool - -//gopherjs:override-signature -func (curve *nistCurve) pointFromAffine(x, y *big.Int) (p nistec.WrappedPoint, err error) - -//gopherjs:override-signature -func (curve *nistCurve) pointToAffine(p nistec.WrappedPoint) (x, y *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) Add(x1, y1, x2, y2 *big.Int) (*big.Int, *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) Double(x1, y1 *big.Int) (*big.Int, *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) normalizeScalar(scalar []byte) []byte - -//gopherjs:override-signature -func (curve *nistCurve) ScalarMult(Bx, By *big.Int, scalar []byte) (*big.Int, *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) ScalarBaseMult(scalar []byte) (*big.Int, *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) CombinedMult(Px, Py *big.Int, s1, s2 []byte) (x, y *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) Unmarshal(data []byte) (x, y *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) UnmarshalCompressed(data []byte) (x, y *big.Int) - -var p224 = &nistCurve{ - newPoint: nistec.NewP224WrappedPoint, -} - -type p256Curve struct { - nistCurve -} - -var p256 = &p256Curve{ - nistCurve: nistCurve{ - newPoint: nistec.NewP256WrappedPoint, - }, -} - -var p521 = &nistCurve{ - newPoint: nistec.NewP521WrappedPoint, -} - -var p384 = &nistCurve{ - newPoint: nistec.NewP384WrappedPoint, -} diff --git a/compiler/natives/src/crypto/internal/nistec/nistec_test.go b/compiler/natives/src/crypto/internal/nistec/nistec_test.go deleted file mode 100644 index 4da8ba9e3..000000000 --- a/compiler/natives/src/crypto/internal/nistec/nistec_test.go +++ /dev/null @@ -1,88 +0,0 @@ -//go:build js - -package nistec_test - -import ( - "crypto/elliptic" - "crypto/internal/nistec" - "testing" -) - -func TestAllocations(t *testing.T) { - t.Skip("testing.AllocsPerRun not supported in GopherJS") -} - -//gopherjs:purge -type nistPoint[T any] interface{} - -func TestEquivalents(t *testing.T) { - t.Run("P224", func(t *testing.T) { - testEquivalents(t, nistec.NewP224WrappedPoint, nistec.NewP224WrappedGenerator, elliptic.P224()) - }) - t.Run("P256", func(t *testing.T) { - testEquivalents(t, nistec.NewP256WrappedPoint, nistec.NewP256WrappedGenerator, elliptic.P256()) - }) - t.Run("P384", func(t *testing.T) { - testEquivalents(t, nistec.NewP384WrappedPoint, nistec.NewP384WrappedGenerator, elliptic.P384()) - }) - t.Run("P521", func(t *testing.T) { - testEquivalents(t, nistec.NewP521WrappedPoint, nistec.NewP521WrappedGenerator, elliptic.P521()) - }) -} - -//gopherjs:override-signature -func testEquivalents(t *testing.T, newPoint, newGenerator func() nistec.WrappedPoint, c elliptic.Curve) - -func TestScalarMult(t *testing.T) { - t.Run("P224", func(t *testing.T) { - testScalarMult(t, nistec.NewP224WrappedPoint, nistec.NewP224WrappedGenerator, elliptic.P224()) - }) - t.Run("P256", func(t *testing.T) { - testScalarMult(t, nistec.NewP256WrappedPoint, nistec.NewP256WrappedGenerator, elliptic.P256()) - }) - t.Run("P384", func(t *testing.T) { - testScalarMult(t, nistec.NewP384WrappedPoint, nistec.NewP384WrappedGenerator, elliptic.P384()) - }) - t.Run("P521", func(t *testing.T) { - testScalarMult(t, nistec.NewP521WrappedPoint, nistec.NewP521WrappedGenerator, elliptic.P521()) - }) -} - -//gopherjs:override-signature -func testScalarMult(t *testing.T, newPoint, newGenerator func() nistec.WrappedPoint, c elliptic.Curve) - -func BenchmarkScalarMult(b *testing.B) { - b.Run("P224", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP224WrappedGenerator(), 28) - }) - b.Run("P256", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP256WrappedGenerator(), 32) - }) - b.Run("P384", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP384WrappedGenerator(), 48) - }) - b.Run("P521", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP521WrappedGenerator(), 66) - }) -} - -//gopherjs:override-signature -func benchmarkScalarMult(b *testing.B, p nistec.WrappedPoint, scalarSize int) - -func BenchmarkScalarBaseMult(b *testing.B) { - b.Run("P224", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP224WrappedGenerator(), 28) - }) - b.Run("P256", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP256WrappedGenerator(), 32) - }) - b.Run("P384", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP384WrappedGenerator(), 48) - }) - b.Run("P521", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP521WrappedGenerator(), 66) - }) -} - -//gopherjs:override-signature -func benchmarkScalarBaseMult(b *testing.B, p nistec.WrappedPoint, scalarSize int) diff --git a/compiler/natives/src/crypto/internal/nistec/wrapper.go b/compiler/natives/src/crypto/internal/nistec/wrapper.go deleted file mode 100644 index a55aa4925..000000000 --- a/compiler/natives/src/crypto/internal/nistec/wrapper.go +++ /dev/null @@ -1,184 +0,0 @@ -//go:build js - -package nistec - -type WrappedPoint interface { - Bytes() []byte - SetBytes(b []byte) (WrappedPoint, error) - Add(w1, w2 WrappedPoint) WrappedPoint - Double(w1 WrappedPoint) WrappedPoint - ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) - ScalarBaseMult(scalar []byte) (WrappedPoint, error) -} - -type p224Wrapper struct { - point *P224Point -} - -func wrapP224(point *P224Point) WrappedPoint { - return p224Wrapper{point: point} -} - -func NewP224WrappedPoint() WrappedPoint { - return wrapP224(NewP224Point()) -} - -func NewP224WrappedGenerator() WrappedPoint { - return wrapP224(NewP224Generator()) -} - -func (w p224Wrapper) Bytes() []byte { - return w.point.Bytes() -} - -func (w p224Wrapper) SetBytes(b []byte) (WrappedPoint, error) { - p, err := w.point.SetBytes(b) - return wrapP224(p), err -} - -func (w p224Wrapper) Add(w1, w2 WrappedPoint) WrappedPoint { - return wrapP224(w.point.Add(w1.(p224Wrapper).point, w2.(p224Wrapper).point)) -} - -func (w p224Wrapper) Double(w1 WrappedPoint) WrappedPoint { - return wrapP224(w.point.Double(w1.(p224Wrapper).point)) -} - -func (w p224Wrapper) ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarMult(w1.(p224Wrapper).point, scalar) - return wrapP224(p), err -} - -func (w p224Wrapper) ScalarBaseMult(scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarBaseMult(scalar) - return wrapP224(p), err -} - -type p256Wrapper struct { - point *P256Point -} - -func wrapP256(point *P256Point) WrappedPoint { - return p256Wrapper{point: point} -} - -func NewP256WrappedPoint() WrappedPoint { - return wrapP256(NewP256Point()) -} - -func NewP256WrappedGenerator() WrappedPoint { - return wrapP256(NewP256Generator()) -} - -func (w p256Wrapper) Bytes() []byte { - return w.point.Bytes() -} - -func (w p256Wrapper) SetBytes(b []byte) (WrappedPoint, error) { - p, err := w.point.SetBytes(b) - return wrapP256(p), err -} - -func (w p256Wrapper) Add(w1, w2 WrappedPoint) WrappedPoint { - return wrapP256(w.point.Add(w1.(p256Wrapper).point, w2.(p256Wrapper).point)) -} - -func (w p256Wrapper) Double(w1 WrappedPoint) WrappedPoint { - return wrapP256(w.point.Double(w1.(p256Wrapper).point)) -} - -func (w p256Wrapper) ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarMult(w1.(p256Wrapper).point, scalar) - return wrapP256(p), err -} - -func (w p256Wrapper) ScalarBaseMult(scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarBaseMult(scalar) - return wrapP256(p), err -} - -type p521Wrapper struct { - point *P521Point -} - -func wrapP521(point *P521Point) WrappedPoint { - return p521Wrapper{point: point} -} - -func NewP521WrappedPoint() WrappedPoint { - return wrapP521(NewP521Point()) -} - -func NewP521WrappedGenerator() WrappedPoint { - return wrapP521(NewP521Generator()) -} - -func (w p521Wrapper) Bytes() []byte { - return w.point.Bytes() -} - -func (w p521Wrapper) SetBytes(b []byte) (WrappedPoint, error) { - p, err := w.point.SetBytes(b) - return wrapP521(p), err -} - -func (w p521Wrapper) Add(w1, w2 WrappedPoint) WrappedPoint { - return wrapP521(w.point.Add(w1.(p521Wrapper).point, w2.(p521Wrapper).point)) -} - -func (w p521Wrapper) Double(w1 WrappedPoint) WrappedPoint { - return wrapP521(w.point.Double(w1.(p521Wrapper).point)) -} - -func (w p521Wrapper) ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarMult(w1.(p521Wrapper).point, scalar) - return wrapP521(p), err -} - -func (w p521Wrapper) ScalarBaseMult(scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarBaseMult(scalar) - return wrapP521(p), err -} - -type p384Wrapper struct { - point *P384Point -} - -func wrapP384(point *P384Point) WrappedPoint { - return p384Wrapper{point: point} -} - -func NewP384WrappedPoint() WrappedPoint { - return wrapP384(NewP384Point()) -} - -func NewP384WrappedGenerator() WrappedPoint { - return wrapP384(NewP384Generator()) -} - -func (w p384Wrapper) Bytes() []byte { - return w.point.Bytes() -} - -func (w p384Wrapper) SetBytes(b []byte) (WrappedPoint, error) { - p, err := w.point.SetBytes(b) - return wrapP384(p), err -} - -func (w p384Wrapper) Add(w1, w2 WrappedPoint) WrappedPoint { - return wrapP384(w.point.Add(w1.(p384Wrapper).point, w2.(p384Wrapper).point)) -} - -func (w p384Wrapper) Double(w1 WrappedPoint) WrappedPoint { - return wrapP384(w.point.Double(w1.(p384Wrapper).point)) -} - -func (w p384Wrapper) ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarMult(w1.(p384Wrapper).point, scalar) - return wrapP384(p), err -} - -func (w p384Wrapper) ScalarBaseMult(scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarBaseMult(scalar) - return wrapP384(p), err -} diff --git a/compiler/natives/src/go/doc/doc_test.go b/compiler/natives/src/go/doc/doc_test.go deleted file mode 100644 index 4d35e880c..000000000 --- a/compiler/natives/src/go/doc/doc_test.go +++ /dev/null @@ -1,37 +0,0 @@ -//go:build js - -package doc - -import ( - "fmt" - "testing" -) - -func compareSlices(t *testing.T, name string, got, want interface{}, compareElem interface{}) { - // TODO(nevkontakte): Remove this override after generics are supported. - // https://github.com/gopherjs/gopherjs/issues/1013. - switch got.(type) { - case []*Func: - got := got.([]*Func) - want := want.([]*Func) - compareElem := compareElem.(func(t *testing.T, msg string, got, want *Func)) - if len(got) != len(want) { - t.Errorf("%s: got %d, want %d", name, len(got), len(want)) - } - for i := 0; i < len(got) && i < len(want); i++ { - compareElem(t, fmt.Sprintf("%s[%d]", name, i), got[i], want[i]) - } - case []*Type: - got := got.([]*Type) - want := want.([]*Type) - compareElem := compareElem.(func(t *testing.T, msg string, got, want *Type)) - if len(got) != len(want) { - t.Errorf("%s: got %d, want %d", name, len(got), len(want)) - } - for i := 0; i < len(got) && i < len(want); i++ { - compareElem(t, fmt.Sprintf("%s[%d]", name, i), got[i], want[i]) - } - default: - t.Errorf("unexpected argument type %T", got) - } -} diff --git a/compiler/natives/src/go/token/position.go b/compiler/natives/src/go/token/position.go deleted file mode 100644 index 8cf111ab2..000000000 --- a/compiler/natives/src/go/token/position.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build js - -package token - -import "sync" - -type FileSet struct { - mutex sync.RWMutex - base int - files []*File - - // replaced atomic.Pointer[File] for go1.19 without generics. - last atomicFilePointer -} - -type atomicFilePointer struct { - v *File -} - -func (x *atomicFilePointer) Load() *File { return x.v } -func (x *atomicFilePointer) Store(val *File) { x.v = val } diff --git a/compiler/natives/src/internal/reflectlite/all_test.go b/compiler/natives/src/internal/reflectlite/all_test.go deleted file mode 100644 index 66a7add0a..000000000 --- a/compiler/natives/src/internal/reflectlite/all_test.go +++ /dev/null @@ -1,27 +0,0 @@ -//go:build js - -package reflectlite_test - -// `A` is used with `B[T any]` and is otherwise not needed. -// -//gopherjs:purge for go1.19 without generics -type ( - A struct{} - B[T any] struct{} -) - -// removing the name tests using `B[T any]` for go1.19 without generics -var nameTests = []nameTest{ - {(*int32)(nil), "int32"}, - {(*D1)(nil), "D1"}, - {(*[]D1)(nil), ""}, - {(*chan D1)(nil), ""}, - {(*func() D1)(nil), ""}, - {(*<-chan D1)(nil), ""}, - {(*chan<- D1)(nil), ""}, - {(*any)(nil), ""}, - {(*interface { - F() - })(nil), ""}, - {(*TheNameOfThisTypeIsExactly255BytesLongSoWhenTheCompilerPrependsTheReflectTestPackageNameAndExtraStarTheLinkerRuntimeAndReflectPackagesWillHaveToCorrectlyDecodeTheSecondLengthByte0123456789_0123456789_0123456789_0123456789_0123456789_012345678)(nil), "TheNameOfThisTypeIsExactly255BytesLongSoWhenTheCompilerPrependsTheReflectTestPackageNameAndExtraStarTheLinkerRuntimeAndReflectPackagesWillHaveToCorrectlyDecodeTheSecondLengthByte0123456789_0123456789_0123456789_0123456789_0123456789_012345678"}, -} diff --git a/compiler/natives/src/net/netip/fuzz_test.go b/compiler/natives/src/net/netip/fuzz_test.go deleted file mode 100644 index 574201d2d..000000000 --- a/compiler/natives/src/net/netip/fuzz_test.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build js - -package netip_test - -import "testing" - -func checkStringParseRoundTrip(t *testing.T, x interface{}, parse interface{}) { - // TODO(nevkontakte): This function requires generics to function. - // Re-enable after https://github.com/gopherjs/gopherjs/issues/1013 is resolved. -} diff --git a/compiler/natives/src/reflect/reflect_test.go b/compiler/natives/src/reflect/reflect_test.go index 9fb413c65..3dab36a06 100644 --- a/compiler/natives/src/reflect/reflect_test.go +++ b/compiler/natives/src/reflect/reflect_test.go @@ -284,16 +284,6 @@ func TestMethodCallValueCodePtr(t *testing.T) { t.Skip("methodValueCallCodePtr() is not applicable in GopherJS") } -//gopherjs:purge for go1.19 without generics -type ( - A struct{} - B[T any] struct{} -) - -func TestIssue50208(t *testing.T) { - t.Skip("This test required generics, which are not yet supported: https://github.com/gopherjs/gopherjs/issues/1013") -} - 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.") } diff --git a/compiler/natives/src/sync/atomic/atomic.go b/compiler/natives/src/sync/atomic/atomic.go index 99aa8559f..900f55dea 100644 --- a/compiler/natives/src/sync/atomic/atomic.go +++ b/compiler/natives/src/sync/atomic/atomic.go @@ -220,5 +220,28 @@ func sameType(x, y interface{}) bool { return js.InternalObject(x).Get("constructor") == js.InternalObject(y).Get("constructor") } -//gopherjs:purge for go1.19 without generics -type Pointer[T any] struct{} +type Pointer[T any] struct { + v *T +} + +func (x *Pointer[T]) Load() *T { + return x.v +} + +func (x *Pointer[T]) Store(val *T) { + x.v = val +} + +func (x *Pointer[T]) Swap(new *T) (old *T) { + old = x.v + x.v = new + return old +} + +func (x *Pointer[T]) CompareAndSwap(old, new *T) bool { + if x.v == old { + x.v = new + return true + } + return false +} diff --git a/compiler/natives/src/sync/atomic/atomic_test.go b/compiler/natives/src/sync/atomic/atomic_test.go index 8dbd0d027..ee5a10be0 100644 --- a/compiler/natives/src/sync/atomic/atomic_test.go +++ b/compiler/natives/src/sync/atomic/atomic_test.go @@ -4,50 +4,8 @@ package atomic_test import ( "testing" - "unsafe" ) -//gopherjs:purge for go1.19 without generics -func testPointers() []unsafe.Pointer {} - -func TestSwapPointer(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestSwapPointerMethod(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestCompareAndSwapPointer(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestCompareAndSwapPointerMethod(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestLoadPointer(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestLoadPointerMethod(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestStorePointer(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestStorePointerMethod(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -//gopherjs:purge for go1.19 without generics -func hammerStoreLoadPointer(t *testing.T, paddr unsafe.Pointer) {} - -//gopherjs:purge for go1.19 without generics -func hammerStoreLoadPointerMethod(t *testing.T, paddr unsafe.Pointer) {} - func TestHammerStoreLoad(t *testing.T) { t.Skip("use of unsafe") } @@ -60,13 +18,6 @@ func TestAutoAligned64(t *testing.T) { t.Skip("GopherJS emulates atomics, which makes alignment irrelevant.") } -func TestNilDeref(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -//gopherjs:purge for go1.19 without generics -type List struct{} - func TestHammer32(t *testing.T) { t.Skip("use of unsafe") } diff --git a/compiler/natives/src/testing/helper_test.go b/compiler/natives/src/testing/helper_test.go deleted file mode 100644 index 3615da315..000000000 --- a/compiler/natives/src/testing/helper_test.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build js - -package testing - -func TestTBHelper(t *T) { - t.Skip("GopherJS does not support generics yet.") -} diff --git a/compiler/natives/src/testing/helperfuncs_test.go b/compiler/natives/src/testing/helperfuncs_test.go deleted file mode 100644 index 560c0466a..000000000 --- a/compiler/natives/src/testing/helperfuncs_test.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build js - -package testing - -//gopherjs:purge for go1.19 without generics -func genericHelper[G any](t *T, msg string) - -//gopherjs:purge for go1.19 without generics -var genericIntHelper = genericHelper[int] - -//gopherjs:purge for go1.19 without generics (uses genericHelper) -func testHelper(t *T) diff --git a/compiler/sources/sources.go b/compiler/sources/sources.go index 1255c1926..5dcf9e2a5 100644 --- a/compiler/sources/sources.go +++ b/compiler/sources/sources.go @@ -1,7 +1,6 @@ package sources import ( - "fmt" "go/ast" "go/token" "go/types" @@ -15,7 +14,6 @@ import ( "github.com/gopherjs/gopherjs/compiler/jsFile" "github.com/gopherjs/gopherjs/compiler/linkname" "github.com/gopherjs/gopherjs/internal/errorList" - "github.com/gopherjs/gopherjs/internal/experiments" ) // Sources is a slice of parsed Go sources and additional data for a package. @@ -147,13 +145,6 @@ func (s *Sources) TypeCheck(importer Importer, sizes types.Sizes, tContext *type return err } - // If generics are not enabled, ensure the package does not requires generics support. - if !experiments.Env.Generics { - if genErr := typeparams.RequiresGenericsSupport(typesInfo); genErr != nil { - return fmt.Errorf("some packages requires generics support (https://github.com/gopherjs/gopherjs/issues/1013): %w", genErr) - } - } - s.baseInfo = typesInfo s.Package = typesPkg return nil diff --git a/internal/experiments/experiments.go b/internal/experiments/experiments.go index 85abce562..fb81a2285 100644 --- a/internal/experiments/experiments.go +++ b/internal/experiments/experiments.go @@ -35,7 +35,7 @@ func init() { // Flags contains flags for currently supported experiments. type Flags struct { - Generics bool `flag:"generics"` + // e.g. Generics bool `flag:"generics"` } // parseFlags parses the `raw` flags string and populates flag values in the diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 0cb3b8143..71b67559c 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -157,7 +157,7 @@ var knownFails = map[string]failReason{ // `4,7: main.T·2[int;main.U·3[int;int]]` will currently output as `4,7: main.T[int;main.U[int]]` // in GopherJS because we doesn't currently add the `·2` and `·3` indicators to the type names // and the nested type arguments to deep nested type, e.g. `U[int;int]` is printed as `U[int]`. - "typeparam/nested.go": {category: usesUnsupportedGenerics, desc: "incomplete support for nested type numbering"}, + "typeparam/nested.go": {category: other, desc: "incomplete support for nested type numbering"}, // These are new tests in Go 1.19 "typeparam/issue51521.go": {category: lowLevelRuntimeDifference, desc: "different panic message when calling a method on nil interface"}, @@ -172,7 +172,6 @@ const ( neverTerminates // Test never terminates (so avoid starting it). usesUnsupportedPackage // Test fails because it imports an unsupported package, e.g., "unsafe". requiresSourceMapSupport // Test fails without source map support (as configured in CI), because it tries to check filename/line number via runtime.Caller. - usesUnsupportedGenerics // Test uses generics (type parameters) that are not currently supported. compilerPanic unsureIfGopherJSSupportsThisFeature lowLevelRuntimeDifference // JavaScript runtime behaves differently from Go in ways that are difficult to work around. From 99b9ba601089c9417bfff6b4a1f3f45ec9be3dfe Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Fri, 15 Aug 2025 15:13:44 -0600 Subject: [PATCH 57/58] add complex integration test --- tests/gencircle_test.go | 2 + tests/testdata/gencircle/trammel/cmp/cmp.go | 10 +++++ .../trammel/collections/collections.go | 31 +++++++++++++ tests/testdata/gencircle/trammel/main.go | 38 ++++++++++++++++ tests/testdata/gencircle/trammel/main.out | 2 + .../testdata/gencircle/trammel/sorts/sorts.go | 30 +++++++++++++ .../gencircle/trammel/stable/mapString.go | 43 +++++++++++++++++++ 7 files changed, 156 insertions(+) create mode 100644 tests/testdata/gencircle/trammel/cmp/cmp.go create mode 100644 tests/testdata/gencircle/trammel/collections/collections.go create mode 100644 tests/testdata/gencircle/trammel/main.go create mode 100644 tests/testdata/gencircle/trammel/main.out create mode 100644 tests/testdata/gencircle/trammel/sorts/sorts.go create mode 100644 tests/testdata/gencircle/trammel/stable/mapString.go diff --git a/tests/gencircle_test.go b/tests/gencircle_test.go index 0915e4c18..d70bc1bc6 100644 --- a/tests/gencircle_test.go +++ b/tests/gencircle_test.go @@ -20,6 +20,8 @@ func Test_GenCircle_Burninate(t *testing.T) { runGenCircleTest(t, `burninate`) } func Test_GenCircle_CatBox(t *testing.T) { runGenCircleTest(t, `catbox`) } +func Test_GenCircle_Trammel(t *testing.T) { runGenCircleTest(t, `trammel`) } + // Cache buster: Keeping the tests from using cached results when only // the test application files are changed. // diff --git a/tests/testdata/gencircle/trammel/cmp/cmp.go b/tests/testdata/gencircle/trammel/cmp/cmp.go new file mode 100644 index 000000000..873dbb51e --- /dev/null +++ b/tests/testdata/gencircle/trammel/cmp/cmp.go @@ -0,0 +1,10 @@ +package cmp + +// Barrowed from the cmp package in the Go standard library +// that isn't available until go1.21. +type Ordered interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | + ~float32 | ~float64 | + ~string +} diff --git a/tests/testdata/gencircle/trammel/collections/collections.go b/tests/testdata/gencircle/trammel/collections/collections.go new file mode 100644 index 000000000..f3f33f0e7 --- /dev/null +++ b/tests/testdata/gencircle/trammel/collections/collections.go @@ -0,0 +1,31 @@ +package collections + +func Populate[K comparable, V any, SK ~[]K, SV ~[]V, M ~map[K]V](m M, keys SK, values SV) { + // Lots of type parameters with parameters referencing each other. + for i, k := range keys { + if i < len(values) { + m[k] = values[i] + } else { + var zero V + m[k] = zero + } + } +} + +func KeysAndValues[K comparable, V any, M ~map[K]V](m M) struct { + Keys []K + Values []V +} { + keys := make([]K, 0, len(m)) + values := make([]V, 0, len(m)) + for k, v := range m { + keys = append(keys, k) + values = append(values, v) + } + // nested generic type that has a type parameter and nest type parameter. + type result[T any] struct { + Keys []T + Values []V + } + return result[K]{Keys: keys, Values: values} +} diff --git a/tests/testdata/gencircle/trammel/main.go b/tests/testdata/gencircle/trammel/main.go new file mode 100644 index 000000000..ce93763c8 --- /dev/null +++ b/tests/testdata/gencircle/trammel/main.go @@ -0,0 +1,38 @@ +// This is a test of several different kinds of generics. +// It is purposefully overly complecated for testing purposes. +// This integration test is similar to `compiler.Test_CrossPackageAnalysis`. + +package main + +import ( + "fmt" + + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/cmp" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/collections" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/stable" +) + +type StableMap[K cmp.Ordered, V any] map[K]V + +func (m StableMap[K, V]) String() string { + return stable.MapString(m, func(k K, v V) string { + return fmt.Sprintf(`%v: %v`, k, v) + }) +} + +type SIMap = StableMap[string, int] +type ISMap = StableMap[int, string] + +func main() { + m1 := SIMap{} + collections.Populate(m1, + []string{"one", "two", "three"}, + []int{1, 2, 3}) + println(m1.String()) + + m2 := ISMap{} + collections.Populate(m2, + []int{4, 5, 6}, + []string{"four", "five", "six"}) + println(m2.String()) +} diff --git a/tests/testdata/gencircle/trammel/main.out b/tests/testdata/gencircle/trammel/main.out new file mode 100644 index 000000000..7a0ccfbda --- /dev/null +++ b/tests/testdata/gencircle/trammel/main.out @@ -0,0 +1,2 @@ +{one: 1, three: 3, two: 2} +{4: four, 5: five, 6: six} diff --git a/tests/testdata/gencircle/trammel/sorts/sorts.go b/tests/testdata/gencircle/trammel/sorts/sorts.go new file mode 100644 index 000000000..d43550cd1 --- /dev/null +++ b/tests/testdata/gencircle/trammel/sorts/sorts.go @@ -0,0 +1,30 @@ +package sorts + +import "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/cmp" + +func Pair[K cmp.Ordered, V any, SK ~[]K, SV ~[]V](k SK, v SV) { + Bubble(struct { // non-generic struct in a generic context. + len func() int + less func(i, j int) bool + swap func(i, j int) + }{ + len: func() int { return len(k) }, + less: func(i, j int) bool { return k[i] < k[j] }, + swap: func(i, j int) { k[i], v[i], k[j], v[j] = k[j], v[j], k[i], v[i] }, + }) +} + +func Bubble(f struct { + len func() int + less func(i, j int) bool + swap func(i, j int) +}) { + length := f.len() + for i := 0; i < length; i++ { + for j := i + 1; j < length; j++ { + if f.less(j, i) { + f.swap(i, j) + } + } + } +} diff --git a/tests/testdata/gencircle/trammel/stable/mapString.go b/tests/testdata/gencircle/trammel/stable/mapString.go new file mode 100644 index 000000000..7204e1f3b --- /dev/null +++ b/tests/testdata/gencircle/trammel/stable/mapString.go @@ -0,0 +1,43 @@ +package stable + +import ( + "strings" + + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/cmp" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/collections" + "github.com/gopherjs/gopherjs/tests/testdata/gencircle/trammel/sorts" +) + +func MapString[K cmp.Ordered, V any, M ~map[K]V](m M, stringer func(K, V) string) string { + // Function parameter with type parameters arguments. + result := collections.KeysAndValues(m) + keys := result.Keys + values := result.Values + + sorts.Pair(keys, values) + + parts := zipper(keys, values, stringer) + return `{` + strings.Join(parts, `, `) + `}` +} + +func zipper[TIn1, TIn2, TOut any, SIn1 ~[]TIn1, SIn2 ~[]TIn2, F ~func(TIn1, TIn2) TOut](s1 SIn1, s2 SIn2, merge F) []TOut { + // Overly complex type parameters including a generic function type. + min := len(s1) + if len(s2) < min { + min = len(s2) + } + result := make([]any, min) + for i := 0; i < min; i++ { + result[i] = merge(s1[i], s2[i]) + } + return castSlice[any, TOut](result) +} + +func castSlice[TIn, TOut any, SIn ~[]TIn, SOut []TOut](s SIn) SOut { + result := make(SOut, len(s)) + for i, v := range s { + // Using a type parameter to cast the type. + result[i] = any(v).(TOut) + } + return result +} From bca215b86c4a0aca1b8c4e7835fbae86603cb0ec Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Mon, 18 Aug 2025 13:13:09 -0600 Subject: [PATCH 58/58] Fixing merge conflicts --- compiler/linkname/linkname_test.go | 2 +- compiler/natives/src/crypto/ecdh/nist.go | 58 ----------- compiler/natives/src/crypto/ecdsa/ecdsa.go | 98 ------------------- .../natives/src/crypto/ecdsa/ecdsa_test.go | 12 --- .../natives/src/crypto/internal/boring/aes.go | 1 - compiler/natives/src/crypto/subtle/xor.go | 3 +- compiler/natives/src/encoding/gob/gob.go | 39 -------- .../coverage/slicereader/slicereader.go | 1 - .../natives/src/internal/godebug/godebug.go | 81 +-------------- .../src/internal/unsafeheader/unsafeheader.go | 1 - compiler/natives/src/math/rand/rand.go | 1 - compiler/natives/src/net/fd_unix.go | 1 - compiler/natives/src/net/http/http.go | 32 ------ compiler/natives/src/reflect/reflect.go | 8 +- compiler/natives/src/reflect/reflect_test.go | 4 + compiler/natives/src/sync/map.go | 48 --------- compiler/natives/src/time/export_test.go | 9 -- compiler/natives/src/time/format.go | 79 --------------- compiler/natives/src/time/format_rfc3339.go | 85 ---------------- 19 files changed, 14 insertions(+), 549 deletions(-) delete mode 100644 compiler/natives/src/crypto/ecdh/nist.go delete mode 100644 compiler/natives/src/crypto/ecdsa/ecdsa.go delete mode 100644 compiler/natives/src/crypto/ecdsa/ecdsa_test.go delete mode 100644 compiler/natives/src/encoding/gob/gob.go delete mode 100644 compiler/natives/src/sync/map.go delete mode 100644 compiler/natives/src/time/export_test.go delete mode 100644 compiler/natives/src/time/format.go delete mode 100644 compiler/natives/src/time/format_rfc3339.go diff --git a/compiler/linkname/linkname_test.go b/compiler/linkname/linkname_test.go index b2ee32998..0f1299407 100644 --- a/compiler/linkname/linkname_test.go +++ b/compiler/linkname/linkname_test.go @@ -220,7 +220,7 @@ func TestParseGoLinknames(t *testing.T) { if len(test.pkgPath) > 0 { pkgPath = test.pkgPath } - directives, err := parseGoLinknames(fset, pkgPath, file) + directives, err := ParseGoLinknames(fset, pkgPath, file) if test.wantError != "" { if err == nil { diff --git a/compiler/natives/src/crypto/ecdh/nist.go b/compiler/natives/src/crypto/ecdh/nist.go deleted file mode 100644 index ecaa84d76..000000000 --- a/compiler/natives/src/crypto/ecdh/nist.go +++ /dev/null @@ -1,58 +0,0 @@ -//go:build js -// +build js - -package ecdh - -import ( - "crypto/internal/nistec" - "io" -) - -//gopherjs:purge for go1.20 without generics -type nistPoint[T any] interface{} - -// temporarily replacement of `nistCurve[Point nistPoint[Point]]` for go1.20 without generics. -type nistCurve struct { - name string - newPoint func() nistec.WrappedPoint - scalarOrder []byte -} - -//gopherjs:override-signature -func (c *nistCurve) String() string - -//gopherjs:override-signature -func (c *nistCurve) GenerateKey(rand io.Reader) (*PrivateKey, error) - -//gopherjs:override-signature -func (c *nistCurve) NewPrivateKey(key []byte) (*PrivateKey, error) - -//gopherjs:override-signature -func (c *nistCurve) privateKeyToPublicKey(key *PrivateKey) *PublicKey - -//gopherjs:override-signature -func (c *nistCurve) NewPublicKey(key []byte) (*PublicKey, error) - -//gopherjs:override-signature -func (c *nistCurve) ecdh(local *PrivateKey, remote *PublicKey) ([]byte, error) - -// temporarily replacement for go1.20 without generics. -var p256 = &nistCurve{ - name: "P-256", - newPoint: nistec.NewP256WrappedPoint, - scalarOrder: p256Order, -} - -// temporarily replacement for go1.20 without generics. -var p384 = &nistCurve{ - name: "P-384", - newPoint: nistec.NewP384WrappedPoint, - scalarOrder: p384Order, -} - -// temporarily replacement for go1.20 without generics. -var p521 = &nistCurve{ - name: "P-521", - newPoint: nistec.NewP521WrappedPoint, - scalarOrder: p521Order, -} diff --git a/compiler/natives/src/crypto/ecdsa/ecdsa.go b/compiler/natives/src/crypto/ecdsa/ecdsa.go deleted file mode 100644 index cf3da4ec8..000000000 --- a/compiler/natives/src/crypto/ecdsa/ecdsa.go +++ /dev/null @@ -1,98 +0,0 @@ -//go:build js -// +build js - -package ecdsa - -import ( - "crypto/elliptic" - "crypto/internal/bigmod" - "crypto/internal/nistec" - "io" - "math/big" -) - -//gopherjs:override-signature -func generateNISTEC(c *nistCurve, rand io.Reader) (*PrivateKey, error) - -//gopherjs:override-signature -func randomPoint(c *nistCurve, rand io.Reader) (k *bigmod.Nat, p nistec.WrappedPoint, err error) - -//gopherjs:override-signature -func signNISTEC(c *nistCurve, priv *PrivateKey, csprng io.Reader, hash []byte) (sig []byte, err error) - -//gopherjs:override-signature -func inverse(c *nistCurve, kInv, k *bigmod.Nat) - -//gopherjs:override-signature -func hashToNat(c *nistCurve, e *bigmod.Nat, hash []byte) - -//gopherjs:override-signature -func verifyNISTEC(c *nistCurve, pub *PublicKey, hash, sig []byte) bool - -//gopherjs:purge for go1.20 without generics -type nistPoint[T any] interface{} - -// temporarily replacement of `nistCurve[Point nistPoint[Point]]` for go1.20 without generics. -type nistCurve struct { - newPoint func() nistec.WrappedPoint - curve elliptic.Curve - N *bigmod.Modulus - nMinus2 []byte -} - -//gopherjs:override-signature -func (curve *nistCurve) pointFromAffine(x, y *big.Int) (p nistec.WrappedPoint, err error) - -//gopherjs:override-signature -func (curve *nistCurve) pointToAffine(p nistec.WrappedPoint) (x, y *big.Int, err error) - -var _p224 *nistCurve - -func p224() *nistCurve { - p224Once.Do(func() { - _p224 = &nistCurve{ - newPoint: nistec.NewP224WrappedPoint, - } - precomputeParams(_p224, elliptic.P224()) - }) - return _p224 -} - -var _p256 *nistCurve - -func p256() *nistCurve { - p256Once.Do(func() { - _p256 = &nistCurve{ - newPoint: nistec.NewP256WrappedPoint, - } - precomputeParams(_p256, elliptic.P256()) - }) - return _p256 -} - -var _p384 *nistCurve - -func p384() *nistCurve { - p384Once.Do(func() { - _p384 = &nistCurve{ - newPoint: nistec.NewP384WrappedPoint, - } - precomputeParams(_p384, elliptic.P384()) - }) - return _p384 -} - -var _p521 *nistCurve - -func p521() *nistCurve { - p521Once.Do(func() { - _p521 = &nistCurve{ - newPoint: nistec.NewP521WrappedPoint, - } - precomputeParams(_p521, elliptic.P521()) - }) - return _p521 -} - -//gopherjs:override-signature -func precomputeParams(c *nistCurve, curve elliptic.Curve) diff --git a/compiler/natives/src/crypto/ecdsa/ecdsa_test.go b/compiler/natives/src/crypto/ecdsa/ecdsa_test.go deleted file mode 100644 index efb4d7b5e..000000000 --- a/compiler/natives/src/crypto/ecdsa/ecdsa_test.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build js -// +build js - -package ecdsa - -import "testing" - -//gopherjs:override-signature -func testRandomPoint(t *testing.T, c *nistCurve) - -//gopherjs:override-signature -func testHashToNat(t *testing.T, c *nistCurve) diff --git a/compiler/natives/src/crypto/internal/boring/aes.go b/compiler/natives/src/crypto/internal/boring/aes.go index e2a840440..9e9ce5fff 100644 --- a/compiler/natives/src/crypto/internal/boring/aes.go +++ b/compiler/natives/src/crypto/internal/boring/aes.go @@ -1,5 +1,4 @@ //go:build js -// +build js package boring diff --git a/compiler/natives/src/crypto/subtle/xor.go b/compiler/natives/src/crypto/subtle/xor.go index eccf98531..6b78bddf2 100644 --- a/compiler/natives/src/crypto/subtle/xor.go +++ b/compiler/natives/src/crypto/subtle/xor.go @@ -1,5 +1,4 @@ //go:build js -// +build js package subtle @@ -74,5 +73,7 @@ const supportsUnaligned = false //gopherjs:purge func xorBytes(dstb, xb, yb *byte, n int) +// TODO(grantnelson-wf): Check if this should be removed or not with generics. +// //gopherjs:purge func xorLoop[T byte | uintptr](dst, x, y []T) {} diff --git a/compiler/natives/src/encoding/gob/gob.go b/compiler/natives/src/encoding/gob/gob.go deleted file mode 100644 index 244f72ed7..000000000 --- a/compiler/natives/src/encoding/gob/gob.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:build js -// +build js - -package gob - -import ( - "reflect" - "sync" -) - -type typeInfo struct { - id typeId - encInit sync.Mutex - - // temporarily replacement of atomic.Pointer[encEngine] for go1.20 without generics. - encoder atomicEncEnginePointer - wire *wireType -} - -type atomicEncEnginePointer struct { - v *encEngine -} - -func (x *atomicEncEnginePointer) Load() *encEngine { return x.v } -func (x *atomicEncEnginePointer) Store(val *encEngine) { x.v = val } - -// temporarily replacement of growSlice[E any] for go1.20 without generics. -func growSlice(v reflect.Value, ps any, length int) { - vps := reflect.ValueOf(ps) // *[]E - vs := vps.Elem() // []E - zero := reflect.Zero(vs.Type().Elem()) - vs.Set(reflect.Append(vs, zero)) - cp := vs.Cap() - if cp > length { - cp = length - } - vs.Set(vs.Slice(0, cp)) - v.Set(vs) -} diff --git a/compiler/natives/src/internal/coverage/slicereader/slicereader.go b/compiler/natives/src/internal/coverage/slicereader/slicereader.go index 4346d7c97..5ddf9ab04 100644 --- a/compiler/natives/src/internal/coverage/slicereader/slicereader.go +++ b/compiler/natives/src/internal/coverage/slicereader/slicereader.go @@ -1,5 +1,4 @@ //go:build js -// +build js package slicereader diff --git a/compiler/natives/src/internal/godebug/godebug.go b/compiler/natives/src/internal/godebug/godebug.go index e43006c3f..3d7d719eb 100644 --- a/compiler/natives/src/internal/godebug/godebug.go +++ b/compiler/natives/src/internal/godebug/godebug.go @@ -1,87 +1,8 @@ //go:build js -// +build js package godebug -import ( - "sync" - _ "unsafe" // go:linkname -) - -type Setting struct { - name string - once sync.Once - - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - value *atomicStringPointer -} - -type atomicStringPointer struct { - v *string -} - -func (x *atomicStringPointer) Load() *string { return x.v } -func (x *atomicStringPointer) Store(val *string) { x.v = val } - -func (s *Setting) Value() string { - s.once.Do(func() { - v, ok := cache.Load(s.name) - if !ok { - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - p := new(atomicStringPointer) - p.Store(&empty) - v, _ = cache.LoadOrStore(s.name, p) - } - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - s.value = v.(*atomicStringPointer) - }) - return *s.value.Load() -} +import _ "unsafe" // go:linkname //go:linkname setUpdate runtime.godebug_setUpdate func setUpdate(update func(def, env string)) - -func update(def, env string) { - updateMu.Lock() - defer updateMu.Unlock() - - did := make(map[string]bool) - parse(did, env) - parse(did, def) - - cache.Range(func(name, v any) bool { - if !did[name.(string)] { - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - v.(*atomicStringPointer).Store(&empty) - } - return true - }) -} - -func parse(did map[string]bool, s string) { - end := len(s) - eq := -1 - for i := end - 1; i >= -1; i-- { - if i == -1 || s[i] == ',' { - if eq >= 0 { - name, value := s[i+1:eq], s[eq+1:end] - if !did[name] { - did[name] = true - v, ok := cache.Load(name) - if !ok { - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - p := new(atomicStringPointer) - p.Store(&empty) - v, _ = cache.LoadOrStore(name, p) - } - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - v.(*atomicStringPointer).Store(&value) - } - } - eq = -1 - end = i - } else if s[i] == '=' { - eq = i - } - } -} diff --git a/compiler/natives/src/internal/unsafeheader/unsafeheader.go b/compiler/natives/src/internal/unsafeheader/unsafeheader.go index 4a7e43342..f4aa32e18 100644 --- a/compiler/natives/src/internal/unsafeheader/unsafeheader.go +++ b/compiler/natives/src/internal/unsafeheader/unsafeheader.go @@ -1,5 +1,4 @@ //go:build js -// +build js package unsafeheader diff --git a/compiler/natives/src/math/rand/rand.go b/compiler/natives/src/math/rand/rand.go index 0dfb1b279..728fe492a 100644 --- a/compiler/natives/src/math/rand/rand.go +++ b/compiler/natives/src/math/rand/rand.go @@ -1,5 +1,4 @@ //go:build js -// +build js package rand diff --git a/compiler/natives/src/net/fd_unix.go b/compiler/natives/src/net/fd_unix.go index d819677d6..706c5ac24 100644 --- a/compiler/natives/src/net/fd_unix.go +++ b/compiler/natives/src/net/fd_unix.go @@ -1,5 +1,4 @@ //go:build js -// +build js package net diff --git a/compiler/natives/src/net/http/http.go b/compiler/natives/src/net/http/http.go index 61ae646f8..fbd7a8927 100644 --- a/compiler/natives/src/net/http/http.go +++ b/compiler/natives/src/net/http/http.go @@ -5,15 +5,10 @@ package http import ( "bufio" "bytes" - "context" - "crypto/tls" "errors" "io" - "net" "net/textproto" "strconv" - "sync" - "sync/atomic" "github.com/gopherjs/gopherjs/js" ) @@ -117,30 +112,3 @@ func (t *XHRTransport) CancelRequest(req *Request) { xhr.Call("abort") } } - -type conn struct { - server *Server - cancelCtx context.CancelFunc - rwc net.Conn - remoteAddr string - tlsState *tls.ConnectionState - werr error - r *connReader - bufr *bufio.Reader - bufw *bufio.Writer - lastMethod string - - // temporarily replacement of `atomic.Pointer[response]` for go1.20 without generics. - curReq atomicResponsePointer - - curState atomic.Uint64 - mu sync.Mutex - hijackedv bool -} - -type atomicResponsePointer struct { - v *response -} - -func (x *atomicResponsePointer) Load() *response { return x.v } -func (x *atomicResponsePointer) Store(val *response) { x.v = val } diff --git a/compiler/natives/src/reflect/reflect.go b/compiler/natives/src/reflect/reflect.go index 940de41c4..17d3bc35a 100644 --- a/compiler/natives/src/reflect/reflect.go +++ b/compiler/natives/src/reflect/reflect.go @@ -1860,8 +1860,12 @@ func valueMethodName() string { // https://github.com/gopherjs/gopherjs/issues/1085 is resolved. methodName := name - if idx := stringsLastIndex(name, '.'); idx >= 0 { - methodName = name[idx+1:] + if idx := stringsLastIndex(methodName, '.'); idx >= 0 { + methodName = methodName[idx+1:] + } + const midDot = '·' // This is same `midDot` as in gopherjs/compiler/utils.go + if idx := stringsLastIndex(methodName, midDot); idx >= 0 { + methodName = methodName[idx+1:] } // Since function name in the call stack doesn't contain receiver name, diff --git a/compiler/natives/src/reflect/reflect_test.go b/compiler/natives/src/reflect/reflect_test.go index 3dab36a06..03bdcdaac 100644 --- a/compiler/natives/src/reflect/reflect_test.go +++ b/compiler/natives/src/reflect/reflect_test.go @@ -307,3 +307,7 @@ func TestValuePanic(t *testing.T) { func TestSetIter(t *testing.T) { t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085") } + +func TestGrow(t *testing.T) { + t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085") +} diff --git a/compiler/natives/src/sync/map.go b/compiler/natives/src/sync/map.go deleted file mode 100644 index 3f81b9b31..000000000 --- a/compiler/natives/src/sync/map.go +++ /dev/null @@ -1,48 +0,0 @@ -//go:build js -// +build js - -package sync - -type Map struct { - mu Mutex - - // replaced atomic.Pointer[readOnly] for go1.20 without generics. - read atomicReadOnlyPointer - - dirty map[any]*entry - misses int -} - -type atomicReadOnlyPointer struct { - v *readOnly -} - -func (x *atomicReadOnlyPointer) Load() *readOnly { return x.v } -func (x *atomicReadOnlyPointer) Store(val *readOnly) { x.v = val } - -type entry struct { - - // replaced atomic.Pointer[any] for go1.20 without generics. - p atomicAnyPointer -} - -type atomicAnyPointer struct { - v *any -} - -func (x *atomicAnyPointer) Load() *any { return x.v } -func (x *atomicAnyPointer) Store(val *any) { x.v = val } - -func (x *atomicAnyPointer) Swap(new *any) *any { - old := x.v - x.v = new - return old -} - -func (x *atomicAnyPointer) CompareAndSwap(old, new *any) bool { - if x.v == old { - x.v = new - return true - } - return false -} diff --git a/compiler/natives/src/time/export_test.go b/compiler/natives/src/time/export_test.go deleted file mode 100644 index 5cd3fc6ab..000000000 --- a/compiler/natives/src/time/export_test.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build js -// +build js - -package time - -// replaced `parseRFC3339[string]` for go1.20 temporarily without generics. -var ParseRFC3339 = func(s string, local *Location) (Time, bool) { - return parseRFC3339(s, local) -} diff --git a/compiler/natives/src/time/format.go b/compiler/natives/src/time/format.go deleted file mode 100644 index 0e1594c19..000000000 --- a/compiler/natives/src/time/format.go +++ /dev/null @@ -1,79 +0,0 @@ -//go:build js -// +build js - -package time - -// copied and replaced for go1.20 temporarily without generics. -func atoi(sAny any) (x int, err error) { - s := asBytes(sAny) - neg := false - if len(s) > 0 && (s[0] == '-' || s[0] == '+') { - neg = s[0] == '-' - s = s[1:] - } - q, remStr, err := leadingInt(s) - rem := []byte(remStr) - x = int(q) - if err != nil || len(rem) > 0 { - return 0, atoiError - } - if neg { - x = -x - } - return x, nil -} - -// copied and replaced for go1.20 temporarily without generics. -func isDigit(sAny any, i int) bool { - s := asBytes(sAny) - if len(s) <= i { - return false - } - c := s[i] - return '0' <= c && c <= '9' -} - -// copied and replaced for go1.20 temporarily without generics. -func parseNanoseconds(sAny any, nbytes int) (ns int, rangeErrString string, err error) { - value := asBytes(sAny) - if !commaOrPeriod(value[0]) { - err = errBad - return - } - if nbytes > 10 { - value = value[:10] - nbytes = 10 - } - if ns, err = atoi(value[1:nbytes]); err != nil { - return - } - if ns < 0 { - rangeErrString = "fractional second" - return - } - scaleDigits := 10 - nbytes - for i := 0; i < scaleDigits; i++ { - ns *= 10 - } - return -} - -// copied and replaced for go1.20 temporarily without generics. -func leadingInt(sAny any) (x uint64, rem string, err error) { - s := asBytes(sAny) - i := 0 - for ; i < len(s); i++ { - c := s[i] - if c < '0' || c > '9' { - break - } - if x > 1<<63/10 { - return 0, rem, errLeadingInt - } - x = x*10 + uint64(c) - '0' - if x > 1<<63 { - return 0, rem, errLeadingInt - } - } - return x, string(s[i:]), nil -} diff --git a/compiler/natives/src/time/format_rfc3339.go b/compiler/natives/src/time/format_rfc3339.go deleted file mode 100644 index 7c69bfc95..000000000 --- a/compiler/natives/src/time/format_rfc3339.go +++ /dev/null @@ -1,85 +0,0 @@ -//go:build js -// +build js - -package time - -import "errors" - -// added for go1.20 temporarily without generics. -func asBytes(s any) []byte { - switch t := s.(type) { - case []byte: - return t - case string: - return []byte(t) - default: - panic(errors.New(`unexpected type passed to asBytes, expected string or []bytes`)) - } -} - -// copied and replaced for go1.20 temporarily without generics. -func parseRFC3339(sAny any, local *Location) (Time, bool) { - s := asBytes(sAny) - ok := true - parseUint := func(s []byte, min, max int) (x int) { - for _, c := range s { - if c < '0' || '9' < c { - ok = false - return min - } - x = x*10 + int(c) - '0' - } - if x < min || max < x { - ok = false - return min - } - return x - } - - if len(s) < len("2006-01-02T15:04:05") { - return Time{}, false - } - year := parseUint(s[0:4], 0, 9999) - month := parseUint(s[5:7], 1, 12) - day := parseUint(s[8:10], 1, daysIn(Month(month), year)) - hour := parseUint(s[11:13], 0, 23) - min := parseUint(s[14:16], 0, 59) - sec := parseUint(s[17:19], 0, 59) - if !ok || !(s[4] == '-' && s[7] == '-' && s[10] == 'T' && s[13] == ':' && s[16] == ':') { - return Time{}, false - } - s = s[19:] - - var nsec int - if len(s) >= 2 && s[0] == '.' && isDigit(s, 1) { - n := 2 - for ; n < len(s) && isDigit(s, n); n++ { - } - nsec, _, _ = parseNanoseconds(s, n) - s = s[n:] - } - - t := Date(year, Month(month), day, hour, min, sec, nsec, UTC) - if len(s) != 1 || s[0] != 'Z' { - if len(s) != len("-07:00") { - return Time{}, false - } - hr := parseUint(s[1:3], 0, 23) - mm := parseUint(s[4:6], 0, 59) - if !ok || !((s[0] == '-' || s[0] == '+') && s[3] == ':') { - return Time{}, false - } - zoneOffset := (hr*60 + mm) * 60 - if s[0] == '-' { - zoneOffset *= -1 - } - t.addSec(-int64(zoneOffset)) - - if _, offset, _, _, _ := local.lookup(t.unixSec()); offset == zoneOffset { - t.setLoc(local) - } else { - t.setLoc(FixedZone("", zoneOffset)) - } - } - return t, true -}