From 59e4d769498fe6589e27bb0a86d7ac0b36d5c46b Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 2 Dec 2023 14:22:01 +0000 Subject: [PATCH 01/28] Add typeparam test cases to the gorepo test suite. This commit updates the run.go to do correct flag parsing and lists all failing test cases as known failures. --- tests/gorepo/run.go | 241 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 218 insertions(+), 23 deletions(-) diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 0ceb99ff5..1bebc0cce 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -24,6 +24,7 @@ import ( "fmt" "hash/fnv" "io" + "io/ioutil" "log" "os" "os/exec" @@ -109,9 +110,6 @@ var knownFails = map[string]failReason{ "fixedbugs/issue23188.go": {desc: "incorrect order of evaluation of index operations"}, "fixedbugs/issue24547.go": {desc: "incorrect computing method sets with shadowed methods"}, - // These are new tests in Go 1.11.5 - "fixedbugs/issue28688.go": {category: notApplicable, desc: "testing runtime optimisations"}, - // These are new tests in Go 1.12. "fixedbugs/issue23837.go": {desc: "missing panic on nil pointer-to-empty-struct dereference"}, "fixedbugs/issue27201.go": {desc: "incorrect stack trace for nil dereference in inlined function"}, @@ -121,7 +119,6 @@ var knownFails = map[string]failReason{ // These are new tests in Go 1.12.9. "fixedbugs/issue30977.go": {category: neverTerminates, desc: "does for { runtime.GC() }"}, "fixedbugs/issue32477.go": {category: notApplicable, desc: "uses runtime.SetFinalizer and runtime.GC"}, - "fixedbugs/issue32680.go": {category: notApplicable, desc: "uses -gcflags=-d=ssa/check/on flag"}, // These are new tests in Go 1.13-1.16. "fixedbugs/issue19113.go": {category: lowLevelRuntimeDifference, desc: "JavaScript bit shifts by negative amount don't cause an exception"}, @@ -134,7 +131,6 @@ var knownFails = map[string]failReason{ "fixedbugs/issue30116u.go": {desc: "GopherJS doesn't specify the array/slice index selector in the out-of-bounds message"}, "fixedbugs/issue34395.go": {category: neverTerminates, desc: "https://github.com/gopherjs/gopherjs/issues/1007"}, "fixedbugs/issue35027.go": {category: usesUnsupportedPackage, desc: "uses unsupported conversion to reflect.SliceHeader and -gcflags=-d=checkptr"}, - "fixedbugs/issue35073.go": {category: usesUnsupportedPackage, desc: "uses unsupported flag -gcflags=-d=checkptr"}, "fixedbugs/issue35576.go": {category: lowLevelRuntimeDifference, desc: "GopherJS print/println format for floats differs from Go's"}, "fixedbugs/issue40917.go": {category: notApplicable, desc: "uses pointer arithmetic and unsupported flag -gcflags=-d=checkptr"}, @@ -149,12 +145,131 @@ var knownFails = map[string]failReason{ "fixedbugs/issue50854.go": {category: lowLevelRuntimeDifference, desc: "negative int32 overflow behaves differently in JS"}, // These are new tests in Go 1.18 - "fixedbugs/issue46938.go": {category: notApplicable, desc: "tests -d=checkptr compiler mode, which GopherJS doesn't support"}, "fixedbugs/issue47928.go": {category: notApplicable, desc: "//go:nointerface is a part of GOEXPERIMENT=fieldtrack and is not supported by GopherJS"}, - "fixedbugs/issue49665.go": {category: other, desc: "attempts to pass -gcflags=-G=3 to enable generics, GopherJS doesn't expect the flag; re-enable in Go 1.19 where the flag is removed"}, "fixedbugs/issue48898.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1128"}, "fixedbugs/issue48536.go": {category: usesUnsupportedPackage, desc: "https://github.com/gopherjs/gopherjs/issues/1130"}, "fixedbugs/issue53600.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, + "typeparam/issue51733.go": {category: usesUnsupportedPackage, desc: "unsafe: uintptr to struct pointer conversion is unsupported"}, + + // 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/absdiff.go": {category: generics, desc: "missing operator support for generic types"}, + "typeparam/absdiff2.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/absdiff3.go": {category: generics, desc: "missing operator support for generic types"}, + "typeparam/boundmethod.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/chans.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/combine.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, + "typeparam/cons.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/dictionaryCapture-noinline.go": {category: generics, desc: "attempts to pass -gcflags=\"-G=3\" flag, incorrectly parsed by run.go"}, + "typeparam/dictionaryCapture.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/dottype.go": {category: generics, desc: "not triaged"}, + "typeparam/double.go": {category: generics, desc: "make() doesn't support generic slice types"}, + "typeparam/eface.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/equal.go": {category: generics, desc: "missing support for the comparable type constraint"}, + "typeparam/fact.go": {category: generics, desc: "missing support for the comparable type constraint"}, + "typeparam/genembed.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/genembed2.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/graph.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/ifaceconv.go": {category: generics, desc: "not triaged"}, + "typeparam/index.go": {category: generics, desc: "undiagnosed: runtime error: comparing uncomparable type undefined"}, + "typeparam/index2.go": {category: generics, desc: "missing index operator support for generic types"}, + "typeparam/interfacearg.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, + "typeparam/issue23536.go": {category: generics, desc: "missing support for generic byte/rune slice to string conversion"}, + "typeparam/issue44688.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue45817.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, + "typeparam/issue47258.go": {category: generics, desc: "missing operator support for generic types"}, + "typeparam/issue47272.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue47514.go": {category: generics, desc: "not triaged"}, + "typeparam/issue47514b.go": {category: generics, desc: "not triaged"}, + "typeparam/issue47684.go": {category: generics, desc: "not triaged"}, + "typeparam/issue47684b.go": {category: generics, desc: "not triaged"}, + "typeparam/issue47684c.go": {category: generics, desc: "not triaged"}, + "typeparam/issue47713.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue47716.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue47723.go": {category: generics, desc: "not triaged"}, + "typeparam/issue47740.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue47740b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue47775b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue47877.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue47901.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue47925.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue47925b.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/issue47925c.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/issue47925d.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/issue48013.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, + "typeparam/issue48016.go": {category: generics, desc: "not triaged"}, + "typeparam/issue48030.go": {category: generics, desc: "not triaged"}, + "typeparam/issue48042.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48047.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48049.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48137.go": {category: generics, desc: "not triaged"}, + "typeparam/issue48225.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48253.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48276a.go": {category: generics, desc: "missing support for the comparable type constraint"}, + "typeparam/issue48276b.go": {category: generics, desc: "not triaged"}, + "typeparam/issue48317.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, + "typeparam/issue48318.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, + "typeparam/issue48344.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48453.go": {category: generics, desc: "make() doesn't support generic slice types"}, + "typeparam/issue48598.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48602.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48617.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48645a.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48645b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48838.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue49049.go": {category: generics, desc: "not triaged"}, + "typeparam/issue49295.go": {category: generics, desc: "len() doesn't support generic pointer to array types"}, + "typeparam/issue49421.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, + "typeparam/issue49547.go": {category: generics, desc: "incorrect type strings for parameterized types"}, + "typeparam/issue49659b.go": {category: generics, desc: "incorrect type strings for parameterized types"}, + "typeparam/issue50002.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/issue50109.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue50109b.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/issue50193.go": {category: generics, desc: "invalid print format for complex numbers"}, + "typeparam/issue50264.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue50419.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue50642.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue50690a.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue50690b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue50690c.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, + "typeparam/issue50833.go": {category: generics, desc: "undiagnosed: compiler panic triggered by a composite literal"}, + "typeparam/issue51303.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue51522a.go": {category: generics, desc: "missing support for the comparable type constraint"}, + "typeparam/issue51522b.go": {category: generics, desc: "missing support for the comparable type constraint"}, + "typeparam/issue51700.go": {category: generics, desc: "not triaged"}, + "typeparam/issue52026.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue52228.go": {category: generics, desc: "not triaged"}, + "typeparam/issue53477.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/list.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/list2.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/lockable.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/map.go": {category: generics, desc: "not triaged"}, + "typeparam/maps.go": {category: generics, desc: "missing support for the comparable type constraint"}, + "typeparam/metrics.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/min.go": {category: generics, desc: "not triaged"}, + "typeparam/nested.go": {category: generics, desc: "missing comparison operator support for generic types"}, + "typeparam/ordered.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/orderedmap.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/pair.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, + "typeparam/sets.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/settable.go": {category: generics, desc: "undiagnosed: len() returns an invalid value on a generic function result"}, + "typeparam/shape1.go": {category: generics, desc: "not triaged"}, + "typeparam/slices.go": {category: generics, desc: "missing operator support for generic types"}, + "typeparam/stringable.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/stringer.go": {category: generics, desc: "not triaged"}, + "typeparam/struct.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, + "typeparam/subdict.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/sum.go": {category: generics, desc: "not triaged"}, + "typeparam/typeswitch1.go": {category: generics, desc: "not triaged"}, + "typeparam/typeswitch2.go": {category: generics, desc: "complex types have different print() format"}, + "typeparam/typeswitch3.go": {category: generics, desc: "missing support for type switching on generic types"}, + "typeparam/typeswitch4.go": {category: generics, desc: "not triaged"}, + "typeparam/typeswitch5.go": {category: generics, desc: "different print() format for floating point types"}, + "typeparam/typeswitch6.go": {category: generics, desc: "not triaged"}, + "typeparam/typeswitch7.go": {category: generics, desc: "not triaged"}, + "typeparam/value.go": {category: generics, desc: "missing support for parameterized type instantiation"}, } type failCategory uint8 @@ -168,6 +283,7 @@ const ( unsureIfGopherJSSupportsThisFeature lowLevelRuntimeDifference // JavaScript runtime behaves differently from Go in ways that are difficult to work around. notApplicable // Test that doesn't need to run under GopherJS; it doesn't apply to the Go language in a general way. + generics // Test requires generics support. ) type failReason struct { @@ -195,7 +311,7 @@ var ( // dirs are the directories to look for *.go files in. // TODO(bradfitz): just use all directories? - dirs = []string{".", "ken", "chan", "interface", "syntax", "dwarf", "fixedbugs"} + dirs = []string{".", "ken", "chan", "interface", "syntax", "dwarf", "fixedbugs", "typeparam"} // ratec controls the max number of tests running at a time. ratec chan bool @@ -456,8 +572,8 @@ func (t *test) goDirName() string { return filepath.Join(t.dir, strings.Replace(t.gofile, ".go", ".dir", -1)) } -func goDirFiles(longdir string) (filter []os.DirEntry, err error) { - files, dirErr := os.ReadDir(longdir) +func goDirFiles(longdir string) (filter []os.FileInfo, err error) { + files, dirErr := ioutil.ReadDir(longdir) if dirErr != nil { return nil, dirErr } @@ -480,7 +596,7 @@ func goDirPackages(longdir string) ([][]string, error) { m := make(map[string]int) for _, file := range files { name := file.Name() - data, err := os.ReadFile(filepath.Join(longdir, name)) + data, err := ioutil.ReadFile(filepath.Join(longdir, name)) if err != nil { return nil, err } @@ -592,7 +708,7 @@ func (t *test) run() { return } - srcBytes, err := os.ReadFile(t.goFileName()) + srcBytes, err := ioutil.ReadFile(t.goFileName()) if err != nil { t.err = err return @@ -633,16 +749,20 @@ func (t *test) run() { var args, flags []string wantError := false - f := strings.Fields(action) + f, err := splitQuoted(action) + if err != nil { + t.err = fmt.Errorf("invalid test recipe: %v", err) + return + } if len(f) > 0 { action = f[0] args = f[1:] } - // GOPHERJS: For now, only run with "run", "cmpout" actions, in "fixedbugs" dir. Skip all others. + // GOPHERJS: For now, only run with "run", "cmpout" actions, in "fixedbugs" and "typeparam" dirs. Skip all others. switch action { case "run", "cmpout": - if filepath.Clean(t.dir) != "fixedbugs" { + if d := filepath.Clean(t.dir); d != "fixedbugs" && d != "typeparam" { action = "skip" } default: @@ -681,7 +801,7 @@ func (t *test) run() { t.makeTempDir() defer os.RemoveAll(t.tempDir) - err = os.WriteFile(filepath.Join(t.tempDir, t.gofile), srcBytes, 0o644) + err = ioutil.WriteFile(filepath.Join(t.tempDir, t.gofile), srcBytes, 0o644) check(err) // A few tests (of things like the environment) require these to be set. @@ -692,6 +812,19 @@ func (t *test) run() { os.Setenv("GOARCH", goarch) } + { + // GopherJS: we don't support any of -gcflags, but for the most part they + // are not too relevant to the outcome of the test. + supportedArgs := []string{} + for _, a := range args { + if strings.HasPrefix(a, "-gcflags") { + continue + } + supportedArgs = append(supportedArgs, a) + } + args = supportedArgs + } + useTmp := true runcmd := func(args ...string) ([]byte, error) { cmd := exec.Command(args[0], args[1:]...) @@ -853,7 +986,7 @@ func (t *test) run() { return } tfile := filepath.Join(t.tempDir, "tmp__.go") - if err := os.WriteFile(tfile, out, 0o666); err != nil { + if err := ioutil.WriteFile(tfile, out, 0o666); err != nil { t.err = fmt.Errorf("write tempfile:%s", err) return } @@ -874,7 +1007,7 @@ func (t *test) run() { return } tfile := filepath.Join(t.tempDir, "tmp__.go") - err = os.WriteFile(tfile, out, 0o666) + err = ioutil.WriteFile(tfile, out, 0o666) if err != nil { t.err = fmt.Errorf("write tempfile:%s", err) return @@ -922,7 +1055,7 @@ func (t *test) String() string { func (t *test) makeTempDir() { var err error - t.tempDir, err = os.MkdirTemp("", "") + t.tempDir, err = ioutil.TempDir("", "") check(err) } @@ -930,7 +1063,7 @@ func (t *test) expectedOutput() string { filename := filepath.Join(t.dir, t.gofile) filename = filename[:len(filename)-len(".go")] filename += ".out" - b, _ := os.ReadFile(filename) + b, _ := ioutil.ReadFile(filename) return string(b) } @@ -1022,7 +1155,7 @@ func (t *test) errorCheck(outStr string, fullshort ...string) (err error) { func (t *test) updateErrors(out string, file string) { // Read in source file. - src, err := os.ReadFile(file) + src, err := ioutil.ReadFile(file) if err != nil { fmt.Fprintln(os.Stderr, err) return @@ -1077,7 +1210,7 @@ func (t *test) updateErrors(out string, file string) { } } // Write new file. - err = os.WriteFile(file, []byte(strings.Join(lines, "\n")), 0o640) + err = ioutil.WriteFile(file, []byte(strings.Join(lines, "\n")), 0o640) if err != nil { fmt.Fprintln(os.Stderr, err) return @@ -1134,7 +1267,7 @@ var ( func (t *test) wantedErrors(file, short string) (errs []wantedError) { cache := make(map[string]*regexp.Regexp) - src, _ := os.ReadFile(file) + src, _ := ioutil.ReadFile(file) for i, line := range strings.Split(string(src), "\n") { lineNum := i + 1 if strings.Contains(line, "////") { @@ -1256,3 +1389,65 @@ func getenv(key, def string) string { } return def } + +// splitQuoted splits the string s around each instance of one or more consecutive +// white space characters while taking into account quotes and escaping, and +// returns an array of substrings of s or an empty list if s contains only white space. +// Single quotes and double quotes are recognized to prevent splitting within the +// quoted region, and are removed from the resulting substrings. If a quote in s +// isn't closed err will be set and r will have the unclosed argument as the +// last element. The backslash is used for escaping. +// +// For example, the following string: +// +// a b:"c d" 'e''f' "g\"" +// +// Would be parsed as: +// +// []string{"a", "b:c d", "ef", `g"`} +// +// [copied from src/go/build/build.go] +func splitQuoted(s string) (r []string, err error) { + var args []string + arg := make([]rune, len(s)) + escaped := false + quoted := false + quote := '\x00' + i := 0 + for _, rune := range s { + switch { + case escaped: + escaped = false + case rune == '\\': + escaped = true + continue + case quote != '\x00': + if rune == quote { + quote = '\x00' + continue + } + case rune == '"' || rune == '\'': + quoted = true + quote = rune + continue + case unicode.IsSpace(rune): + if quoted || i > 0 { + quoted = false + args = append(args, string(arg[:i])) + i = 0 + } + continue + } + arg[i] = rune + i++ + } + if quoted || i > 0 { + args = append(args, string(arg[:i])) + } + if quote != 0 { + err = errors.New("unclosed quote") + } else if escaped { + err = errors.New("unfinished escaping") + } + return args, err +} From 82b84ff324c98d1c8374820dd20427dec5caeeb8 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 2 Dec 2023 14:28:37 +0000 Subject: [PATCH 02/28] Do not fail compilation when encountering generics. --- compiler/package.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/compiler/package.go b/compiler/package.go index 93c22f1c5..403f9a355 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -6,7 +6,6 @@ import ( "fmt" "go/ast" "go/constant" - "go/scanner" "go/token" "go/types" "sort" @@ -400,12 +399,6 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor for _, fun := range functions { o := funcCtx.pkgCtx.Defs[fun.Name].(*types.Func) - if fun.Type.TypeParams.NumFields() > 0 { - return nil, scanner.Error{ - Pos: fileSet.Position(fun.Type.TypeParams.Pos()), - Msg: fmt.Sprintf("function %s: type parameters are not supported by GopherJS: https://github.com/gopherjs/gopherjs/issues/1013", o.Name()), - } - } funcInfo := funcCtx.pkgCtx.FuncDeclInfos[o] d := Decl{ FullName: o.FullName(), @@ -489,13 +482,6 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor } typeName := funcCtx.objectName(o) - if named, ok := o.Type().(*types.Named); ok && named.TypeParams().Len() > 0 { - return nil, scanner.Error{ - Pos: fileSet.Position(o.Pos()), - Msg: fmt.Sprintf("type %s: type parameters are not supported by GopherJS: https://github.com/gopherjs/gopherjs/issues/1013", o.Name()), - } - } - d := Decl{ Vars: []string{typeName}, DceObjectFilter: o.Name(), From 911867900f639e5867a4fb8062ef2096b8ee70aa Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 17 Dec 2023 11:23:11 +0000 Subject: [PATCH 03/28] Implement a new internal library for discovering generic instances. The library scans type-checked Go AST and incrementally discovers instantiations of generic types and functions in it. Each instantiation is recorded with the origin object and a set of type arguments. The scan is seeded from two sources: - Any Instances that have been added to the working InstanceSet externally (for example, from references by other packages). - Any instances that are discovered within the non-generic code in the package itself. The discovery proceeds to search for instances in the generic code by traversing its AST for each set of previously discovered type arguments. In that scan, whenever a type is defined in terms of a type param, we map it onto a concrete type given the current set of type arguments. For now, the library is not fully ready to handle generic instances that come across the package boundaries, that will come later. The type mapping logic turns out to be fairly complex, so instead of reimplementing it I decided to cheat and "borrow" it from the go/types package via a go:linkname directive. I didn't want to vendor it because I expect it would be changing fairly regularly in future. We shall see whether this was a wise choice. This commit also includes a couple of supporting refactorings: - Moved the symbols logic used by go:linkname to a separate package, since it turned out to be useful for generics as well. - Added a testingx helper library with a generic `Must(t)` helper to make trivial error handling in tests less verbose. --- compiler/compiler.go | 3 +- compiler/internal/symbol/symbol.go | 60 ++++ compiler/internal/symbol/symbol_test.go | 54 +++ compiler/internal/typeparams/collect.go | 211 +++++++++++ compiler/internal/typeparams/collect_test.go | 334 ++++++++++++++++++ compiler/internal/typeparams/instance.go | 86 +++++ compiler/internal/typeparams/instance_test.go | 197 +++++++++++ compiler/internal/typeparams/subst.go | 13 + compiler/linkname.go | 76 +--- compiler/linkname_test.go | 56 +-- compiler/package.go | 3 +- compiler/typesutil/map.go | 34 ++ go.mod | 8 +- go.sum | 18 +- internal/srctesting/srctesting.go | 1 + internal/testingx/must.go | 24 ++ 16 files changed, 1049 insertions(+), 129 deletions(-) create mode 100644 compiler/internal/symbol/symbol.go create mode 100644 compiler/internal/symbol/symbol_test.go create mode 100644 compiler/internal/typeparams/collect.go create mode 100644 compiler/internal/typeparams/collect_test.go create mode 100644 compiler/internal/typeparams/instance.go create mode 100644 compiler/internal/typeparams/instance_test.go create mode 100644 compiler/internal/typeparams/subst.go create mode 100644 compiler/typesutil/map.go create mode 100644 internal/testingx/must.go diff --git a/compiler/compiler.go b/compiler/compiler.go index 0588a923c..b3b77d168 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -17,6 +17,7 @@ import ( "strings" "time" + "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/gopherjs/gopherjs/compiler/prelude" "golang.org/x/tools/go/gcexportdata" ) @@ -112,7 +113,7 @@ type Decl struct { // Go compiler/linker toolchain. Used by GopherJS to support go:linkname // directives. Must be set for decls that are supported by go:linkname // implementation. - LinkingName SymName + LinkingName symbol.Name // A list of package-level JavaScript variable names this symbol needs to declare. Vars []string // NamedRecvType is method named recv declare. diff --git a/compiler/internal/symbol/symbol.go b/compiler/internal/symbol/symbol.go new file mode 100644 index 000000000..851ca1ef6 --- /dev/null +++ b/compiler/internal/symbol/symbol.go @@ -0,0 +1,60 @@ +package symbol + +import ( + "go/types" + "strings" +) + +// Name uniquely identifies a named symbol within a program. +// +// This is a logical equivalent of a symbol name used by traditional linkers. +// The following properties should hold true: +// +// - Each named symbol within a program has a unique Name. +// - Similarly named methods of different types will have different symbol names. +// - The string representation is opaque and should not be attempted to reversed +// to a struct form. +type Name struct { + PkgPath string // Full package import path. + Name string // Symbol name. +} + +// New constructs SymName for a given named symbol. +func New(o types.Object) Name { + if fun, ok := o.(*types.Func); ok { + sig := fun.Type().(*types.Signature) + if recv := sig.Recv(); recv != nil { + // Special case: disambiguate names for different types' methods. + typ := recv.Type() + if ptr, ok := typ.(*types.Pointer); ok { + return Name{ + PkgPath: o.Pkg().Path(), + Name: "(*" + ptr.Elem().(*types.Named).Obj().Name() + ")." + o.Name(), + } + } + return Name{ + PkgPath: o.Pkg().Path(), + Name: typ.(*types.Named).Obj().Name() + "." + o.Name(), + } + } + } + return Name{ + PkgPath: o.Pkg().Path(), + Name: o.Name(), + } +} + +func (n Name) String() string { return n.PkgPath + "." + n.Name } + +func (n Name) IsMethod() (recv string, method string, ok bool) { + pos := strings.IndexByte(n.Name, '.') + if pos == -1 { + return + } + recv, method, ok = n.Name[:pos], n.Name[pos+1:], true + size := len(recv) + if size > 2 && recv[0] == '(' && recv[size-1] == ')' { + recv = recv[1 : size-1] + } + return +} diff --git a/compiler/internal/symbol/symbol_test.go b/compiler/internal/symbol/symbol_test.go new file mode 100644 index 000000000..d4fc4196a --- /dev/null +++ b/compiler/internal/symbol/symbol_test.go @@ -0,0 +1,54 @@ +package symbol + +import ( + "go/token" + "go/types" + "testing" + + "github.com/gopherjs/gopherjs/internal/srctesting" +) + +func TestName(t *testing.T) { + const src = `package testcase + + func AFunction() {} + type AType struct {} + func (AType) AMethod() {} + func (*AType) APointerMethod() {} + var AVariable int32 + ` + + fset := token.NewFileSet() + _, pkg := srctesting.Check(t, fset, srctesting.Parse(t, fset, src)) + + tests := []struct { + obj types.Object + want Name + }{ + { + obj: pkg.Scope().Lookup("AFunction"), + want: Name{PkgPath: "test", Name: "AFunction"}, + }, { + obj: pkg.Scope().Lookup("AType"), + want: Name{PkgPath: "test", Name: "AType"}, + }, { + obj: types.NewMethodSet(pkg.Scope().Lookup("AType").Type()).Lookup(pkg, "AMethod").Obj(), + want: Name{PkgPath: "test", Name: "AType.AMethod"}, + }, { + obj: types.NewMethodSet(types.NewPointer(pkg.Scope().Lookup("AType").Type())).Lookup(pkg, "APointerMethod").Obj(), + want: Name{PkgPath: "test", Name: "(*AType).APointerMethod"}, + }, { + obj: pkg.Scope().Lookup("AVariable"), + want: Name{PkgPath: "test", Name: "AVariable"}, + }, + } + + for _, test := range tests { + t.Run(test.obj.Name(), func(t *testing.T) { + got := New(test.obj) + if got != test.want { + t.Errorf("NewSymName(%q) returned %#v, want: %#v", test.obj.Name(), got, test.want) + } + }) + } +} diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go new file mode 100644 index 000000000..7c04125f4 --- /dev/null +++ b/compiler/internal/typeparams/collect.go @@ -0,0 +1,211 @@ +package typeparams + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "github.com/gopherjs/gopherjs/compiler/typesutil" +) + +// Resolver translates types defined in terms of type parameters into concrete +// types, given a mapping from type params to type arguments. +type Resolver struct { + TContext *types.Context + Map map[*types.TypeParam]types.Type + + memo typesutil.Map[types.Type] +} + +// 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 { + r := &Resolver{ + TContext: tc, + Map: map[*types.TypeParam]types.Type{}, + } + if len(tParams) != len(tArgs) { + panic(fmt.Errorf("len(tParams)=%d not equal len(tArgs)=%d", len(tParams), len(tArgs))) + } + for i := range tParams { + r.Map[tParams[i]] = tArgs[i] + } + return r +} + +// 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.Map == nil { + return typ // No substitutions to be made. + } + if concrete := r.memo.At(typ); concrete != nil { + return concrete + } + concrete := goTypesCheckerSubst((*types.Checker)(nil), token.NoPos, typ, substMap(r.Map), r.TContext) + r.memo.Set(typ, concrete) + return concrete +} + +// 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 +} + +// ToSlice converts TypeParamList into a slice with the sale 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) + } + return result +} + +// visitor implements ast.Visitor and collects instances of generic types and +// functions into an InstanceSet. +// +// When traversing an AST subtree corresponding to a generic type, method or +// function, Resolver must be provided mapping the type parameters into concrete +// types. +type visitor struct { + instances *InstanceSet + resolver *Resolver + info *types.Info +} + +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. + + ident, ok := n.(*ast.Ident) + if !ok { + return + } + + instance, ok := c.info.Instances[ident] + if !ok { + return + } + + c.instances.Add(Instance{ + Object: c.info.ObjectOf(ident), + TArgs: c.resolver.SubstituteAll(instance.TypeArgs), + }) + return +} + +// seedVisitor implements ast.Visitor that collects information necessary to +// kickstart generic instantiation discovery. +// +// It serves double duty: +// - Builds a map from types.Object instances representing generic types, +// methods and functions to AST nodes that define them. +// - Collects an initial set of generic instantiations in the non-generic code. +type seedVisitor struct { + visitor + objMap map[types.Object]ast.Node +} + +var _ ast.Visitor = &seedVisitor{} + +func (c *seedVisitor) Visit(n ast.Node) ast.Visitor { + // Generic functions, methods and types require type arguments to scan for + // generic instantiations, remember their node for later and do not descend + // further. + switch n := n.(type) { + case *ast.FuncDecl: + obj := c.info.Defs[n.Name] + sig := obj.Type().(*types.Signature) + if sig.TypeParams().Len() != 0 || sig.RecvTypeParams().Len() != 0 { + c.objMap[obj] = n + return nil + } + case *ast.TypeSpec: + obj := c.info.Defs[n.Name] + named, ok := obj.Type().(*types.Named) + if !ok { + break + } + if named.TypeParams().Len() != 0 && named.TypeArgs().Len() == 0 { + c.objMap[obj] = n + return nil + } + } + + // Otherwise check for fully defined instantiations and descend further into + // the AST tree. + c.visitor.Visit(n) + return c +} + +// Collector scans type-checked AST tree and adds discovered generic type and +// function instances to the InstanceSet. +// +// Collector will scan non-generic code for any instantiations of generic types +// or functions and add them to the InstanceSet. Then it will scan generic types +// and function with discovered sets of type arguments for more instantiations, +// until no new ones are discovered. +// +// InstanceSet may contain unprocessed instances of generic types and functions, +// which will be also scanned, for example found in depending packages. +// +// Note that methods of generic types are never added to the InstanceSet, since +// they can be easily inferred from the receiver type instances. +type Collector struct { + TContext *types.Context + Info *types.Info + Instances *InstanceSet +} + +// Scan package files for generic instances. +func (c *Collector) Scan(files ...*ast.File) { + objMap := map[types.Object]ast.Node{} + + // Collect instances of generic objects in non-generic code in the package and + // add then to the existing InstanceSet. + sc := seedVisitor{ + visitor: visitor{ + instances: c.Instances, + resolver: nil, + info: c.Info, + }, + objMap: objMap, + } + for _, file := range files { + ast.Walk(&sc, file) + } + + for !c.Instances.exhausted() { + inst, _ := c.Instances.next() + switch typ := inst.Object.Type().(type) { + case *types.Signature: + v := visitor{ + instances: c.Instances, + resolver: NewResolver(c.TContext, ToSlice(typ.TypeParams()), inst.TArgs), + info: c.Info, + } + ast.Walk(&v, objMap[inst.Object]) + 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]) + + for i := 0; i < typ.NumMethods(); i++ { + method := typ.Method(i) + v.resolver = NewResolver(c.TContext, ToSlice(method.Type().(*types.Signature).RecvTypeParams()), inst.TArgs) + ast.Walk(&v, objMap[method]) + } + } + } +} diff --git a/compiler/internal/typeparams/collect_test.go b/compiler/internal/typeparams/collect_test.go new file mode 100644 index 000000000..78b35cc3a --- /dev/null +++ b/compiler/internal/typeparams/collect_test.go @@ -0,0 +1,334 @@ +package typeparams + +import ( + "go/ast" + "go/token" + "go/types" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/gopherjs/gopherjs/internal/srctesting" + "golang.org/x/tools/go/ast/astutil" +) + +func TestVisitor(t *testing.T) { + // This test verifies that instance collector is able to discover + // instantiations of generic types and functions in all possible contexts. + const src = `package testcase + + type A struct{} + type B struct{} + type C struct{} + type D struct{} + type E struct{} + type F struct{} + type G struct{} + + type typ[T any, V any] []T + func (t *typ[T, V]) method(x T) {} + func fun[U any, W any](x U, y W) {} + + func entry1(arg typ[int8, A]) (result typ[int16, A]) { + fun(1, A{}) + fun[int8, A](1, A{}) + println(fun[int16, A]) + + t := typ[int, A]{} + t.method(0) + (*typ[int32, A]).method(nil, 0) + type x struct{ T []typ[int64, A] } + + return + } + + func entry2[T any](arg typ[int8, T]) (result typ[int16, T]) { + var zeroT T + fun(1, zeroT) + fun[int8, T](1, zeroT) + println(fun[int16, T]) + + t := typ[int, T]{} + t.method(0) + (*typ[int32, T]).method(nil, 0) + type x struct{ T []typ[int64, T] } + + return + } + + type entry3[T any] struct{ + typ[int, T] + field1 struct { field2 typ[int8, T] } + } + func (e entry3[T]) method(arg typ[int8, T]) (result typ[int16, T]) { + var zeroT T + fun(1, zeroT) + fun[int8, T](1, zeroT) + println(fun[int16, T]) + + t := typ[int, T]{} + t.method(0) + (*typ[int32, T]).method(nil, 0) + type x struct{ T []typ[int64, T] } + + return + } + + type entry4 struct{ + typ[int, E] + field1 struct { field2 typ[int8, E] } + } + + type entry5 = typ[int, F] + ` + fset := token.NewFileSet() + file := srctesting.Parse(t, fset, src) + info, pkg := srctesting.Check(t, fset, file) + + lookupObj := func(name string) types.Object { + parts := strings.Split(name, ".") + obj := pkg.Scope().Lookup(parts[0]) + if len(parts) == 1 { + return obj + } + obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), parts[1]) + return obj + } + lookupType := func(name string) types.Type { return lookupObj(name).Type() } + lookupDecl := func(name string) ast.Node { + obj := lookupObj(name) + path, _ := astutil.PathEnclosingInterval(file, obj.Pos(), obj.Pos()) + for _, n := range path { + switch n.(type) { + case *ast.FuncDecl, *ast.TypeSpec: + return n + } + } + t.Fatalf("Could not find AST node representing %v", obj) + return nil + } + + // Generates a list of instances we expect to discover from functions and + // methods. Sentinel type is a type parameter we use uniquely within one + // context, which allows us to make sure that collection is not being tested + // against a wrong part of AST. + instancesInFunc := func(sentinel types.Type) []Instance { + return []Instance{ + { + // Called with type arguments inferred. + Object: lookupObj("fun"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + // Called with type arguments explicitly specified. + Object: lookupObj("fun"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, { + // Passed as an argument. + Object: lookupObj("fun"), + TArgs: []types.Type{types.Typ[types.Int16], sentinel}, + }, { + // Literal expression. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + // Function argument. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, { + // Function return type. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int16], sentinel}, + }, { + // Method expression. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int32], sentinel}, + }, { + // Type decl statement. + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int64], sentinel}, + }, + } + } + + // Generates a list of instances we expect to discover from type declarations. + // Sentinel type is a type parameter we use uniquely within one context, which + // allows us to make sure that collection is not being tested against a wrong + // part of AST. + instancesInType := func(sentinel types.Type) []Instance { + return []Instance{ + { + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, + } + } + + tests := []struct { + descr string + resolver *Resolver + node ast.Node + want []Instance + }{ + { + descr: "non-generic function", + resolver: nil, + node: lookupDecl("entry1"), + want: instancesInFunc(lookupType("A")), + }, { + descr: "generic function", + resolver: NewResolver( + types.NewContext(), + ToSlice(lookupType("entry2").(*types.Signature).TypeParams()), + []types.Type{lookupType("B")}, + ), + node: lookupDecl("entry2"), + want: instancesInFunc(lookupType("B")), + }, { + descr: "generic method", + resolver: NewResolver( + types.NewContext(), + ToSlice(lookupType("entry3.method").(*types.Signature).RecvTypeParams()), + []types.Type{lookupType("C")}, + ), + node: lookupDecl("entry3.method"), + want: append( + instancesInFunc(lookupType("C")), + Instance{ + Object: lookupObj("entry3"), + TArgs: []types.Type{lookupType("C")}, + }, + ), + }, { + descr: "generic type declaration", + resolver: NewResolver( + types.NewContext(), + ToSlice(lookupType("entry3").(*types.Named).TypeParams()), + []types.Type{lookupType("D")}, + ), + node: lookupDecl("entry3"), + want: instancesInType(lookupType("D")), + }, { + descr: "non-generic type declaration", + resolver: nil, + node: lookupDecl("entry4"), + want: instancesInType(lookupType("E")), + }, { + descr: "non-generic type alias", + resolver: nil, + node: lookupDecl("entry5"), + want: []Instance{ + { + Object: lookupObj("typ"), + TArgs: []types.Type{types.Typ[types.Int], lookupType("F")}, + }, + }, + }, + } + + for _, test := range tests { + v := visitor{ + instances: &InstanceSet{}, + resolver: test.resolver, + info: info, + } + ast.Walk(&v, test.node) + got := v.instances.Values() + if diff := cmp.Diff(test.want, got, instanceOpts()); diff != "" { + t.Errorf("Discovered instance diff (-want,+got):\n%s", diff) + } + } +} + +func TestSeedVisitor(t *testing.T) { + src := `package test + type typ[T any] int + func (t typ[T]) method(arg T) { var x typ[string]; _ = x } + func fun[T any](arg T) { var y typ[string]; _ = y } + + const a typ[int] = 1 + var b typ[int] + type c struct { field typ[int8] } + func (_ c) method() { var _ typ[int16] } + type d = typ[int32] + func e() { var _ typ[int64] } + ` + + fset := token.NewFileSet() + file := srctesting.Parse(t, fset, src) + info, pkg := srctesting.Check(t, fset, file) + + sv := seedVisitor{ + visitor: visitor{ + instances: &InstanceSet{}, + resolver: nil, + info: info, + }, + objMap: map[types.Object]ast.Node{}, + } + ast.Walk(&sv, file) + + inst := func(tArg types.Type) Instance { + return Instance{ + Object: pkg.Scope().Lookup("typ"), + TArgs: []types.Type{tArg}, + } + } + want := []Instance{ + inst(types.Typ[types.Int]), + inst(types.Typ[types.Int8]), + inst(types.Typ[types.Int16]), + inst(types.Typ[types.Int32]), + inst(types.Typ[types.Int64]), + } + got := sv.instances.Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != "" { + t.Errorf("Instances from initialSeeder contain diff (-want,+got):\n%s", diff) + } +} + +func TestCollector(t *testing.T) { + src := `package test + type typ[T any] int + func (t typ[T]) method(arg T) { var _ typ[int]; fun[int8](0) } + func fun[T any](arg T) { var _ typ[int16] } + + type ignore = int + + func a() { + var _ typ[int32] + fun[int64](0) + } + ` + + fset := token.NewFileSet() + file := srctesting.Parse(t, fset, src) + info, pkg := srctesting.Check(t, fset, file) + + c := Collector{ + TContext: types.NewContext(), + Info: info, + Instances: &InstanceSet{}, + } + c.Scan(file) + + inst := func(name string, tArg types.Type) Instance { + return Instance{ + Object: pkg.Scope().Lookup(name), + TArgs: []types.Type{tArg}, + } + } + want := []Instance{ + inst("typ", types.Typ[types.Int]), + inst("fun", types.Typ[types.Int8]), + inst("typ", types.Typ[types.Int16]), + inst("typ", types.Typ[types.Int32]), + inst("fun", types.Typ[types.Int64]), + } + got := c.Instances.Values() + if diff := cmp.Diff(want, got, instanceOpts()); diff != "" { + t.Errorf("Instances from initialSeeder contain diff (-want,+got):\n%s", diff) + } +} diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go new file mode 100644 index 000000000..b42ccdfdb --- /dev/null +++ b/compiler/internal/typeparams/instance.go @@ -0,0 +1,86 @@ +package typeparams + +import ( + "go/types" + "strings" + + "github.com/gopherjs/gopherjs/compiler/internal/symbol" + "golang.org/x/exp/maps" +) + +// Instance of a generic type or function. +// +// Non-generic objects can be represented as an Instance with zero type params, +// they are instances of themselves. +type Instance struct { + Object types.Object // Object to be instantiated. + TArgs []types.Type // Type params to instantiate with. +} + +// ID returns a string that uniquely identifies an instantiation of the generic +// object with the provided type arguments. +func (i *Instance) ID() string { + buf := strings.Builder{} + buf.WriteString(symbol.New(i.Object).String()) + if len(i.TArgs) == 0 { + return buf.String() + } + + buf.WriteRune('<') + for i, tArg := range i.TArgs { + if i != 0 { + buf.WriteString(", ") + } + buf.WriteString(types.TypeString(tArg, nil)) + } + buf.WriteRune('>') + return buf.String() +} + +// InstanceSet allows collecting and processing unique Instances. +// +// Each Instance may be added to the set any number of times, but it will be +// returned for processing exactly once. Processing order is not specified. +type InstanceSet struct { + unprocessed []Instance + seen map[string]Instance +} + +// Add instances to the set. Instances that have been previously added to the +// set won't be requeued for processing regardless of whether they have been +// processed already. +func (iset *InstanceSet) Add(instances ...Instance) *InstanceSet { + for _, inst := range instances { + if _, ok := iset.seen[inst.ID()]; ok { + continue + } + if iset.seen == nil { + iset.seen = map[string]Instance{} + } + iset.unprocessed = append(iset.unprocessed, inst) + + iset.seen[inst.ID()] = inst + } + return iset +} + +// next returns the next Instance to be processed. +// +// If there are no unprocessed instances, the second returned value will be false. +func (iset *InstanceSet) next() (Instance, bool) { + if iset.exhausted() { + return Instance{}, false + } + idx := len(iset.unprocessed) - 1 + next := iset.unprocessed[idx] + iset.unprocessed = iset.unprocessed[0:idx] + return next, true +} + +// exhausted returns true if there are no unprocessed instances in the set. +func (iset *InstanceSet) exhausted() bool { return len(iset.unprocessed) == 0 } + +// Values returns instances that are currently in the set. Order is not specified. +func (iset *InstanceSet) Values() []Instance { + return maps.Values(iset.seen) +} diff --git a/compiler/internal/typeparams/instance_test.go b/compiler/internal/typeparams/instance_test.go new file mode 100644 index 000000000..b461f7f68 --- /dev/null +++ b/compiler/internal/typeparams/instance_test.go @@ -0,0 +1,197 @@ +package typeparams + +import ( + "go/token" + "go/types" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/gopherjs/gopherjs/internal/srctesting" + "github.com/gopherjs/gopherjs/internal/testingx" +) + +func instanceOpts() cmp.Options { + return cmp.Options{ + // Instances are represented by their IDs for diffing purposes. + cmp.Transformer("InstanceID", func(i Instance) string { + return i.ID() + }), + // Order of instances in a slice doesn't matter, sort them by ID. + cmpopts.SortSlices(func(a, b Instance) bool { + return a.ID() < b.ID() + }), + } +} + +func TestInstanceID(t *testing.T) { + const src = `package testcase + + type Ints []int + + type Typ[T any, V any] []T + func (t Typ[T, V]) Method(x T) {} + + type typ[T any, V any] []T + func (t typ[T, V]) method(x T) {} + + func Fun[U any, W any](x, y U) {} + func fun[U any, W any](x, y U) {} + ` + fset := token.NewFileSet() + _, pkg := srctesting.Check(t, fset, srctesting.Parse(t, fset, src)) + mustType := testingx.Must[types.Type](t) + + tests := []struct { + descr string + instance Instance + want string + }{{ + descr: "exported type", + instance: Instance{ + Object: pkg.Scope().Lookup("Typ"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + want: "test.Typ", + }, { + descr: "exported method", + instance: Instance{ + Object: pkg.Scope().Lookup("Typ").Type().(*types.Named).Method(0), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + want: "test.Typ.Method", + }, { + descr: "exported function", + instance: Instance{ + Object: pkg.Scope().Lookup("Fun"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + want: "test.Fun", + }, { + descr: "unexported type", + instance: Instance{ + Object: pkg.Scope().Lookup("typ"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + want: "test.typ", + }, { + descr: "unexported method", + instance: Instance{ + Object: pkg.Scope().Lookup("typ").Type().(*types.Named).Method(0), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + want: "test.typ.method", + }, { + descr: "unexported function", + instance: Instance{ + Object: pkg.Scope().Lookup("fun"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, + }, + want: "test.fun", + }, { + descr: "no type params", + instance: Instance{ + Object: pkg.Scope().Lookup("Ints"), + }, + want: "test.Ints", + }, { + descr: "complex parameter type", + instance: Instance{ + Object: pkg.Scope().Lookup("fun"), + TArgs: []types.Type{ + types.NewSlice(types.Typ[types.Int]), + mustType(types.Instantiate(nil, pkg.Scope().Lookup("typ").Type(), []types.Type{ + types.Typ[types.Int], + types.Typ[types.String], + }, true)), + }, + }, + want: "test.fun<[]int, test.typ[int, string]>", + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + got := test.instance.ID() + if got != test.want { + t.Errorf("Got: instance ID %q. Want: %q.", got, test.want) + } + }) + } +} + +func TestInstanceQueue(t *testing.T) { + const src = `package test + type Typ[T any, V any] []T + func Fun[U any, W any](x, y U) {} + ` + fset := token.NewFileSet() + _, pkg := srctesting.Check(t, fset, srctesting.Parse(t, fset, src)) + + i1 := Instance{ + Object: pkg.Scope().Lookup("Typ"), + TArgs: []types.Type{types.Typ[types.String], types.Typ[types.String]}, + } + i2 := Instance{ + Object: pkg.Scope().Lookup("Typ"), + TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.Int]}, + } + i3 := Instance{ + Object: pkg.Scope().Lookup("Fun"), + TArgs: []types.Type{types.Typ[types.String], types.Typ[types.String]}, + } + + set := InstanceSet{} + set.Add(i1, i2) + + if ex := set.exhausted(); ex { + t.Errorf("Got: set.exhausted() = true. Want: false") + } + + gotValues := set.Values() + wantValues := []Instance{i1, i2} + if diff := cmp.Diff(wantValues, gotValues, instanceOpts()); diff != "" { + t.Errorf("set.Values() returned diff (-want,+got):\n%s", diff) + } + + p1, ok := set.next() + if !ok { + t.Errorf("Got: _, ok := set.next(); ok == false. Want: true.") + } + p2, ok := set.next() + if !ok { + t.Errorf("Got: _, ok := set.next(); ok == false. Want: true.") + } + if ex := set.exhausted(); !ex { + t.Errorf("Got: set.exhausted() = false. Want: true") + } + + _, ok = set.next() + if ok { + t.Errorf("Got: _, ok := set.next(); ok == true. Want: false.") + } + + set.Add(i1) // Has been enqueued before. + if ex := set.exhausted(); !ex { + t.Errorf("Got: set.exhausted() = false. Want: true") + } + + set.Add(i3) + p3, ok := set.next() + if !ok { + t.Errorf("Got: _, ok := set.next(); ok == false. Want: true.") + } + + added := []Instance{i1, i2, i3} + processed := []Instance{p1, p2, p3} + + diff := cmp.Diff(added, processed, instanceOpts()) + if diff != "" { + t.Errorf("Processed instances differ from added (-want,+got):\n%s", diff) + } + + gotValues = set.Values() + wantValues = []Instance{i1, i2, i3} + if diff := cmp.Diff(wantValues, gotValues, instanceOpts()); diff != "" { + t.Errorf("set.Values() returned diff (-want,+got):\n%s", diff) + } +} diff --git a/compiler/internal/typeparams/subst.go b/compiler/internal/typeparams/subst.go new file mode 100644 index 000000000..ad1be7c48 --- /dev/null +++ b/compiler/internal/typeparams/subst.go @@ -0,0 +1,13 @@ +package typeparams + +import ( + "go/token" + "go/types" + + _ "unsafe" // For go:linkname. +) + +type substMap map[*types.TypeParam]types.Type + +//go:linkname goTypesCheckerSubst go/types.(*Checker).subst +func goTypesCheckerSubst(check *types.Checker, pos token.Pos, typ types.Type, smap substMap, ctxt *types.Context) types.Type diff --git a/compiler/linkname.go b/compiler/linkname.go index ae1e3ea2b..0bb2b3509 100644 --- a/compiler/linkname.go +++ b/compiler/linkname.go @@ -4,10 +4,10 @@ import ( "fmt" "go/ast" "go/token" - "go/types" "strings" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/symbol" ) // GoLinkname describes a go:linkname compiler directive found in the source code. @@ -17,62 +17,8 @@ import ( // symbols referencing it. This is subtly different from the upstream Go // implementation, which simply overrides symbol name the linker will use. type GoLinkname struct { - Implementation SymName - Reference SymName -} - -// SymName uniquely identifies a named submol within a program. -// -// This is a logical equivalent of a symbol name used by traditional linkers. -// The following properties should hold true: -// -// - Each named symbol within a program has a unique SymName. -// - Similarly named methods of different types will have different symbol names. -// - The string representation is opaque and should not be attempted to reversed -// to a struct form. -type SymName struct { - PkgPath string // Full package import path. - Name string // Symbol name. -} - -// newSymName constructs SymName for a given named symbol. -func newSymName(o types.Object) SymName { - if fun, ok := o.(*types.Func); ok { - sig := fun.Type().(*types.Signature) - if recv := sig.Recv(); recv != nil { - // Special case: disambiguate names for different types' methods. - typ := recv.Type() - if ptr, ok := typ.(*types.Pointer); ok { - return SymName{ - PkgPath: o.Pkg().Path(), - Name: "(*" + ptr.Elem().(*types.Named).Obj().Name() + ")." + o.Name(), - } - } - return SymName{ - PkgPath: o.Pkg().Path(), - Name: typ.(*types.Named).Obj().Name() + "." + o.Name(), - } - } - } - return SymName{ - PkgPath: o.Pkg().Path(), - Name: o.Name(), - } -} - -func (n SymName) String() string { return n.PkgPath + "." + n.Name } - -func (n SymName) IsMethod() (recv string, method string, ok bool) { - pos := strings.IndexByte(n.Name, '.') - if pos == -1 { - return - } - recv, method, ok = n.Name[:pos], n.Name[pos+1:], true - size := len(recv) - if size > 2 && recv[0] == '(' && recv[size-1] == ')' { - recv = recv[1 : size-1] - } - return + Implementation symbol.Name + Reference symbol.Name } // parseGoLinknames processed comments in a source file and extracts //go:linkname @@ -152,8 +98,8 @@ func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]Go } // Local function has no body, treat it as a reference to an external implementation. directives = append(directives, GoLinkname{ - Reference: SymName{PkgPath: localPkg, Name: localName}, - Implementation: SymName{PkgPath: extPkg, Name: extName}, + Reference: symbol.Name{PkgPath: localPkg, Name: localName}, + Implementation: symbol.Name{PkgPath: extPkg, Name: extName}, }) return nil } @@ -172,17 +118,17 @@ func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]Go // 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 { - byImplementation map[SymName][]GoLinkname - byReference map[SymName]GoLinkname + byImplementation map[symbol.Name][]GoLinkname + byReference map[symbol.Name]GoLinkname } // Add more GoLinkname directives into the set. func (gls *goLinknameSet) Add(entries []GoLinkname) error { if gls.byImplementation == nil { - gls.byImplementation = map[SymName][]GoLinkname{} + gls.byImplementation = map[symbol.Name][]GoLinkname{} } if gls.byReference == nil { - gls.byReference = map[SymName]GoLinkname{} + gls.byReference = map[symbol.Name]GoLinkname{} } for _, e := range entries { gls.byImplementation[e.Implementation] = append(gls.byImplementation[e.Implementation], e) @@ -197,7 +143,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 SymName) bool { +func (gls *goLinknameSet) IsImplementation(sym symbol.Name) bool { _, found := gls.byImplementation[sym] return found } @@ -205,7 +151,7 @@ func (gls *goLinknameSet) IsImplementation(sym SymName) 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 SymName) (SymName, 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_test.go index d0ce9c542..7f46c6cfb 100644 --- a/compiler/linkname_test.go +++ b/compiler/linkname_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/gopherjs/gopherjs/compiler/internal/symbol" ) func parseSource(t *testing.T, src string) (*ast.File, *token.FileSet) { @@ -41,49 +42,6 @@ func makePackage(t *testing.T, src string) *types.Package { return pkg } -func TestSymName(t *testing.T) { - pkg := makePackage(t, - `package testcase - - func AFunction() {} - type AType struct {} - func (AType) AMethod() {} - func (*AType) APointerMethod() {} - var AVariable int32 - `) - - tests := []struct { - obj types.Object - want SymName - }{ - { - obj: pkg.Scope().Lookup("AFunction"), - want: SymName{PkgPath: "testcase", Name: "AFunction"}, - }, { - obj: pkg.Scope().Lookup("AType"), - want: SymName{PkgPath: "testcase", Name: "AType"}, - }, { - obj: types.NewMethodSet(pkg.Scope().Lookup("AType").Type()).Lookup(pkg, "AMethod").Obj(), - want: SymName{PkgPath: "testcase", Name: "AType.AMethod"}, - }, { - obj: types.NewMethodSet(types.NewPointer(pkg.Scope().Lookup("AType").Type())).Lookup(pkg, "APointerMethod").Obj(), - want: SymName{PkgPath: "testcase", Name: "(*AType).APointerMethod"}, - }, { - obj: pkg.Scope().Lookup("AVariable"), - want: SymName{PkgPath: "testcase", Name: "AVariable"}, - }, - } - - for _, test := range tests { - t.Run(test.obj.Name(), func(t *testing.T) { - got := newSymName(test.obj) - if got != test.want { - t.Errorf("NewSymName(%q) returned %#v, want: %#v", test.obj.Name(), got, test.want) - } - }) - } -} - func TestParseGoLinknames(t *testing.T) { tests := []struct { desc string @@ -114,8 +72,8 @@ func TestParseGoLinknames(t *testing.T) { `, wantDirectives: []GoLinkname{ { - Reference: SymName{PkgPath: "testcase", Name: "a"}, - Implementation: SymName{PkgPath: "other/package", Name: "testcase_a"}, + Reference: symbol.Name{PkgPath: "testcase", Name: "a"}, + Implementation: symbol.Name{PkgPath: "other/package", Name: "testcase_a"}, }, }, }, { @@ -132,11 +90,11 @@ func TestParseGoLinknames(t *testing.T) { `, wantDirectives: []GoLinkname{ { - Reference: SymName{PkgPath: "testcase", Name: "a"}, - Implementation: SymName{PkgPath: "other/package", Name: "a"}, + Reference: symbol.Name{PkgPath: "testcase", Name: "a"}, + Implementation: symbol.Name{PkgPath: "other/package", Name: "a"}, }, { - Reference: SymName{PkgPath: "testcase", Name: "b"}, - Implementation: SymName{PkgPath: "other/package", Name: "b"}, + Reference: symbol.Name{PkgPath: "testcase", Name: "b"}, + Implementation: symbol.Name{PkgPath: "other/package", Name: "b"}, }, }, }, { diff --git a/compiler/package.go b/compiler/package.go index 403f9a355..8bf75efe9 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -14,6 +14,7 @@ import ( "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/neelance/astrewrite" "golang.org/x/tools/go/gcexportdata" "golang.org/x/tools/go/types/typeutil" @@ -404,7 +405,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor FullName: o.FullName(), Blocking: len(funcInfo.Blocking) != 0, } - d.LinkingName = newSymName(o) + d.LinkingName = symbol.New(o) if fun.Recv == nil { d.Vars = []string{funcCtx.objectName(o)} d.DceObjectFilter = o.Name() diff --git a/compiler/typesutil/map.go b/compiler/typesutil/map.go new file mode 100644 index 000000000..146f09765 --- /dev/null +++ b/compiler/typesutil/map.go @@ -0,0 +1,34 @@ +package typesutil + +import ( + "go/types" + + "golang.org/x/tools/go/types/typeutil" +) + +// Map is a type-safe wrapper around golang.org/x/tools/go/types/typeutil.Map. +type Map[Val any] struct{ impl typeutil.Map } + +func (m *Map[Val]) At(key types.Type) Val { + val := m.impl.At(key) + if val != nil { + return val.(Val) + } + var zero Val + return zero +} + +func (m *Map[Val]) Set(key types.Type, value Val) (prev Val) { + old := m.impl.Set(key, value) + if old != nil { + return old.(Val) + } + var zero Val + return zero +} + +func (m *Map[Val]) Delete(key types.Type) bool { return m.impl.Delete(key) } + +func (m *Map[Val]) Len() int { return m.impl.Len() } + +func (m *Map[Val]) String() string { return m.impl.String() } diff --git a/go.mod b/go.mod index 8edafd89b..19d78c54e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/evanw/esbuild v0.18.0 github.com/fsnotify/fsnotify v1.5.1 - github.com/google/go-cmp v0.5.7 + github.com/google/go-cmp v0.5.8 github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 @@ -13,13 +13,13 @@ 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/sync v0.3.0 + golang.org/x/sync v0.5.0 golang.org/x/sys v0.10.0 golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 - golang.org/x/tools v0.11.0 + golang.org/x/tools v0.16.0 ) require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect - golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect + golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 ) diff --git a/go.sum b/go.sum index 349d599ba..56fd0e995 100644 --- a/go.sum +++ b/go.sum @@ -120,8 +120,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -270,6 +270,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= +golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= 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= @@ -295,7 +297,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= 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= @@ -355,8 +357,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.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= @@ -467,14 +469,12 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= -golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/tools v0.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= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= diff --git a/internal/srctesting/srctesting.go b/internal/srctesting/srctesting.go index 1d9cecd20..3da0be22b 100644 --- a/internal/srctesting/srctesting.go +++ b/internal/srctesting/srctesting.go @@ -37,6 +37,7 @@ func Check(t *testing.T, fset *token.FileSet, files ...*ast.File) (*types.Info, 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), } config := &types.Config{ Sizes: &types.StdSizes{WordSize: 4, MaxAlign: 8}, diff --git a/internal/testingx/must.go b/internal/testingx/must.go new file mode 100644 index 000000000..9b289f5b0 --- /dev/null +++ b/internal/testingx/must.go @@ -0,0 +1,24 @@ +// Package testingx provides helpers for use with the testing package. +package testingx + +import "testing" + +// Must provides a concise way to handle handle returned error in tests that +// "should never happen"©. +// +// This function can be used in test case setup that can be presumed to be +// correct, but technically may return an error. This function MUST NOT be used +// to check for test case conditions themselves because it provides a generic, +// nondescript test error message. +// +// func startServer(addr string) (*server, err) +// mustServer := testingx.Must[*server](t) +// mustServer(startServer(":8080")) +func Must[T any](t *testing.T) func(v T, err error) T { + return func(v T, err error) T { + if err != nil { + t.Fatalf("Got: unexpected error: %s. Want: no error.", err) + } + return v + } +} From af64433d555dbed04b4cefb27eb26cab7edb1a24 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 24 Dec 2023 23:26:40 +0000 Subject: [PATCH 04/28] Instantiate generic functions for each discovered set of type args. When translating a function with type params, we translate it for each discovered combination of type arguments. Like regular functions, generic ones are assigned a variable name, but instead of representing the function directly, it is mapping from instance keys to type-specific translation of the function. The key is a string that is derived from type arguments and is guaranteed to be different for different set of type arguments. To make this possible, I've make following changes: - For functions we now emit two decls, one that reserves the variable name, and one per instantiation that generates function body. This is necessary to make sure variable containing generic functions is initialized once. - Decl struct now has a new RefExpr field that contains a JS expression referencing the object the decl represents. This is necessary because the variable name that was previously used for linkname purposes is now stored in a different decl as explained above. - Mapping from object names to variable names is now stored in funcContext. This is necessary since each generic function is translated several times and we need to create a new local variable for objects within it each time. - Added handling of ast.IndexExpr and ast.IndexListExpr in the compiler for the purposes of specifying type parameters. At this stage, the implementation is incomplete in several major ways: - Generic types are not supported. - Generic instantiation discovery is only possible within a single package. Detecting instantiations from other packages will require changes to the build package. - When translating generic functions we do not yet apply type parameter to type argument mapping. - DCE does not distinguish between different instantiations of the same object and doesn't properly handle variable reservation decls. All those issues will be addressed in later commits. --- compiler/compiler.go | 8 +- compiler/expressions.go | 24 ++- compiler/internal/typeparams/collect.go | 3 + compiler/internal/typeparams/instance.go | 23 ++- compiler/internal/typeparams/instance_test.go | 37 ++-- compiler/package.go | 158 ++++++++++++------ compiler/statements.go | 2 +- compiler/utils.go | 54 +++++- tests/gorepo/run.go | 9 - 9 files changed, 229 insertions(+), 89 deletions(-) diff --git a/compiler/compiler.go b/compiler/compiler.go index b3b77d168..1614b8a7f 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -116,6 +116,10 @@ type Decl struct { LinkingName symbol.Name // A list of package-level JavaScript variable names this symbol needs to declare. Vars []string + // A JS expression by which the object represented by this decl may be + // referenced within the package context. Empty if the decl represents no such + // object. + RefExpr string // NamedRecvType is method named recv declare. NamedRecvType string // JavaScript code that declares basic information about a symbol. For a type @@ -327,7 +331,7 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameS if recv, method, ok := d.LinkingName.IsMethod(); ok { code = fmt.Sprintf("\t$linknames[%q] = $unsafeMethodToFunction(%v,%q,%t);\n", d.LinkingName.String(), d.NamedRecvType, method, strings.HasPrefix(recv, "*")) } else { - code = fmt.Sprintf("\t$linknames[%q] = %s;\n", d.LinkingName.String(), d.Vars[0]) + code = fmt.Sprintf("\t$linknames[%q] = %s;\n", d.LinkingName.String(), d.RefExpr) } if _, err := w.Write(removeWhitespace([]byte(code), minify)); err != nil { return err @@ -358,7 +362,7 @@ func WritePkgCode(pkg *Archive, dceSelection map[*Decl]struct{}, gls goLinknameS 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.Vars[0], impl.String())) + lines = append(lines, fmt.Sprintf("\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, "")) diff --git a/compiler/expressions.go b/compiler/expressions.go index db08cc31f..5b5bee93a 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -13,6 +13,7 @@ import ( "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) @@ -203,11 +204,16 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } case *ast.FuncLit: - _, fun := translateFunction(e.Type, nil, e.Body, fc, exprType.(*types.Signature), fc.pkgCtx.FuncLitInfos[e], "") + _, fun := translateFunction(e.Type, nil, e.Body, fc, exprType.(*types.Signature), fc.pkgCtx.FuncLitInfos[e], "", typeparams.Instance{}) if len(fc.pkgCtx.escapingVars) != 0 { names := make([]string, 0, len(fc.pkgCtx.escapingVars)) for obj := range fc.pkgCtx.escapingVars { - names = append(names, fc.pkgCtx.objectNames[obj]) + name, ok := fc.assignedObjectName(obj) + if !ok { + // This should never happen. + panic(fmt.Errorf("escaping variable %s hasn't been assigned a JS name", obj)) + } + names = append(names, name) } sort.Strings(names) list := strings.Join(names, ", ") @@ -240,7 +246,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case *ast.Ident: obj := fc.pkgCtx.Uses[x].(*types.Var) if fc.pkgCtx.escapingVars[obj] { - return fc.formatExpr("(%1s.$ptr || (%1s.$ptr = new %2s(function() { return this.$target[0]; }, function($v) { this.$target[0] = $v; }, %1s)))", fc.pkgCtx.objectNames[obj], fc.typeName(exprType)) + return fc.formatExpr("(%1s.$ptr || (%1s.$ptr = new %2s(function() { return this.$target[0]; }, function($v) { this.$target[0] = $v; }, %1s)))", fc.objectName(obj), fc.typeName(exprType)) } return fc.formatExpr(`(%1s || (%1s = new %2s(function() { return %3s; }, function($v) { %4s })))`, fc.varPtrName(obj), fc.typeName(exprType), fc.objectName(obj), fc.translateAssign(x, fc.newIdent("$v", elemType), false)) case *ast.SelectorExpr: @@ -520,10 +526,18 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { ) case *types.Basic: return fc.formatExpr("%e.charCodeAt(%f)", e.X, e.Index) + case *types.Signature: + return fc.formatExpr("%s", fc.instName(fc.instanceOf(e.X.(*ast.Ident)))) default: panic(fmt.Sprintf("Unhandled IndexExpr: %T\n", t)) } - + case *ast.IndexListExpr: + switch t := fc.pkgCtx.TypeOf(e.X).Underlying().(type) { + case *types.Signature: + return fc.formatExpr("%s", fc.instName(fc.instanceOf(e.X.(*ast.Ident)))) + default: + panic(fmt.Errorf("unhandled IndexListExpr: %T", t)) + } case *ast.SliceExpr: if b, isBasic := fc.pkgCtx.TypeOf(e.X).Underlying().(*types.Basic); isBasic && isString(b) { switch { @@ -777,7 +791,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case *types.Var, *types.Const: return fc.formatExpr("%s", fc.objectName(o)) case *types.Func: - return fc.formatExpr("%s", fc.objectName(o)) + return fc.formatExpr("%s", fc.instName(fc.instanceOf(e))) case *types.TypeName: return fc.formatExpr("%s", fc.typeName(o.Type())) case *types.Nil: diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index 7c04125f4..fac958d93 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -166,6 +166,9 @@ type Collector struct { // Scan package files for generic instances. func (c *Collector) Scan(files ...*ast.File) { + if c.Info.Instances == nil || c.Info.Defs == nil { + panic(fmt.Errorf("types.Info must have Instances and Defs populated")) + } objMap := map[types.Object]ast.Node{} // Collect instances of generic objects in non-generic code in the package and diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index b42ccdfdb..760f68f3b 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -1,6 +1,7 @@ package typeparams import ( + "fmt" "go/types" "strings" @@ -20,23 +21,35 @@ type Instance struct { // ID returns a string that uniquely identifies an instantiation of the generic // object with the provided type arguments. func (i *Instance) ID() string { - buf := strings.Builder{} - buf.WriteString(symbol.New(i.Object).String()) + sym := symbol.New(i.Object).String() if len(i.TArgs) == 0 { - return buf.String() + return sym } - buf.WriteRune('<') + return fmt.Sprintf("%s<%s>", sym, i.Key()) +} + +// Key returns a string that uniquely identifies this instance among other +// instances of this particular object. +// +// Although in practice it is derived from type arguments, no particular +// guarantees are made about format of content of the string. +func (i *Instance) Key() string { + buf := strings.Builder{} for i, tArg := range i.TArgs { if i != 0 { buf.WriteString(", ") } buf.WriteString(types.TypeString(tArg, nil)) } - buf.WriteRune('>') 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 +} + // 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/instance_test.go b/compiler/internal/typeparams/instance_test.go index b461f7f68..70ca7fd2a 100644 --- a/compiler/internal/typeparams/instance_test.go +++ b/compiler/internal/typeparams/instance_test.go @@ -24,7 +24,7 @@ func instanceOpts() cmp.Options { } } -func TestInstanceID(t *testing.T) { +func TestInstanceKeyAndID(t *testing.T) { const src = `package testcase type Ints []int @@ -45,55 +45,63 @@ func TestInstanceID(t *testing.T) { tests := []struct { descr string instance Instance - want string + wantID string + wantKey string }{{ descr: "exported type", instance: Instance{ Object: pkg.Scope().Lookup("Typ"), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - want: "test.Typ", + wantID: "test.Typ", + wantKey: "int, string", }, { descr: "exported method", instance: Instance{ Object: pkg.Scope().Lookup("Typ").Type().(*types.Named).Method(0), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - want: "test.Typ.Method", + wantID: "test.Typ.Method", + wantKey: "int, string", }, { descr: "exported function", instance: Instance{ Object: pkg.Scope().Lookup("Fun"), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - want: "test.Fun", + wantID: "test.Fun", + wantKey: "int, string", }, { descr: "unexported type", instance: Instance{ Object: pkg.Scope().Lookup("typ"), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - want: "test.typ", + wantID: "test.typ", + wantKey: "int, string", }, { descr: "unexported method", instance: Instance{ Object: pkg.Scope().Lookup("typ").Type().(*types.Named).Method(0), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - want: "test.typ.method", + wantID: "test.typ.method", + wantKey: "int, string", }, { descr: "unexported function", instance: Instance{ Object: pkg.Scope().Lookup("fun"), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - want: "test.fun", + wantID: "test.fun", + wantKey: "int, string", }, { descr: "no type params", instance: Instance{ Object: pkg.Scope().Lookup("Ints"), }, - want: "test.Ints", + wantID: "test.Ints", + wantKey: "", }, { descr: "complex parameter type", instance: Instance{ @@ -106,14 +114,19 @@ func TestInstanceID(t *testing.T) { }, true)), }, }, - want: "test.fun<[]int, test.typ[int, string]>", + wantID: "test.fun<[]int, test.typ[int, string]>", + wantKey: "[]int, test.typ[int, string]", }} for _, test := range tests { t.Run(test.descr, func(t *testing.T) { got := test.instance.ID() - if got != test.want { - t.Errorf("Got: instance ID %q. Want: %q.", got, test.want) + if got != test.wantID { + t.Errorf("Got: instance ID %q. Want: %q.", got, test.wantID) + } + got = test.instance.Key() + if got != test.wantKey { + t.Errorf("Got: instance key %q. Want: %q.", got, test.wantKey) } }) } diff --git a/compiler/package.go b/compiler/package.go index 8bf75efe9..4f2711227 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -15,6 +15,7 @@ import ( "github.com/gopherjs/gopherjs/compiler/analysis" "github.com/gopherjs/gopherjs/compiler/astutil" "github.com/gopherjs/gopherjs/compiler/internal/symbol" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/neelance/astrewrite" "golang.org/x/tools/go/gcexportdata" "golang.org/x/tools/go/types/typeutil" @@ -25,9 +26,9 @@ type pkgContext struct { *analysis.Info additionalSelections map[*ast.SelectorExpr]selection + typesCtx *types.Context typeNames []*types.TypeName pkgVars map[string]string - objectNames map[types.Object]string varPtrNames map[*types.Var]string anonTypes []*types.TypeName anonTypeMap typeutil.Map @@ -87,6 +88,9 @@ type funcContext struct { delayedOutput []byte posAvailable bool pos token.Pos + typeResolver *typeparams.Resolver + // Mapping from function-level objects to JS variable names they have been assigned. + objectNames map[types.Object]string } type flowData struct { @@ -150,6 +154,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor Implicits: make(map[ast.Node]types.Object), Selections: make(map[*ast.SelectorExpr]*types.Selection), Scopes: make(map[ast.Node]*types.Scope), + Instances: make(map[*ast.Ident]types.Instance), } var errList ErrorList @@ -171,6 +176,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor var importError error var previousErr error config := &types.Config{ + Context: types.NewContext(), Importer: packageImporter{ importContext: importContext, importError: &importError, @@ -230,6 +236,18 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor } panic(fullName) } + + tc := typeparams.Collector{ + TContext: config.Context, + Info: typesInfo, + Instances: &typeparams.InstanceSet{}, + } + tc.Scan(simplifiedFiles...) + instancesByObj := map[symbol.Name][]typeparams.Instance{} + for _, inst := range tc.Instances.Values() { + instancesByObj[symbol.New(inst.Object)] = append(instancesByObj[symbol.New(inst.Object)], inst) + } + pkgInfo := analysis.AnalyzePkg(simplifiedFiles, fileSet, typesInfo, typesPkg, isBlocking) funcCtx := &funcContext{ FuncInfo: pkgInfo.InitFuncInfo, @@ -237,8 +255,8 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor Info: pkgInfo, additionalSelections: make(map[*ast.SelectorExpr]selection), + typesCtx: config.Context, pkgVars: make(map[string]string), - objectNames: make(map[types.Object]string), varPtrNames: make(map[*types.Var]string), escapingVars: make(map[*types.Var]bool), indentation: 1, @@ -250,6 +268,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor flowDatas: map[*types.Label]*flowData{nil: {}}, caseCounter: 1, labelCases: make(map[*types.Label]int), + objectNames: map[types.Object]string{}, } for name := range reservedKeywords { funcCtx.allVars[name] = 1 @@ -400,49 +419,73 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor for _, fun := range functions { o := funcCtx.pkgCtx.Defs[fun.Name].(*types.Func) - funcInfo := funcCtx.pkgCtx.FuncDeclInfos[o] - d := Decl{ - FullName: o.FullName(), - Blocking: len(funcInfo.Blocking) != 0, + var instances []typeparams.Instance + if o.Type().(*types.Signature).TypeParams().Len() != 0 { + instances = instancesByObj[symbol.New(o)] + } else { + instances = []typeparams.Instance{{Object: o}} } - d.LinkingName = symbol.New(o) + if fun.Recv == nil { - d.Vars = []string{funcCtx.objectName(o)} - d.DceObjectFilter = o.Name() - switch o.Name() { - case "main": - mainFunc = o - d.DceObjectFilter = "" - case "init": - d.InitCode = funcCtx.CatchOutput(1, func() { - id := funcCtx.newIdent("", types.NewSignature(nil, nil, nil, false)) - funcCtx.pkgCtx.Uses[id] = o - call := &ast.CallExpr{Fun: id} - if len(funcCtx.pkgCtx.FuncDeclInfos[o].Blocking) != 0 { - funcCtx.Blocking[call] = true - } - funcCtx.translateStmt(&ast.ExprStmt{X: call}, nil) + // 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 of all + // instances are dead. + varDecl := Decl{} + varDecl.Vars = []string{funcCtx.objectName(o)} + if o.Type().(*types.Signature).TypeParams().Len() != 0 { + varDecl.DeclCode = funcCtx.CatchOutput(0, func() { + funcCtx.Printf("%s = {};", funcCtx.objectName(o)) }) - d.DceObjectFilter = "" } - } else { - recvType := o.Type().(*types.Signature).Recv().Type() - ptr, isPointer := recvType.(*types.Pointer) - namedRecvType, _ := recvType.(*types.Named) - if isPointer { - namedRecvType = ptr.Elem().(*types.Named) + funcDecls = append(funcDecls, &varDecl) + } + + for _, inst := range instances { + funcInfo := funcCtx.pkgCtx.FuncDeclInfos[o] + d := Decl{ + FullName: o.FullName(), + Blocking: len(funcInfo.Blocking) != 0, } - d.NamedRecvType = funcCtx.objectName(namedRecvType.Obj()) - d.DceObjectFilter = namedRecvType.Obj().Name() - if !fun.Name.IsExported() { - d.DceMethodFilter = o.Name() + "~" + d.LinkingName = symbol.New(o) + if fun.Recv == nil { + d.RefExpr = funcCtx.instName(inst) + d.DceObjectFilter = o.Name() + switch o.Name() { + case "main": + mainFunc = o + d.DceObjectFilter = "" + case "init": + d.InitCode = funcCtx.CatchOutput(1, func() { + id := funcCtx.newIdent("", types.NewSignature(nil, nil, nil, false)) + funcCtx.pkgCtx.Uses[id] = o + call := &ast.CallExpr{Fun: id} + if len(funcCtx.pkgCtx.FuncDeclInfos[o].Blocking) != 0 { + funcCtx.Blocking[call] = true + } + funcCtx.translateStmt(&ast.ExprStmt{X: call}, nil) + }) + d.DceObjectFilter = "" + } + } else { + recvType := o.Type().(*types.Signature).Recv().Type() + ptr, isPointer := recvType.(*types.Pointer) + namedRecvType, _ := recvType.(*types.Named) + if isPointer { + namedRecvType = ptr.Elem().(*types.Named) + } + d.NamedRecvType = funcCtx.objectName(namedRecvType.Obj()) + d.DceObjectFilter = namedRecvType.Obj().Name() + if !fun.Name.IsExported() { + d.DceMethodFilter = o.Name() + "~" + } } - } - d.DceDeps = collectDependencies(func() { - d.DeclCode = funcCtx.translateToplevelFunction(fun, funcInfo) - }) - funcDecls = append(funcDecls, &d) + d.DceDeps = collectDependencies(func() { + d.DeclCode = funcCtx.translateToplevelFunction(fun, funcInfo, inst) + }) + funcDecls = append(funcDecls, &d) + } } if typesPkg.Name() == "main" { if mainFunc == nil { @@ -653,8 +696,8 @@ func (fc *funcContext) initArgs(ty types.Type) string { } } -func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analysis.FuncInfo) []byte { - o := fc.pkgCtx.Defs[fun.Name].(*types.Func) +func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analysis.FuncInfo, inst typeparams.Instance) []byte { + o := inst.Object.(*types.Func) sig := o.Type().(*types.Signature) var recv *ast.Ident if fun.Recv != nil && fun.Recv.List[0].Names != nil { @@ -667,7 +710,7 @@ func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analys return []byte(fmt.Sprintf("\t%s = function() {\n\t\t$throwRuntimeError(\"native function not implemented: %s\");\n\t};\n", funcRef, o.FullName())) } - params, fun := translateFunction(fun.Type, recv, fun.Body, fc, sig, info, funcRef) + params, fun := translateFunction(fun.Type, recv, fun.Body, fc, sig, info, funcRef, inst) joinedParams = strings.Join(params, ", ") return []byte(fmt.Sprintf("\t%s = %s;\n", funcRef, fun)) } @@ -675,7 +718,7 @@ func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analys code := bytes.NewBuffer(nil) if fun.Recv == nil { - funcRef := fc.objectName(o) + funcRef := fc.instName(inst) code.Write(primaryFunction(funcRef)) if fun.Name.IsExported() { fmt.Fprintf(code, "\t$pkg.%s = %s;\n", encodeIdent(fun.Name.Name), funcRef) @@ -719,27 +762,38 @@ func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analys return code.Bytes() } -func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, outerContext *funcContext, sig *types.Signature, info *analysis.FuncInfo, funcRef string) ([]string, string) { +func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, outerContext *funcContext, sig *types.Signature, info *analysis.FuncInfo, funcRef string, inst typeparams.Instance) ([]string, string) { if info == nil { panic("nil info") } c := &funcContext{ - FuncInfo: info, - pkgCtx: outerContext.pkgCtx, - parent: outerContext, - sig: sig, - allVars: make(map[string]int, len(outerContext.allVars)), - localVars: []string{}, - flowDatas: map[*types.Label]*flowData{nil: {}}, - caseCounter: 1, - labelCases: make(map[*types.Label]int), + FuncInfo: info, + pkgCtx: outerContext.pkgCtx, + parent: outerContext, + sig: sig, + allVars: make(map[string]int, len(outerContext.allVars)), + localVars: []string{}, + flowDatas: map[*types.Label]*flowData{nil: {}}, + caseCounter: 1, + labelCases: make(map[*types.Label]int), + typeResolver: outerContext.typeResolver, + objectNames: map[types.Object]string{}, } for k, v := range outerContext.allVars { c.allVars[k] = v } prevEV := c.pkgCtx.escapingVars + if sig.TypeParams().Len() > 0 { + c.typeResolver = typeparams.NewResolver(c.pkgCtx.typesCtx, typeparams.ToSlice(sig.TypeParams()), inst.TArgs) + } else if sig.RecvTypeParams().Len() > 0 { + c.typeResolver = typeparams.NewResolver(c.pkgCtx.typesCtx, typeparams.ToSlice(sig.RecvTypeParams()), inst.TArgs) + } + if c.objectNames == nil { + c.objectNames = map[types.Object]string{} + } + var params []string for _, param := range typ.Params.List { if len(param.Names) == 0 { diff --git a/compiler/statements.go b/compiler/statements.go index f8d791948..c1ec42bc5 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -444,7 +444,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { for _, spec := range decl.Specs { o := fc.pkgCtx.Defs[spec.(*ast.TypeSpec).Name].(*types.TypeName) fc.pkgCtx.typeNames = append(fc.pkgCtx.typeNames, o) - fc.pkgCtx.objectNames[o] = fc.newVariableWithLevel(o.Name(), true) + fc.root().objectNames[o] = fc.newVariableWithLevel(o.Name(), true) fc.pkgCtx.dependencies[o] = true } case token.CONST: diff --git a/compiler/utils.go b/compiler/utils.go index 8b18c29c1..184f1ecce 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -19,9 +19,18 @@ import ( "unicode" "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" ) +// root returns the topmost function context corresponding to the package scope. +func (fc *funcContext) root() *funcContext { + if fc.parent == nil { + return fc + } + return fc.parent.root() +} + func (fc *funcContext) Write(b []byte) (int, error) { fc.writePos() fc.output = append(fc.output, b...) @@ -283,7 +292,7 @@ func (fc *funcContext) newIdent(name string, t types.Type) *ast.Ident { fc.setType(ident, t) obj := types.NewVar(0, fc.pkgCtx.Pkg, name, t) fc.pkgCtx.Uses[ident] = obj - fc.pkgCtx.objectNames[obj] = name + fc.objectNames[obj] = name return ident } @@ -322,6 +331,23 @@ func isPkgLevel(o types.Object) bool { return o.Parent() != nil && o.Parent().Parent() == types.Universe } +// assignedObjectName checks if the object has been previously assigned a name +// in this or one of the parent contexts. If not, found will be false false. +func (fc *funcContext) assignedObjectName(o types.Object) (name string, found bool) { + if fc == nil { + return "", false + } + if name, found := fc.parent.assignedObjectName(o); found { + return name, true + } + + name, found = fc.objectNames[o] + return name, found +} + +// objectName returns a JS expression that refers to the given object. If the +// object hasn't been previously assigned a JS variable name, it will be +// allocated as needed. func (fc *funcContext) objectName(o types.Object) string { if isPkgLevel(o) { fc.pkgCtx.dependencies[o] = true @@ -331,10 +357,10 @@ func (fc *funcContext) objectName(o types.Object) string { } } - name, ok := fc.pkgCtx.objectNames[o] + name, ok := fc.assignedObjectName(o) if !ok { name = fc.newVariableWithLevel(o.Name(), isPkgLevel(o)) - fc.pkgCtx.objectNames[o] = name + fc.objectNames[o] = name } if v, ok := o.(*types.Var); ok && fc.pkgCtx.escapingVars[v] { @@ -343,6 +369,17 @@ func (fc *funcContext) objectName(o types.Object) string { return name } +// instName returns a JS expression that refers to the provided instance of a +// function or type. Non-generic objects may be represented as an instance with +// zero type arguments. +func (fc *funcContext) instName(inst typeparams.Instance) string { + objName := fc.objectName(inst.Object) + if len(inst.TArgs) == 0 { + return objName + } + return fmt.Sprintf("%s[%q]", objName, inst.Key()) +} + func (fc *funcContext) varPtrName(o *types.Var) string { if isPkgLevel(o) && o.Exported() { return fc.pkgVar(o.Pkg()) + "." + o.Name() + "$ptr" @@ -383,6 +420,17 @@ func (fc *funcContext) typeName(ty types.Type) string { return anonType.Name() } +// instanceOf constructs an instance description of the object the ident is +// referring to. For non-generic objects, it will return a trivial instance with +// no type arguments. +func (fc *funcContext) instanceOf(ident *ast.Ident) typeparams.Instance { + inst := typeparams.Instance{Object: fc.pkgCtx.ObjectOf(ident)} + if i, ok := fc.pkgCtx.Instances[ident]; ok { + inst.TArgs = fc.typeResolver.SubstituteAll(i.TypeArgs) + } + return inst +} + func (fc *funcContext) externalize(s string, t types.Type) string { if typesutil.IsJsObject(t) { return s diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 1bebc0cce..8f8cdff27 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -181,14 +181,9 @@ var knownFails = map[string]failReason{ "typeparam/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, "typeparam/issue47258.go": {category: generics, desc: "missing operator support for generic types"}, "typeparam/issue47272.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47514.go": {category: generics, desc: "not triaged"}, - "typeparam/issue47514b.go": {category: generics, desc: "not triaged"}, - "typeparam/issue47684.go": {category: generics, desc: "not triaged"}, "typeparam/issue47684b.go": {category: generics, desc: "not triaged"}, - "typeparam/issue47684c.go": {category: generics, desc: "not triaged"}, "typeparam/issue47713.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue47716.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47723.go": {category: generics, desc: "not triaged"}, "typeparam/issue47740.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue47740b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue47775b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, @@ -200,7 +195,6 @@ var knownFails = map[string]failReason{ "typeparam/issue47925d.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, "typeparam/issue48013.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, "typeparam/issue48016.go": {category: generics, desc: "not triaged"}, - "typeparam/issue48030.go": {category: generics, desc: "not triaged"}, "typeparam/issue48042.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue48047.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue48049.go": {category: generics, desc: "missing support for parameterized type instantiation"}, @@ -208,7 +202,6 @@ var knownFails = map[string]failReason{ "typeparam/issue48225.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue48253.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue48276a.go": {category: generics, desc: "missing support for the comparable type constraint"}, - "typeparam/issue48276b.go": {category: generics, desc: "not triaged"}, "typeparam/issue48317.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, "typeparam/issue48318.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, "typeparam/issue48344.go": {category: generics, desc: "missing support for parameterized type instantiation"}, @@ -219,7 +212,6 @@ var knownFails = map[string]failReason{ "typeparam/issue48645a.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue48645b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue48838.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue49049.go": {category: generics, desc: "not triaged"}, "typeparam/issue49295.go": {category: generics, desc: "len() doesn't support generic pointer to array types"}, "typeparam/issue49421.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, "typeparam/issue49547.go": {category: generics, desc: "incorrect type strings for parameterized types"}, @@ -248,7 +240,6 @@ var knownFails = map[string]failReason{ "typeparam/map.go": {category: generics, desc: "not triaged"}, "typeparam/maps.go": {category: generics, desc: "missing support for the comparable type constraint"}, "typeparam/metrics.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/min.go": {category: generics, desc: "not triaged"}, "typeparam/nested.go": {category: generics, desc: "missing comparison operator support for generic types"}, "typeparam/ordered.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/orderedmap.go": {category: generics, desc: "missing support for parameterized type instantiation"}, From f1a7c2a1697119c010c1f54bb46a3742d0f450e3 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Thu, 4 Jan 2024 14:29:22 +0000 Subject: [PATCH 05/28] Type parameter substitution when translating function instances. Instead of using type information directly from types.Info.TypeOf, we now use a funcContext.typeOf, which passes the resulting type through typeparams.Resolver. If we are translating a parameterized code, it will perform type parameter substitution according to the type arguments of the instance we are currently translating. This allows us to generate a type-specific code for each function instance, making sure correct operations are used in each case. --- compiler/expressions.go | 87 ++++++++++++------------- compiler/internal/typeparams/collect.go | 2 +- compiler/package.go | 11 ++-- compiler/statements.go | 32 ++++----- compiler/utils.go | 9 ++- tests/gorepo/run.go | 29 --------- 6 files changed, 73 insertions(+), 97 deletions(-) diff --git a/compiler/expressions.go b/compiler/expressions.go index 5b5bee93a..3cc2e2bd4 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -34,7 +34,7 @@ func (e *expression) StringWithParens() string { } func (fc *funcContext) translateExpr(expr ast.Expr) *expression { - exprType := fc.pkgCtx.TypeOf(expr) + exprType := fc.typeOf(expr) if value := fc.pkgCtx.Types[expr].Value; value != nil { basic := exprType.Underlying().(*types.Basic) switch { @@ -77,19 +77,16 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } } - var obj types.Object + var inst typeparams.Instance switch e := expr.(type) { case *ast.SelectorExpr: - obj = fc.pkgCtx.Uses[e.Sel] + inst = fc.instanceOf(e.Sel) case *ast.Ident: - obj = fc.pkgCtx.Defs[e] - if obj == nil { - obj = fc.pkgCtx.Uses[e] - } + inst = fc.instanceOf(e) } - if obj != nil && typesutil.IsJsPackage(obj.Pkg()) { - switch obj.Name() { + if inst.Object != nil && typesutil.IsJsPackage(inst.Object.Pkg()) { + switch inst.Object.Name() { case "Global": return fc.formatExpr("$global") case "Module": @@ -222,7 +219,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { return fc.formatExpr("(%s)", fun) case *ast.UnaryExpr: - t := fc.pkgCtx.TypeOf(e.X) + t := fc.typeOf(e.X) switch e.Op { case token.AND: if typesutil.IsJsObject(exprType) { @@ -242,7 +239,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { switch x := astutil.RemoveParens(e.X).(type) { case *ast.CompositeLit: - return fc.formatExpr("$newDataPointer(%e, %s)", x, fc.typeName(fc.pkgCtx.TypeOf(e))) + return fc.formatExpr("$newDataPointer(%e, %s)", x, fc.typeName(fc.typeOf(e))) case *ast.Ident: obj := fc.pkgCtx.Uses[x].(*types.Var) if fc.pkgCtx.escapingVars[obj] { @@ -256,12 +253,12 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { obj := fc.pkgCtx.Uses[x.Sel].(*types.Var) return fc.formatExpr(`(%1s || (%1s = new %2s(function() { return %3s; }, function($v) { %4s })))`, fc.varPtrName(obj), fc.typeName(exprType), fc.objectName(obj), fc.translateAssign(x, fc.newIdent("$v", elemType), false)) } - newSel := &ast.SelectorExpr{X: fc.newIdent("this.$target", fc.pkgCtx.TypeOf(x.X)), Sel: x.Sel} + newSel := &ast.SelectorExpr{X: fc.newIdent("this.$target", fc.typeOf(x.X)), Sel: x.Sel} fc.setType(newSel, exprType) fc.pkgCtx.additionalSelections[newSel] = sel return fc.formatExpr("(%1e.$ptr_%2s || (%1e.$ptr_%2s = new %3s(function() { return %4e; }, function($v) { %5s }, %1e)))", x.X, x.Sel.Name, fc.typeName(exprType), newSel, fc.translateAssign(newSel, fc.newIdent("$v", exprType), false)) case *ast.IndexExpr: - if _, ok := fc.pkgCtx.TypeOf(x.X).Underlying().(*types.Slice); ok { + if _, ok := fc.typeOf(x.X).Underlying().(*types.Slice); ok { return fc.formatExpr("$indexPtr(%1e.$array, %1e.$offset + %2e, %3s)", x.X, x.Index, fc.typeName(exprType)) } return fc.formatExpr("$indexPtr(%e, %e, %s)", x.X, x.Index, fc.typeName(exprType)) @@ -318,8 +315,8 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { })) } - t := fc.pkgCtx.TypeOf(e.X) - t2 := fc.pkgCtx.TypeOf(e.Y) + t := fc.typeOf(e.X) + t2 := fc.typeOf(e.Y) _, isInterface := t2.Underlying().(*types.Interface) if isInterface || types.Identical(t, types.Typ[types.UntypedNil]) { t = t2 @@ -483,7 +480,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { return fc.formatParenExpr("%e", e.X) case *ast.IndexExpr: - switch t := fc.pkgCtx.TypeOf(e.X).Underlying().(type) { + switch t := fc.typeOf(e.X).Underlying().(type) { case *types.Pointer: if _, ok := t.Elem().Underlying().(*types.Array); !ok { // Should never happen in type-checked code. @@ -504,7 +501,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case *types.Slice: return fc.formatExpr(rangeCheck("%1e.$array[%1e.$offset + %2f]", fc.pkgCtx.Types[e.Index].Value != nil, false), e.X, e.Index) case *types.Map: - if typesutil.IsJsObject(fc.pkgCtx.TypeOf(e.Index)) { + if typesutil.IsJsObject(fc.typeOf(e.Index)) { fc.pkgCtx.errList = append(fc.pkgCtx.errList, types.Error{Fset: fc.pkgCtx.fileSet, Pos: e.Index.Pos(), Msg: "cannot use js.Object as map key"}) } key := fmt.Sprintf("%s.keyFor(%s)", fc.typeName(t.Key()), fc.translateImplicitConversion(e.Index, t.Key())) @@ -532,14 +529,14 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { panic(fmt.Sprintf("Unhandled IndexExpr: %T\n", t)) } case *ast.IndexListExpr: - switch t := fc.pkgCtx.TypeOf(e.X).Underlying().(type) { + switch t := fc.typeOf(e.X).Underlying().(type) { case *types.Signature: return fc.formatExpr("%s", fc.instName(fc.instanceOf(e.X.(*ast.Ident)))) default: panic(fmt.Errorf("unhandled IndexListExpr: %T", t)) } case *ast.SliceExpr: - if b, isBasic := fc.pkgCtx.TypeOf(e.X).Underlying().(*types.Basic); isBasic && isString(b) { + if b, isBasic := fc.typeOf(e.X).Underlying().(*types.Basic); isBasic && isString(b) { switch { case e.Low == nil && e.High == nil: return fc.translateExpr(e.X) @@ -573,7 +570,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { sel, ok := fc.pkgCtx.SelectionOf(e) if !ok { // qualified identifier - return fc.formatExpr("%s", fc.objectName(obj)) + return fc.formatExpr("%s", fc.instName(inst)) } switch sel.Kind() { @@ -604,10 +601,10 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { plainFun := astutil.RemoveParens(e.Fun) if astutil.IsTypeExpr(plainFun, fc.pkgCtx.Info.Info) { - return fc.formatExpr("(%s)", fc.translateConversion(e.Args[0], fc.pkgCtx.TypeOf(plainFun))) + return fc.formatExpr("(%s)", fc.translateConversion(e.Args[0], fc.typeOf(plainFun))) } - sig := fc.pkgCtx.TypeOf(plainFun).Underlying().(*types.Signature) + sig := fc.typeOf(plainFun).Underlying().(*types.Signature) switch f := plainFun.(type) { case *ast.Ident: @@ -637,7 +634,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } externalizeExpr := func(e ast.Expr) string { - t := fc.pkgCtx.TypeOf(e) + t := fc.typeOf(e) if types.Identical(t, types.Typ[types.UntypedNil]) { return "null" } @@ -757,11 +754,11 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } case *ast.StarExpr: - if typesutil.IsJsObject(fc.pkgCtx.TypeOf(e.X)) { + if typesutil.IsJsObject(fc.typeOf(e.X)) { return fc.formatExpr("new $jsObjectPtr(%e)", e.X) } if c1, isCall := e.X.(*ast.CallExpr); isCall && len(c1.Args) == 1 { - if c2, isCall := c1.Args[0].(*ast.CallExpr); isCall && len(c2.Args) == 1 && types.Identical(fc.pkgCtx.TypeOf(c2.Fun), types.Typ[types.UnsafePointer]) { + if c2, isCall := c1.Args[0].(*ast.CallExpr); isCall && len(c2.Args) == 1 && types.Identical(fc.typeOf(c2.Fun), types.Typ[types.UnsafePointer]) { if unary, isUnary := c2.Args[0].(*ast.UnaryExpr); isUnary && unary.Op == token.AND { return fc.translateExpr(unary.X) // unsafe conversion } @@ -777,7 +774,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if e.Type == nil { return fc.translateExpr(e.X) } - t := fc.pkgCtx.TypeOf(e.Type) + t := fc.typeOf(e.Type) if _, isTuple := exprType.(*types.Tuple); isTuple { return fc.formatExpr("$assertType(%e, %s, true)", e.X, fc.typeName(t)) } @@ -787,11 +784,11 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if e.Name == "_" { panic("Tried to translate underscore identifier.") } - switch o := obj.(type) { + switch o := inst.Object.(type) { case *types.Var, *types.Const: - return fc.formatExpr("%s", fc.objectName(o)) + return fc.formatExpr("%s", fc.instName(inst)) case *types.Func: - return fc.formatExpr("%s", fc.instName(fc.instanceOf(e))) + return fc.formatExpr("%s", fc.instName(inst)) case *types.TypeName: return fc.formatExpr("%s", fc.typeName(o.Type())) case *types.Nil: @@ -868,7 +865,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, case *ast.SelectorExpr: isJs = typesutil.IsJsPackage(fc.pkgCtx.Uses[fun.Sel].Pkg()) } - sig := fc.pkgCtx.TypeOf(expr.Fun).Underlying().(*types.Signature) + sig := fc.typeOf(expr.Fun).Underlying().(*types.Signature) sigTypes := signatureTypes{Sig: sig} args := fc.translateArgs(sig, expr.Args, expr.Ellipsis.IsValid()) @@ -964,9 +961,9 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args return fc.formatExpr("$newDataPointer(%e, %s)", fc.zeroValue(t.Elem()), fc.typeName(t)) } case "make": - switch argType := fc.pkgCtx.TypeOf(args[0]).Underlying().(type) { + switch argType := fc.typeOf(args[0]).Underlying().(type) { case *types.Slice: - t := fc.typeName(fc.pkgCtx.TypeOf(args[0])) + t := fc.typeName(fc.typeOf(args[0])) if len(args) == 3 { return fc.formatExpr("$makeSlice(%s, %f, %f)", t, args[1], args[2]) } @@ -981,12 +978,12 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args if len(args) == 2 { length = fc.formatExpr("%f", args[1]).String() } - return fc.formatExpr("new $Chan(%s, %s)", fc.typeName(fc.pkgCtx.TypeOf(args[0]).Underlying().(*types.Chan).Elem()), length) + return fc.formatExpr("new $Chan(%s, %s)", fc.typeName(fc.typeOf(args[0]).Underlying().(*types.Chan).Elem()), length) default: panic(fmt.Sprintf("Unhandled make type: %T\n", argType)) } case "len": - switch argType := fc.pkgCtx.TypeOf(args[0]).Underlying().(type) { + switch argType := fc.typeOf(args[0]).Underlying().(type) { case *types.Basic: return fc.formatExpr("%e.length", args[0]) case *types.Slice: @@ -1002,7 +999,7 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args panic(fmt.Sprintf("Unhandled len type: %T\n", argType)) } case "cap": - switch argType := fc.pkgCtx.TypeOf(args[0]).Underlying().(type) { + switch argType := fc.typeOf(args[0]).Underlying().(type) { case *types.Slice, *types.Chan: return fc.formatExpr("%e.$capacity", args[0]) case *types.Pointer: @@ -1022,7 +1019,7 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args return fc.formatExpr("$append(%e, %s)", args[0], strings.Join(fc.translateExprSlice(args[1:], sliceType.Elem()), ", ")) case "delete": args = fc.expandTupleArgs(args) - keyType := fc.pkgCtx.TypeOf(args[0]).Underlying().(*types.Map).Key() + keyType := fc.typeOf(args[0]).Underlying().(*types.Map).Key() return fc.formatExpr( `$mapDelete(%1e, %2s.keyFor(%3s))`, args[0], @@ -1031,7 +1028,7 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args ) case "copy": args = fc.expandTupleArgs(args) - if basic, isBasic := fc.pkgCtx.TypeOf(args[1]).Underlying().(*types.Basic); isBasic && isString(basic) { + if basic, isBasic := fc.typeOf(args[1]).Underlying().(*types.Basic); isBasic && isString(basic) { return fc.formatExpr("$copyString(%e, %e)", args[0], args[1]) } return fc.formatExpr("$copySlice(%e, %e)", args[0], args[1]) @@ -1083,13 +1080,13 @@ func (fc *funcContext) translateExprSlice(exprs []ast.Expr, desiredType types.Ty } func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type) *expression { - exprType := fc.pkgCtx.TypeOf(expr) + exprType := fc.typeOf(expr) if types.Identical(exprType, desiredType) { return fc.translateExpr(expr) } if fc.pkgCtx.Pkg.Path() == "reflect" || fc.pkgCtx.Pkg.Path() == "internal/reflectlite" { - if call, isCall := expr.(*ast.CallExpr); isCall && types.Identical(fc.pkgCtx.TypeOf(call.Fun), types.Typ[types.UnsafePointer]) { + if call, isCall := expr.(*ast.CallExpr); isCall && types.Identical(fc.typeOf(call.Fun), types.Typ[types.UnsafePointer]) { if ptr, isPtr := desiredType.(*types.Pointer); isPtr { if named, isNamed := ptr.Elem().(*types.Named); isNamed { switch named.Obj().Name() { @@ -1164,7 +1161,7 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type return fc.formatExpr("new Uint8Array(0)") } } - if ptr, isPtr := fc.pkgCtx.TypeOf(expr).(*types.Pointer); fc.pkgCtx.Pkg.Path() == "syscall" && isPtr { + if ptr, isPtr := fc.typeOf(expr).(*types.Pointer); fc.pkgCtx.Pkg.Path() == "syscall" && isPtr { if s, isStruct := ptr.Elem().Underlying().(*types.Struct); isStruct { array := fc.newVariable("_array") target := fc.newVariable("_struct") @@ -1177,7 +1174,7 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type } if call, ok := expr.(*ast.CallExpr); ok { if id, ok := call.Fun.(*ast.Ident); ok && id.Name == "new" { - return fc.formatExpr("new Uint8Array(%d)", int(sizes32.Sizeof(fc.pkgCtx.TypeOf(call.Args[0])))) + return fc.formatExpr("new Uint8Array(%d)", int(sizes32.Sizeof(fc.typeOf(call.Args[0])))) } } } @@ -1266,7 +1263,7 @@ func (fc *funcContext) translateImplicitConversion(expr ast.Expr, desiredType ty return fc.translateExpr(expr) } - exprType := fc.pkgCtx.TypeOf(expr) + exprType := fc.typeOf(expr) if types.Identical(exprType, desiredType) { return fc.translateExpr(expr) } @@ -1297,7 +1294,7 @@ func (fc *funcContext) translateImplicitConversion(expr ast.Expr, desiredType ty } func (fc *funcContext) translateConversionToSlice(expr ast.Expr, desiredType types.Type) *expression { - switch fc.pkgCtx.TypeOf(expr).Underlying().(type) { + switch fc.typeOf(expr).Underlying().(type) { case *types.Array, *types.Pointer: return fc.formatExpr("new %s(%e)", fc.typeName(desiredType), expr) } @@ -1475,7 +1472,7 @@ func (fc *funcContext) formatExprInternal(format string, a []interface{}, parens out.WriteString(strconv.FormatInt(d, 10)) return } - if is64Bit(fc.pkgCtx.TypeOf(e).Underlying().(*types.Basic)) { + if is64Bit(fc.typeOf(e).Underlying().(*types.Basic)) { out.WriteString("$flatten64(") writeExpr("") out.WriteString(")") @@ -1486,7 +1483,7 @@ func (fc *funcContext) formatExprInternal(format string, a []interface{}, parens e := a[n].(ast.Expr) if val := fc.pkgCtx.Types[e].Value; val != nil { d, _ := constant.Uint64Val(constant.ToInt(val)) - if fc.pkgCtx.TypeOf(e).Underlying().(*types.Basic).Kind() == types.Int64 { + if fc.typeOf(e).Underlying().(*types.Basic).Kind() == types.Int64 { out.WriteString(strconv.FormatInt(int64(d)>>32, 10)) return } diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index fac958d93..62e37db00 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -37,7 +37,7 @@ func NewResolver(tc *types.Context, tParams []*types.TypeParam, tArgs []types.Ty // 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.Map == nil { + if r == nil || r.Map == nil || typ == nil { return typ // No substitutions to be made. } if concrete := r.memo.At(typ); concrete != nil { diff --git a/compiler/package.go b/compiler/package.go index 4f2711227..8e5e562a6 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -418,9 +418,10 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor var mainFunc *types.Func for _, fun := range functions { o := funcCtx.pkgCtx.Defs[fun.Name].(*types.Func) + sig := o.Type().(*types.Signature) var instances []typeparams.Instance - if o.Type().(*types.Signature).TypeParams().Len() != 0 { + if sig.TypeParams().Len() != 0 { instances = instancesByObj[symbol.New(o)] } else { instances = []typeparams.Instance{{Object: o}} @@ -457,7 +458,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor d.DceObjectFilter = "" case "init": d.InitCode = funcCtx.CatchOutput(1, func() { - id := funcCtx.newIdent("", types.NewSignature(nil, nil, nil, false)) + id := funcCtx.newIdent("", types.NewSignatureType( /*recv=*/ nil /*rectTypeParams=*/, nil /*typeParams=*/, nil /*params=*/, nil /*results=*/, nil /*variadic=*/, false)) funcCtx.pkgCtx.Uses[id] = o call := &ast.CallExpr{Fun: id} if len(funcCtx.pkgCtx.FuncDeclInfos[o].Blocking) != 0 { @@ -491,7 +492,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor if mainFunc == nil { return nil, fmt.Errorf("missing main function") } - id := funcCtx.newIdent("", types.NewSignature(nil, nil, nil, false)) + id := funcCtx.newIdent("", types.NewSignatureType( /*recv=*/ nil /*rectTypeParams=*/, nil /*typeParams=*/, nil /*params=*/, nil /*results=*/, nil /*variadic=*/, false)) funcCtx.pkgCtx.Uses[id] = mainFunc call := &ast.CallExpr{Fun: id} ifStmt := &ast.IfStmt{ @@ -771,7 +772,6 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, FuncInfo: info, pkgCtx: outerContext.pkgCtx, parent: outerContext, - sig: sig, allVars: make(map[string]int, len(outerContext.allVars)), localVars: []string{}, flowDatas: map[*types.Label]*flowData{nil: {}}, @@ -793,6 +793,7 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, if c.objectNames == nil { c.objectNames = map[types.Object]string{} } + c.sig = c.typeResolver.Substitute(sig).(*types.Signature) var params []string for _, param := range typ.Params.List { @@ -828,7 +829,7 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, if recv != nil && !isBlank(recv) { this := "this" - if isWrapped(c.pkgCtx.TypeOf(recv)) { + if isWrapped(c.typeOf(recv)) { this = "this.$val" // Unwrap receiver value. } c.Printf("%s = %s;", c.translateExpr(recv), this) diff --git a/compiler/statements.go b/compiler/statements.go index c1ec42bc5..0df5e72b6 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -135,10 +135,10 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { } fc.Printf("%s = %s;", refVar, fc.translateExpr(expr)) translateCond := func(cond ast.Expr) *expression { - if types.Identical(fc.pkgCtx.TypeOf(cond), types.Typ[types.UntypedNil]) { + if types.Identical(fc.typeOf(cond), types.Typ[types.UntypedNil]) { return fc.formatExpr("%s === $ifaceNil", refVar) } - return fc.formatExpr("$assertType(%s, %s, true)[1]", refVar, fc.typeName(fc.pkgCtx.TypeOf(cond))) + return fc.formatExpr("$assertType(%s, %s, true)[1]", refVar, fc.typeName(fc.typeOf(cond))) } var caseClauses []*ast.CaseClause var defaultClause *ast.CaseClause @@ -190,7 +190,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { refVar := fc.newVariable("_ref") fc.Printf("%s = %s;", refVar, fc.translateExpr(s.X)) - switch t := fc.pkgCtx.TypeOf(s.X).Underlying().(type) { + switch t := fc.typeOf(s.X).Underlying().(type) { case *types.Basic: iVar := fc.newVariable("_i") fc.Printf("%s = 0;", iVar) @@ -380,7 +380,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { case len(s.Lhs) == 1 && len(s.Rhs) == 1: lhs := astutil.RemoveParens(s.Lhs[0]) if isBlank(lhs) { - fc.Printf("$unused(%s);", fc.translateImplicitConversion(s.Rhs[0], fc.pkgCtx.TypeOf(s.Lhs[0]))) + fc.Printf("$unused(%s);", fc.translateImplicitConversion(s.Rhs[0], fc.typeOf(s.Lhs[0]))) return } fc.Printf("%s", fc.translateAssign(lhs, s.Rhs[0], s.Tok == token.DEFINE)) @@ -388,7 +388,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { case len(s.Lhs) > 1 && len(s.Rhs) == 1: tupleVar := fc.newVariable("_tuple") fc.Printf("%s = %s;", tupleVar, fc.translateExpr(s.Rhs[0])) - tuple := fc.pkgCtx.TypeOf(s.Rhs[0]).(*types.Tuple) + tuple := fc.typeOf(s.Rhs[0]).(*types.Tuple) for i, lhs := range s.Lhs { lhs = astutil.RemoveParens(lhs) if !isBlank(lhs) { @@ -403,12 +403,12 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { fc.Printf("$unused(%s);", fc.translateExpr(rhs)) continue } - fc.Printf("%s", fc.translateAssign(fc.newIdent(tmpVars[i], fc.pkgCtx.TypeOf(s.Lhs[i])), rhs, true)) + fc.Printf("%s", fc.translateAssign(fc.newIdent(tmpVars[i], fc.typeOf(s.Lhs[i])), rhs, true)) } for i, lhs := range s.Lhs { lhs = astutil.RemoveParens(lhs) if !isBlank(lhs) { - fc.Printf("%s", fc.translateAssign(lhs, fc.newIdent(tmpVars[i], fc.pkgCtx.TypeOf(lhs)), s.Tok == token.DEFINE)) + fc.Printf("%s", fc.translateAssign(lhs, fc.newIdent(tmpVars[i], fc.typeOf(lhs)), s.Tok == token.DEFINE)) } } @@ -431,7 +431,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { if len(rhs) == 0 { rhs = make([]ast.Expr, len(lhs)) for i, e := range lhs { - rhs[i] = fc.zeroValue(fc.pkgCtx.TypeOf(e)) + rhs[i] = fc.zeroValue(fc.typeOf(e)) } } fc.translateStmt(&ast.AssignStmt{ @@ -469,7 +469,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { fc.Printf("$go(%s, %s);", callable, arglist) case *ast.SendStmt: - chanType := fc.pkgCtx.TypeOf(s.Chan).Underlying().(*types.Chan) + chanType := fc.typeOf(s.Chan).Underlying().(*types.Chan) call := &ast.CallExpr{ Fun: fc.newIdent("$send", types.NewSignature(nil, types.NewTuple(types.NewVar(0, nil, "", chanType), types.NewVar(0, nil, "", chanType.Elem())), nil, false)), Args: []ast.Expr{s.Chan, fc.newIdent(fc.translateImplicitConversionWithCloning(s.Value, chanType.Elem()).String(), chanType.Elem())}, @@ -494,7 +494,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { case *ast.AssignStmt: channels = append(channels, fc.formatExpr("[%e]", astutil.RemoveParens(comm.Rhs[0]).(*ast.UnaryExpr).X).String()) case *ast.SendStmt: - chanType := fc.pkgCtx.TypeOf(comm.Chan).Underlying().(*types.Chan) + chanType := fc.typeOf(comm.Chan).Underlying().(*types.Chan) channels = append(channels, fc.formatExpr("[%e, %s]", comm.Chan, fc.translateImplicitConversionWithCloning(comm.Value, chanType.Elem())).String()) default: panic(fmt.Sprintf("unhandled: %T", comm)) @@ -505,7 +505,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { var bodyPrefix []ast.Stmt if assign, ok := clause.Comm.(*ast.AssignStmt); ok { - switch rhsType := fc.pkgCtx.TypeOf(assign.Rhs[0]).(type) { + switch rhsType := fc.typeOf(assign.Rhs[0]).(type) { case *types.Tuple: bodyPrefix = []ast.Stmt{&ast.AssignStmt{Lhs: assign.Lhs, Rhs: []ast.Expr{fc.newIdent(selectionVar+"[1]", rhsType)}, Tok: assign.Tok}} default: @@ -700,8 +700,8 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { } if l, ok := lhs.(*ast.IndexExpr); ok { - if t, ok := fc.pkgCtx.TypeOf(l.X).Underlying().(*types.Map); ok { - if typesutil.IsJsObject(fc.pkgCtx.TypeOf(l.Index)) { + if t, ok := fc.typeOf(l.X).Underlying().(*types.Map); ok { + if typesutil.IsJsObject(fc.typeOf(l.Index)) { fc.pkgCtx.errList = append(fc.pkgCtx.errList, types.Error{Fset: fc.pkgCtx.fileSet, Pos: l.Index.Pos(), Msg: "cannot use js.Object as map key"}) } keyVar := fc.newVariable("_key") @@ -718,7 +718,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { } } - lhsType := fc.pkgCtx.TypeOf(lhs) + lhsType := fc.typeOf(lhs) rhsExpr := fc.translateConversion(rhs, lhsType) if _, ok := rhs.(*ast.CompositeLit); ok && define { return fmt.Sprintf("%s = %s;", fc.translateExpr(lhs), rhsExpr) // skip $copy @@ -755,7 +755,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { case *ast.StarExpr: return fmt.Sprintf("%s.$set(%s);", fc.translateExpr(l.X), rhsExpr) case *ast.IndexExpr: - switch t := fc.pkgCtx.TypeOf(l.X).Underlying().(type) { + switch t := fc.typeOf(l.X).Underlying().(type) { case *types.Array, *types.Pointer: pattern := rangeCheck("%1e[%2f] = %3s", fc.pkgCtx.Types[l.Index].Value != nil, true) if _, ok := t.(*types.Pointer); ok { // check pointer for nil (attribute getter causes a panic) @@ -787,7 +787,7 @@ func (fc *funcContext) translateResults(results []ast.Expr) string { return " " + v.String() default: if len(results) == 1 { - resultTuple := fc.pkgCtx.TypeOf(results[0]).(*types.Tuple) + resultTuple := fc.typeOf(results[0]).(*types.Tuple) if resultTuple.Len() != tuple.Len() { panic("invalid tuple return assignment") diff --git a/compiler/utils.go b/compiler/utils.go index 184f1ecce..6d244a236 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -110,7 +110,7 @@ func (fc *funcContext) expandTupleArgs(argExprs []ast.Expr) []ast.Expr { return argExprs } - tuple, isTuple := fc.pkgCtx.TypeOf(argExprs[0]).(*types.Tuple) + tuple, isTuple := fc.typeOf(argExprs[0]).(*types.Tuple) if !isTuple { return argExprs } @@ -431,6 +431,13 @@ func (fc *funcContext) instanceOf(ident *ast.Ident) typeparams.Instance { return inst } +// typeOf returns a type associated with the given AST expression. For types +// defined in terms of type parameters, it will substitute type parameters with +// concrete types from the current set of type arguments. +func (fc *funcContext) typeOf(expr ast.Expr) types.Type { + return fc.typeResolver.Substitute(fc.pkgCtx.TypeOf(expr)) +} + func (fc *funcContext) externalize(s string, t types.Type) string { if typesutil.IsJsObject(t) { return s diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 8f8cdff27..9f09271f3 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -156,7 +156,6 @@ var knownFails = map[string]failReason{ // fixed. "typeparam/absdiff.go": {category: generics, desc: "missing operator support for generic types"}, "typeparam/absdiff2.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/absdiff3.go": {category: generics, desc: "missing operator support for generic types"}, "typeparam/boundmethod.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/chans.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/combine.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, @@ -164,24 +163,16 @@ var knownFails = map[string]failReason{ "typeparam/dictionaryCapture-noinline.go": {category: generics, desc: "attempts to pass -gcflags=\"-G=3\" flag, incorrectly parsed by run.go"}, "typeparam/dictionaryCapture.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/dottype.go": {category: generics, desc: "not triaged"}, - "typeparam/double.go": {category: generics, desc: "make() doesn't support generic slice types"}, "typeparam/eface.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, "typeparam/equal.go": {category: generics, desc: "missing support for the comparable type constraint"}, - "typeparam/fact.go": {category: generics, desc: "missing support for the comparable type constraint"}, "typeparam/genembed.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/genembed2.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/graph.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/ifaceconv.go": {category: generics, desc: "not triaged"}, - "typeparam/index.go": {category: generics, desc: "undiagnosed: runtime error: comparing uncomparable type undefined"}, - "typeparam/index2.go": {category: generics, desc: "missing index operator support for generic types"}, "typeparam/interfacearg.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, - "typeparam/issue23536.go": {category: generics, desc: "missing support for generic byte/rune slice to string conversion"}, "typeparam/issue44688.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue45817.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, - "typeparam/issue47258.go": {category: generics, desc: "missing operator support for generic types"}, "typeparam/issue47272.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47684b.go": {category: generics, desc: "not triaged"}, "typeparam/issue47713.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue47716.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue47740.go": {category: generics, desc: "missing support for parameterized type instantiation"}, @@ -194,18 +185,14 @@ var knownFails = map[string]failReason{ "typeparam/issue47925c.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, "typeparam/issue47925d.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, "typeparam/issue48013.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, - "typeparam/issue48016.go": {category: generics, desc: "not triaged"}, "typeparam/issue48042.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue48047.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue48049.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48137.go": {category: generics, desc: "not triaged"}, "typeparam/issue48225.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue48253.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48276a.go": {category: generics, desc: "missing support for the comparable type constraint"}, "typeparam/issue48317.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, "typeparam/issue48318.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, "typeparam/issue48344.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48453.go": {category: generics, desc: "make() doesn't support generic slice types"}, "typeparam/issue48598.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue48602.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue48617.go": {category: generics, desc: "missing support for parameterized type instantiation"}, @@ -219,47 +206,31 @@ var knownFails = map[string]failReason{ "typeparam/issue50002.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, "typeparam/issue50109.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue50109b.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/issue50193.go": {category: generics, desc: "invalid print format for complex numbers"}, "typeparam/issue50264.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue50419.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue50642.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue50690a.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue50690b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/issue50690c.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, - "typeparam/issue50833.go": {category: generics, desc: "undiagnosed: compiler panic triggered by a composite literal"}, "typeparam/issue51303.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue51522a.go": {category: generics, desc: "missing support for the comparable type constraint"}, - "typeparam/issue51522b.go": {category: generics, desc: "missing support for the comparable type constraint"}, - "typeparam/issue51700.go": {category: generics, desc: "not triaged"}, "typeparam/issue52026.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue52228.go": {category: generics, desc: "not triaged"}, "typeparam/issue53477.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, "typeparam/list.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/list2.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/lockable.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/map.go": {category: generics, desc: "not triaged"}, - "typeparam/maps.go": {category: generics, desc: "missing support for the comparable type constraint"}, "typeparam/metrics.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/nested.go": {category: generics, desc: "missing comparison operator support for generic types"}, "typeparam/ordered.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/orderedmap.go": {category: generics, desc: "missing support for parameterized type instantiation"}, "typeparam/pair.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, "typeparam/sets.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/settable.go": {category: generics, desc: "undiagnosed: len() returns an invalid value on a generic function result"}, "typeparam/shape1.go": {category: generics, desc: "not triaged"}, - "typeparam/slices.go": {category: generics, desc: "missing operator support for generic types"}, "typeparam/stringable.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/stringer.go": {category: generics, desc: "not triaged"}, "typeparam/struct.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, "typeparam/subdict.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/sum.go": {category: generics, desc: "not triaged"}, - "typeparam/typeswitch1.go": {category: generics, desc: "not triaged"}, "typeparam/typeswitch2.go": {category: generics, desc: "complex types have different print() format"}, "typeparam/typeswitch3.go": {category: generics, desc: "missing support for type switching on generic types"}, - "typeparam/typeswitch4.go": {category: generics, desc: "not triaged"}, "typeparam/typeswitch5.go": {category: generics, desc: "different print() format for floating point types"}, - "typeparam/typeswitch6.go": {category: generics, desc: "not triaged"}, - "typeparam/typeswitch7.go": {category: generics, desc: "not triaged"}, "typeparam/value.go": {category: generics, desc: "missing support for parameterized type instantiation"}, } From ae5a9c251223f476145db809bb45c446c1983e6a Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Thu, 4 Jan 2024 14:56:44 +0000 Subject: [PATCH 06/28] Explicitly generate method instances for generic names types. Initially I thought it would be trivial to derive them from the main type instances, but it led to more duplicated code than necessary, so now they will be discovered and processed explicitly. --- compiler/internal/typeparams/collect.go | 26 +++--- compiler/internal/typeparams/collect_test.go | 88 ++++++++++++++------ compiler/internal/typeparams/utils.go | 16 ++++ internal/srctesting/srctesting.go | 14 ++++ 4 files changed, 109 insertions(+), 35 deletions(-) create mode 100644 compiler/internal/typeparams/utils.go diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index 62e37db00..db088b56d 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -7,6 +7,7 @@ import ( "go/types" "github.com/gopherjs/gopherjs/compiler/typesutil" + "golang.org/x/exp/typeparams" ) // Resolver translates types defined in terms of type parameters into concrete @@ -94,10 +95,21 @@ func (c *visitor) Visit(n ast.Node) (w ast.Visitor) { return } + obj := c.info.ObjectOf(ident) c.instances.Add(Instance{ - Object: c.info.ObjectOf(ident), + Object: obj, TArgs: c.resolver.SubstituteAll(instance.TypeArgs), }) + + if t, ok := obj.Type().(*types.Named); ok { + 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. + TArgs: c.resolver.SubstituteAll(instance.TypeArgs), + }) + } + } return } @@ -156,8 +168,8 @@ func (c *seedVisitor) Visit(n ast.Node) ast.Visitor { // InstanceSet may contain unprocessed instances of generic types and functions, // which will be also scanned, for example found in depending packages. // -// Note that methods of generic types are never added to the InstanceSet, since -// they can be easily inferred from the receiver type instances. +// Note that instanced of generic type methods are automatically added to the +// set whenever their receiver type instance is encountered. type Collector struct { TContext *types.Context Info *types.Info @@ -191,7 +203,7 @@ func (c *Collector) Scan(files ...*ast.File) { case *types.Signature: v := visitor{ instances: c.Instances, - resolver: NewResolver(c.TContext, ToSlice(typ.TypeParams()), inst.TArgs), + resolver: NewResolver(c.TContext, ToSlice(SignatureTypeParams(typ)), inst.TArgs), info: c.Info, } ast.Walk(&v, objMap[inst.Object]) @@ -203,12 +215,6 @@ func (c *Collector) Scan(files ...*ast.File) { info: c.Info, } ast.Walk(&v, objMap[obj]) - - for i := 0; i < typ.NumMethods(); i++ { - method := typ.Method(i) - v.resolver = NewResolver(c.TContext, ToSlice(method.Type().(*types.Signature).RecvTypeParams()), inst.TArgs) - ast.Walk(&v, objMap[method]) - } } } } diff --git a/compiler/internal/typeparams/collect_test.go b/compiler/internal/typeparams/collect_test.go index 78b35cc3a..778915c57 100644 --- a/compiler/internal/typeparams/collect_test.go +++ b/compiler/internal/typeparams/collect_test.go @@ -4,7 +4,6 @@ import ( "go/ast" "go/token" "go/types" - "strings" "testing" "github.com/google/go-cmp/cmp" @@ -86,13 +85,7 @@ func TestVisitor(t *testing.T) { info, pkg := srctesting.Check(t, fset, file) lookupObj := func(name string) types.Object { - parts := strings.Split(name, ".") - obj := pkg.Scope().Lookup(parts[0]) - if len(parts) == 1 { - return obj - } - obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), parts[1]) - return obj + return srctesting.LookupObj(pkg, name) } lookupType := func(name string) types.Type { return lookupObj(name).Type() } lookupDecl := func(name string) ast.Node { @@ -130,22 +123,37 @@ func TestVisitor(t *testing.T) { // Literal expression. Object: lookupObj("typ"), TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, }, { // Function argument. Object: lookupObj("typ"), TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, }, { // Function return type. Object: lookupObj("typ"), TArgs: []types.Type{types.Typ[types.Int16], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int16], sentinel}, }, { // Method expression. Object: lookupObj("typ"), TArgs: []types.Type{types.Typ[types.Int32], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int32], sentinel}, }, { // Type decl statement. Object: lookupObj("typ"), TArgs: []types.Type{types.Typ[types.Int64], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int64], sentinel}, }, } } @@ -159,9 +167,15 @@ func TestVisitor(t *testing.T) { { Object: lookupObj("typ"), TArgs: []types.Type{types.Typ[types.Int], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int], sentinel}, }, { Object: lookupObj("typ"), TArgs: []types.Type{types.Typ[types.Int8], sentinel}, + }, { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int8], sentinel}, }, } } @@ -200,6 +214,10 @@ func TestVisitor(t *testing.T) { Object: lookupObj("entry3"), TArgs: []types.Type{lookupType("C")}, }, + Instance{ + Object: lookupObj("entry3.method"), + TArgs: []types.Type{lookupType("C")}, + }, ), }, { descr: "generic type declaration", @@ -224,21 +242,27 @@ func TestVisitor(t *testing.T) { Object: lookupObj("typ"), TArgs: []types.Type{types.Typ[types.Int], lookupType("F")}, }, + { + Object: lookupObj("typ.method"), + TArgs: []types.Type{types.Typ[types.Int], lookupType("F")}, + }, }, }, } for _, test := range tests { - v := visitor{ - instances: &InstanceSet{}, - resolver: test.resolver, - info: info, - } - ast.Walk(&v, test.node) - got := v.instances.Values() - if diff := cmp.Diff(test.want, got, instanceOpts()); diff != "" { - t.Errorf("Discovered instance diff (-want,+got):\n%s", diff) - } + t.Run(test.descr, func(t *testing.T) { + v := visitor{ + instances: &InstanceSet{}, + resolver: test.resolver, + info: info, + } + ast.Walk(&v, test.node) + got := v.instances.Values() + if diff := cmp.Diff(test.want, got, instanceOpts()); diff != "" { + t.Errorf("Discovered instance diff (-want,+got):\n%s", diff) + } + }) } } @@ -270,18 +294,29 @@ func TestSeedVisitor(t *testing.T) { } ast.Walk(&sv, file) - inst := func(tArg types.Type) Instance { + tInst := func(tArg types.Type) Instance { return Instance{ Object: pkg.Scope().Lookup("typ"), TArgs: []types.Type{tArg}, } } + mInst := func(tArg types.Type) Instance { + return Instance{ + Object: srctesting.LookupObj(pkg, "typ.method"), + TArgs: []types.Type{tArg}, + } + } want := []Instance{ - inst(types.Typ[types.Int]), - inst(types.Typ[types.Int8]), - inst(types.Typ[types.Int16]), - inst(types.Typ[types.Int32]), - inst(types.Typ[types.Int64]), + tInst(types.Typ[types.Int]), + mInst(types.Typ[types.Int]), + tInst(types.Typ[types.Int8]), + mInst(types.Typ[types.Int8]), + tInst(types.Typ[types.Int16]), + mInst(types.Typ[types.Int16]), + tInst(types.Typ[types.Int32]), + mInst(types.Typ[types.Int32]), + tInst(types.Typ[types.Int64]), + mInst(types.Typ[types.Int64]), } got := sv.instances.Values() if diff := cmp.Diff(want, got, instanceOpts()); diff != "" { @@ -316,15 +351,18 @@ func TestCollector(t *testing.T) { inst := func(name string, tArg types.Type) Instance { return Instance{ - Object: pkg.Scope().Lookup(name), + Object: srctesting.LookupObj(pkg, name), TArgs: []types.Type{tArg}, } } want := []Instance{ inst("typ", types.Typ[types.Int]), + inst("typ.method", types.Typ[types.Int]), inst("fun", 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]), } got := c.Instances.Values() diff --git a/compiler/internal/typeparams/utils.go b/compiler/internal/typeparams/utils.go new file mode 100644 index 000000000..1d4598f3b --- /dev/null +++ b/compiler/internal/typeparams/utils.go @@ -0,0 +1,16 @@ +package typeparams + +import "go/types" + +// SignatureTypeParams returns receiver type params for methods, or function +// type params for standalone functions, or nil for non-generic functions and +// methods. +func SignatureTypeParams(sig *types.Signature) *types.TypeParamList { + if tp := sig.RecvTypeParams(); tp != nil { + return tp + } else if tp := sig.TypeParams(); tp != nil { + return tp + } else { + return nil + } +} diff --git a/internal/srctesting/srctesting.go b/internal/srctesting/srctesting.go index 3da0be22b..de3753efc 100644 --- a/internal/srctesting/srctesting.go +++ b/internal/srctesting/srctesting.go @@ -9,6 +9,7 @@ import ( "go/parser" "go/token" "go/types" + "strings" "testing" ) @@ -80,3 +81,16 @@ func Format(t *testing.T, fset *token.FileSet, node any) string { } return buf.String() } + +// LookupObj returns a top-level object with the given name. +// +// Methods can be referred to as RecvTypeName.MethodName. +func LookupObj(pkg *types.Package, name string) types.Object { + parts := strings.Split(name, ".") + obj := pkg.Scope().Lookup(parts[0]) + if len(parts) == 1 { + return obj + } + obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), parts[1]) + return obj +} From cdddbe215fc8717d616dc84d945d0b28efc682cf Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Fri, 5 Jan 2024 15:00:14 +0000 Subject: [PATCH 07/28] Implement generic type instantiation. Similar to standalone functions, we generate a set of initialization code for each combination of type parameters discovered. Code for metadata initialization is generated with type argument substitution active for the given instantiation. Methods are translated essentially the same as standalone functions, except that they are assigned to the receiver type object instead of a standalone variable. --- compiler/analysis/info.go | 5 + compiler/astutil/astutil.go | 7 + compiler/internal/typeparams/instance.go | 19 +++ compiler/package.go | 200 +++++++++++++---------- compiler/typesutil/typesutil.go | 17 ++ compiler/utils.go | 6 +- go.mod | 1 + go.sum | 2 + tests/gorepo/run.go | 114 ++++--------- 9 files changed, 204 insertions(+), 167 deletions(-) diff --git a/compiler/analysis/info.go b/compiler/analysis/info.go index c984b726f..07174a932 100644 --- a/compiler/analysis/info.go +++ b/compiler/analysis/info.go @@ -9,6 +9,7 @@ import ( "github.com/gopherjs/gopherjs/compiler/astutil" "github.com/gopherjs/gopherjs/compiler/typesutil" + "golang.org/x/exp/typeparams" ) type continueStmt struct { @@ -93,6 +94,9 @@ func (info *Info) newFuncInfo(n ast.Node) *FuncInfo { } func (info *Info) IsBlocking(fun *types.Func) bool { + if info.FuncDeclInfos[fun] == nil { + panic("nil info") + } return len(info.FuncDeclInfos[fun].Blocking) > 0 } @@ -354,6 +358,7 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { func (fi *FuncInfo) callToNamedFunc(callee types.Object) { switch o := callee.(type) { case *types.Func: + o = typeparams.OriginMethod(o) // TODO(nevkontakte): Can be replaced with o.Origin() in Go 1.19. if recv := o.Type().(*types.Signature).Recv(); recv != nil { if _, ok := recv.Type().Underlying().(*types.Interface); ok { // Conservatively assume that an interface implementation may be blocking. diff --git a/compiler/astutil/astutil.go b/compiler/astutil/astutil.go index 30febe1cb..dfa820438 100644 --- a/compiler/astutil/astutil.go +++ b/compiler/astutil/astutil.go @@ -65,10 +65,17 @@ func FuncKey(d *ast.FuncDecl) string { if d.Recv == nil || len(d.Recv.List) == 0 { return d.Name.Name } + // Each if-statement progressively unwraps receiver type expression. recv := d.Recv.List[0].Type if star, ok := recv.(*ast.StarExpr); ok { recv = star.X } + if index, ok := recv.(*ast.IndexExpr); ok { + recv = index.X + } + if index, ok := recv.(*ast.IndexListExpr); ok { + recv = index.X + } return recv.(*ast.Ident).Name + "." + d.Name.Name } diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index 760f68f3b..69b198d08 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/gopherjs/gopherjs/compiler/internal/symbol" + "github.com/gopherjs/gopherjs/compiler/typesutil" "golang.org/x/exp/maps" ) @@ -50,6 +51,24 @@ func (i *Instance) IsTrivial() bool { return len(i.TArgs) == 0 } +// Recv returns an instance of the receiver type of a method. +// +// Returns zero value if not a method. +func (i *Instance) Recv() Instance { + sig, ok := i.Object.Type().(*types.Signature) + if !ok { + return Instance{} + } + recv := typesutil.RecvType(sig) + if recv == nil { + return Instance{} + } + return Instance{ + Object: recv.Obj(), + TArgs: i.TArgs, + } +} + // 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/package.go b/compiler/package.go index 8e5e562a6..10732f84f 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -421,7 +421,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor sig := o.Type().(*types.Signature) var instances []typeparams.Instance - if sig.TypeParams().Len() != 0 { + if typeparams.SignatureTypeParams(sig) != nil { instances = instancesByObj[symbol.New(o)] } else { instances = []typeparams.Instance{{Object: o}} @@ -525,88 +525,116 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor if o.IsAlias() { continue } + typ := o.Type().(*types.Named) + var instances []typeparams.Instance + if typ.TypeParams() != nil { + instances = instancesByObj[symbol.New(o)] + } else { + instances = []typeparams.Instance{{Object: o}} + } + typeName := funcCtx.objectName(o) - d := Decl{ - Vars: []string{typeName}, - DceObjectFilter: o.Name(), + varDecl := Decl{Vars: []string{typeName}} + if typ.TypeParams() != nil { + varDecl.DeclCode = funcCtx.CatchOutput(0, func() { + funcCtx.Printf("%s = {};", funcCtx.objectName(o)) + }) } - d.DceDeps = collectDependencies(func() { - d.DeclCode = funcCtx.CatchOutput(0, func() { - typeName := funcCtx.objectName(o) - lhs := typeName - if isPkgLevel(o) { - lhs += " = $pkg." + encodeIdent(o.Name()) + if isPkgLevel(o) { + varDecl.TypeInitCode = funcCtx.CatchOutput(0, func() { + funcCtx.Printf("$pkg.%s = %s;", encodeIdent(o.Name()), funcCtx.objectName(o)) + }) + } + typeDecls = append(typeDecls, &varDecl) + + for _, inst := range instances { + funcCtx.typeResolver = typeparams.NewResolver(funcCtx.pkgCtx.typesCtx, typeparams.ToSlice(typ.TypeParams()), inst.TArgs) + + named := typ + if !inst.IsTrivial() { + instantiated, err := types.Instantiate(funcCtx.pkgCtx.typesCtx, typ, inst.TArgs, true) + if err != nil { + return nil, fmt.Errorf("failed to instantiate type %v with args %v: %w", typ, inst.TArgs, err) } - size := int64(0) - constructor := "null" - switch t := o.Type().Underlying().(type) { - case *types.Struct: - params := make([]string, t.NumFields()) - for i := 0; i < t.NumFields(); i++ { - params[i] = fieldName(t, i) + "_" - } - constructor = fmt.Sprintf("function(%s) {\n\t\tthis.$val = this;\n\t\tif (arguments.length === 0) {\n", strings.Join(params, ", ")) - for i := 0; i < t.NumFields(); i++ { - constructor += fmt.Sprintf("\t\t\tthis.%s = %s;\n", fieldName(t, i), funcCtx.translateExpr(funcCtx.zeroValue(t.Field(i).Type())).String()) + named = instantiated.(*types.Named) + } + underlying := named.Underlying() + d := Decl{ + DceObjectFilter: o.Name(), + } + d.DceDeps = collectDependencies(func() { + d.DeclCode = funcCtx.CatchOutput(0, func() { + size := int64(0) + constructor := "null" + switch t := underlying.(type) { + case *types.Struct: + params := make([]string, t.NumFields()) + for i := 0; i < t.NumFields(); i++ { + params[i] = fieldName(t, i) + "_" + } + constructor = fmt.Sprintf("function(%s) {\n\t\tthis.$val = this;\n\t\tif (arguments.length === 0) {\n", strings.Join(params, ", ")) + for i := 0; i < t.NumFields(); i++ { + constructor += fmt.Sprintf("\t\t\tthis.%s = %s;\n", fieldName(t, i), funcCtx.translateExpr(funcCtx.zeroValue(t.Field(i).Type())).String()) + } + constructor += "\t\t\treturn;\n\t\t}\n" + for i := 0; i < t.NumFields(); i++ { + constructor += fmt.Sprintf("\t\tthis.%[1]s = %[1]s_;\n", fieldName(t, i)) + } + constructor += "\t}" + case *types.Basic, *types.Array, *types.Slice, *types.Chan, *types.Signature, *types.Interface, *types.Pointer, *types.Map: + size = sizes32.Sizeof(t) } - constructor += "\t\t\treturn;\n\t\t}\n" - for i := 0; i < t.NumFields(); i++ { - constructor += fmt.Sprintf("\t\tthis.%[1]s = %[1]s_;\n", fieldName(t, i)) + if tPointer, ok := underlying.(*types.Pointer); ok { + if _, ok := tPointer.Elem().Underlying().(*types.Array); ok { + // Array pointers have non-default constructors to support wrapping + // of the native objects. + constructor = "$arrayPtrCtor()" + } } - constructor += "\t}" - case *types.Basic, *types.Array, *types.Slice, *types.Chan, *types.Signature, *types.Interface, *types.Pointer, *types.Map: - size = sizes32.Sizeof(t) - } - if tPointer, ok := o.Type().Underlying().(*types.Pointer); ok { - if _, ok := tPointer.Elem().Underlying().(*types.Array); ok { - // Array pointers have non-default constructors to support wrapping - // of the native objects. - constructor = "$arrayPtrCtor()" + funcCtx.Printf(`%s = $newType(%d, %s, "%s.%s", %t, "%s", %t, %s);`, funcCtx.instName(inst), size, typeKind(typ), o.Pkg().Name(), o.Name(), o.Name() != "", o.Pkg().Path(), o.Exported(), constructor) + }) + d.MethodListCode = funcCtx.CatchOutput(0, func() { + if _, ok := underlying.(*types.Interface); ok { + return } - } - funcCtx.Printf(`%s = $newType(%d, %s, "%s.%s", %t, "%s", %t, %s);`, lhs, size, typeKind(o.Type()), o.Pkg().Name(), o.Name(), o.Name() != "", o.Pkg().Path(), o.Exported(), constructor) - }) - d.MethodListCode = funcCtx.CatchOutput(0, func() { - named := o.Type().(*types.Named) - if _, ok := named.Underlying().(*types.Interface); ok { - return - } - var methods []string - var ptrMethods []string - for i := 0; i < named.NumMethods(); i++ { - method := named.Method(i) - name := method.Name() - if reservedKeywords[name] { - name += "$" + var methods []string + var ptrMethods []string + for i := 0; i < named.NumMethods(); i++ { + method := named.Method(i) + name := method.Name() + if reservedKeywords[name] { + name += "$" + } + pkgPath := "" + if !method.Exported() { + pkgPath = method.Pkg().Path() + } + t := method.Type().(*types.Signature) + entry := fmt.Sprintf(`{prop: "%s", name: %s, pkg: "%s", typ: $funcType(%s)}`, name, encodeString(method.Name()), pkgPath, funcCtx.initArgs(t)) + if _, isPtr := t.Recv().Type().(*types.Pointer); isPtr { + ptrMethods = append(ptrMethods, entry) + continue + } + methods = append(methods, entry) } - pkgPath := "" - if !method.Exported() { - pkgPath = method.Pkg().Path() + if len(methods) > 0 { + funcCtx.Printf("%s.methods = [%s];", funcCtx.instName(inst), strings.Join(methods, ", ")) } - t := method.Type().(*types.Signature) - entry := fmt.Sprintf(`{prop: "%s", name: %s, pkg: "%s", typ: $funcType(%s)}`, name, encodeString(method.Name()), pkgPath, funcCtx.initArgs(t)) - if _, isPtr := t.Recv().Type().(*types.Pointer); isPtr { - ptrMethods = append(ptrMethods, entry) - continue + if len(ptrMethods) > 0 { + funcCtx.Printf("%s.methods = [%s];", funcCtx.typeName(types.NewPointer(named)), strings.Join(ptrMethods, ", ")) } - methods = append(methods, entry) - } - if len(methods) > 0 { - funcCtx.Printf("%s.methods = [%s];", funcCtx.typeName(named), strings.Join(methods, ", ")) - } - if len(ptrMethods) > 0 { - funcCtx.Printf("%s.methods = [%s];", funcCtx.typeName(types.NewPointer(named)), strings.Join(ptrMethods, ", ")) + }) + switch t := underlying.(type) { + case *types.Array, *types.Chan, *types.Interface, *types.Map, *types.Pointer, *types.Slice, *types.Signature, *types.Struct: + d.TypeInitCode = funcCtx.CatchOutput(0, func() { + funcCtx.Printf("%s.init(%s);", funcCtx.instName(inst), funcCtx.initArgs(t)) + }) } }) - switch t := o.Type().Underlying().(type) { - case *types.Array, *types.Chan, *types.Interface, *types.Map, *types.Pointer, *types.Slice, *types.Signature, *types.Struct: - d.TypeInitCode = funcCtx.CatchOutput(0, func() { - funcCtx.Printf("%s.init(%s);", funcCtx.objectName(o), funcCtx.initArgs(t)) - }) - } - }) - typeDecls = append(typeDecls, &d) + typeDecls = append(typeDecls, &d) + } + funcCtx.typeResolver = nil } // anonymous types @@ -727,39 +755,35 @@ func (fc *funcContext) translateToplevelFunction(fun *ast.FuncDecl, info *analys return code.Bytes() } - recvType := sig.Recv().Type() - ptr, isPointer := recvType.(*types.Pointer) - namedRecvType, _ := recvType.(*types.Named) - if isPointer { - namedRecvType = ptr.Elem().(*types.Named) - } - typeName := fc.objectName(namedRecvType.Obj()) + recvInst := inst.Recv() + recvInstName := fc.instName(recvInst) + recvType := recvInst.Object.Type().(*types.Named) funName := fun.Name.Name if reservedKeywords[funName] { funName += "$" } - if _, isStruct := namedRecvType.Underlying().(*types.Struct); isStruct { - code.Write(primaryFunction(typeName + ".ptr.prototype." + funName)) - fmt.Fprintf(code, "\t%s.prototype.%s = function(%s) { return this.$val.%s(%s); };\n", typeName, funName, joinedParams, funName, joinedParams) + if _, isStruct := recvType.Underlying().(*types.Struct); isStruct { + code.Write(primaryFunction(recvInstName + ".ptr.prototype." + funName)) + fmt.Fprintf(code, "\t%s.prototype.%s = function(%s) { return this.$val.%s(%s); };\n", recvInstName, funName, joinedParams, funName, joinedParams) return code.Bytes() } - if isPointer { + if ptr, isPointer := sig.Recv().Type().(*types.Pointer); isPointer { if _, isArray := ptr.Elem().Underlying().(*types.Array); isArray { - code.Write(primaryFunction(typeName + ".prototype." + funName)) - fmt.Fprintf(code, "\t$ptrType(%s).prototype.%s = function(%s) { return (new %s(this.$get())).%s(%s); };\n", typeName, funName, joinedParams, typeName, funName, joinedParams) + code.Write(primaryFunction(recvInstName + ".prototype." + funName)) + fmt.Fprintf(code, "\t$ptrType(%s).prototype.%s = function(%s) { return (new %s(this.$get())).%s(%s); };\n", recvInstName, funName, joinedParams, recvInstName, funName, joinedParams) return code.Bytes() } - return primaryFunction(fmt.Sprintf("$ptrType(%s).prototype.%s", typeName, funName)) + return primaryFunction(fmt.Sprintf("$ptrType(%s).prototype.%s", recvInstName, funName)) } value := "this.$get()" if isWrapped(recvType) { - value = fmt.Sprintf("new %s(%s)", typeName, value) + value = fmt.Sprintf("new %s(%s)", recvInstName, value) } - code.Write(primaryFunction(typeName + ".prototype." + funName)) - fmt.Fprintf(code, "\t$ptrType(%s).prototype.%s = function(%s) { return %s.%s(%s); };\n", typeName, funName, joinedParams, value, funName, joinedParams) + code.Write(primaryFunction(recvInstName + ".prototype." + funName)) + fmt.Fprintf(code, "\t$ptrType(%s).prototype.%s = function(%s) { return %s.%s(%s); };\n", recvInstName, funName, joinedParams, value, funName, joinedParams) return code.Bytes() } diff --git a/compiler/typesutil/typesutil.go b/compiler/typesutil/typesutil.go index 600925b81..60728d3ae 100644 --- a/compiler/typesutil/typesutil.go +++ b/compiler/typesutil/typesutil.go @@ -14,3 +14,20 @@ func IsJsObject(t types.Type) bool { named, isNamed := ptr.Elem().(*types.Named) return isNamed && IsJsPackage(named.Obj().Pkg()) && named.Obj().Name() == "Object" } + +// RecvType returns a named type of a method receiver, or nil if it's not a method. +// +// For methods on a pointer receiver, the underlying named type is returned. +func RecvType(sig *types.Signature) *types.Named { + recv := sig.Recv() + if recv == nil { + return nil + } + + typ := recv.Type() + if ptrType, ok := typ.(*types.Pointer); ok { + typ = ptrType.Elem() + } + + return typ.(*types.Named) +} diff --git a/compiler/utils.go b/compiler/utils.go index 6d244a236..54239bee8 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -401,7 +401,11 @@ func (fc *funcContext) typeName(ty types.Type) string { if t.Obj().Name() == "error" { return "$error" } - return fc.objectName(t.Obj()) + inst := typeparams.Instance{Object: t.Obj()} + for i := 0; i < t.TypeArgs().Len(); i++ { + inst.TArgs = append(inst.TArgs, t.TypeArgs().At(i)) + } + return fc.instName(inst) case *types.Interface: if t.Empty() { return "$emptyInterface" diff --git a/go.mod b/go.mod index 19d78c54e..03879b3f3 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ 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 56fd0e995..1288a90dd 100644 --- a/go.sum +++ b/go.sum @@ -272,6 +272,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +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= diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 9f09271f3..020a01209 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -154,84 +154,42 @@ var knownFails = map[string]failReason{ // 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/absdiff.go": {category: generics, desc: "missing operator support for generic types"}, - "typeparam/absdiff2.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/boundmethod.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/chans.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/combine.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, - "typeparam/cons.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/dictionaryCapture-noinline.go": {category: generics, desc: "attempts to pass -gcflags=\"-G=3\" flag, incorrectly parsed by run.go"}, - "typeparam/dictionaryCapture.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/dottype.go": {category: generics, desc: "not triaged"}, - "typeparam/eface.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/equal.go": {category: generics, desc: "missing support for the comparable type constraint"}, - "typeparam/genembed.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/genembed2.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/graph.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/interfacearg.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, - "typeparam/issue44688.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue45817.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, - "typeparam/issue47272.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47713.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47716.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47740.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47740b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47775b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47877.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47901.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47925.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47925b.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/issue47925c.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/issue47925d.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/issue48013.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, - "typeparam/issue48042.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48047.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48049.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48225.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48253.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48317.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, - "typeparam/issue48318.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, - "typeparam/issue48344.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48598.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48602.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48617.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48645a.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48645b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48838.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue49295.go": {category: generics, desc: "len() doesn't support generic pointer to array types"}, - "typeparam/issue49421.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, - "typeparam/issue49547.go": {category: generics, desc: "incorrect type strings for parameterized types"}, - "typeparam/issue49659b.go": {category: generics, desc: "incorrect type strings for parameterized types"}, - "typeparam/issue50002.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/issue50109.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue50109b.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/issue50264.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue50419.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue50642.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue50690a.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue50690b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue50690c.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, - "typeparam/issue51303.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue52026.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue53477.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/list.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/list2.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/lockable.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/metrics.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/nested.go": {category: generics, desc: "missing comparison operator support for generic types"}, - "typeparam/ordered.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/orderedmap.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/pair.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, - "typeparam/sets.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/shape1.go": {category: generics, desc: "not triaged"}, - "typeparam/stringable.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/struct.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, - "typeparam/subdict.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/typeswitch2.go": {category: generics, desc: "complex types have different print() format"}, - "typeparam/typeswitch3.go": {category: generics, desc: "missing support for type switching on generic types"}, - "typeparam/typeswitch5.go": {category: generics, desc: "different print() format for floating point types"}, - "typeparam/value.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/boundmethod.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/chans.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/dictionaryCapture.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/dottype.go": {category: generics, desc: "not triaged"}, + "typeparam/eface.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/equal.go": {category: generics, desc: "missing support for the comparable type constraint"}, + "typeparam/issue44688.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, + "typeparam/issue47713.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue47716.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue47901.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue47925b.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/issue47925c.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/issue47925d.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/issue48253.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48598.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48602.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue48645b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue49295.go": {category: generics, desc: "len() doesn't support generic pointer to array types"}, + "typeparam/issue49547.go": {category: generics, desc: "incorrect type strings for parameterized types"}, + "typeparam/issue50002.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/issue50109b.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/issue50264.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue51303.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/issue53477.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, + "typeparam/list2.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/metrics.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/nested.go": {category: generics, desc: "missing comparison operator support for generic types"}, + "typeparam/ordered.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/orderedmap.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/sets.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/shape1.go": {category: generics, desc: "not triaged"}, + "typeparam/struct.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, + "typeparam/typeswitch2.go": {category: generics, desc: "complex types have different print() format"}, + "typeparam/typeswitch3.go": {category: generics, desc: "missing support for type switching on generic types"}, + "typeparam/typeswitch5.go": {category: generics, desc: "different print() format for floating point types"}, } type failCategory uint8 From b65288dd0a191679fd7fb208d7d9d6d999aee542 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Fri, 5 Jan 2024 16:49:54 +0000 Subject: [PATCH 08/28] Re-triage failing test cases. --- tests/gorepo/run.go | 70 ++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 020a01209..513b52886 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -154,42 +154,42 @@ var knownFails = map[string]failReason{ // 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/boundmethod.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/chans.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/dictionaryCapture.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/dottype.go": {category: generics, desc: "not triaged"}, - "typeparam/eface.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/equal.go": {category: generics, desc: "missing support for the comparable type constraint"}, - "typeparam/issue44688.go": {category: generics, desc: "missing support for parameterized type instantiation"}, + "typeparam/boundmethod.go": {category: generics, desc: "bug: method expression causes runtime error on invocation"}, + "typeparam/chans.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, + "typeparam/dictionaryCapture.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, + "typeparam/dottype.go": {category: generics, desc: "bug: type assertion: Cannot read property 'kind' of undefined"}, + "typeparam/eface.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, + "typeparam/equal.go": {category: generics, desc: "bug: equal: Cannot read property 'kind' of undefined"}, + "typeparam/issue44688.go": {category: generics, desc: "bug: Type$1 is not defined"}, "typeparam/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, - "typeparam/issue47713.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47716.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47901.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue47925b.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/issue47925c.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/issue47925d.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/issue48253.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48598.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48602.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue48645b.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue49295.go": {category: generics, desc: "len() doesn't support generic pointer to array types"}, - "typeparam/issue49547.go": {category: generics, desc: "incorrect type strings for parameterized types"}, - "typeparam/issue50002.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/issue50109b.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/issue50264.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue51303.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/issue53477.go": {category: generics, desc: "missing support for conversion into a parameterized type"}, - "typeparam/list2.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/metrics.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/nested.go": {category: generics, desc: "missing comparison operator support for generic types"}, - "typeparam/ordered.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/orderedmap.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/sets.go": {category: generics, desc: "missing support for parameterized type instantiation"}, - "typeparam/shape1.go": {category: generics, desc: "not triaged"}, - "typeparam/struct.go": {category: generics, desc: "undiagnosed: nil pointer panic in the compiler"}, - "typeparam/typeswitch2.go": {category: generics, desc: "complex types have different print() format"}, - "typeparam/typeswitch3.go": {category: generics, desc: "missing support for type switching on generic types"}, - "typeparam/typeswitch5.go": {category: generics, desc: "different print() format for floating point types"}, + "typeparam/issue47713.go": {category: generics, desc: "bug: Cannot create property '$ptr' on XXX"}, + "typeparam/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, + "typeparam/issue47901.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, + "typeparam/issue47925b.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, + "typeparam/issue47925c.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, + "typeparam/issue47925d.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, + "typeparam/issue48253.go": {category: generics, desc: "bug: Type$1 is not defined"}, + "typeparam/issue48598.go": {category: generics, desc: "bug: compiler panic: R has unexpected type *types.TypeParam at compiler/utils.go:884"}, + "typeparam/issue48602.go": {category: generics, desc: "bug: compiler panic: R has unexpected type *types.TypeParam at compiler/utils.go:884"}, + "typeparam/issue48645b.go": {category: generics, desc: "bug: compiler panic: R has unexpected type *types.TypeParam at compiler/utils.go:884"}, + "typeparam/issue49295.go": {category: generics, desc: "bug: compiler panic: interface conversion: interface {} is int64, not int at compiler/expressions.go:1458"}, + "typeparam/issue49547.go": {category: generics, desc: "bug: incorrect type string"}, + "typeparam/issue50002.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, + "typeparam/issue50109b.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, + "typeparam/issue50264.go": {category: generics, desc: "bug: clone: Cannot read property 'zero' of undefined"}, + "typeparam/issue51303.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, + "typeparam/issue53477.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, + "typeparam/list2.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, + "typeparam/metrics.go": {category: generics, desc: "bug: append: Cannot read property '$array' of undefined"}, + "typeparam/nested.go": {category: generics, desc: "bug: compiler panic: ast.Walk: unexpected node type "}, + "typeparam/ordered.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, + "typeparam/orderedmap.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, + "typeparam/sets.go": {category: generics, desc: "bug: clone: Cannot read property 'zero' of undefined"}, + "typeparam/shape1.go": {category: generics, desc: "bug: Cannot create property '$ptr' on XXX"}, + "typeparam/struct.go": {category: generics, desc: "bug: Type$1 is not defined"}, + "typeparam/typeswitch2.go": {category: generics, desc: "undiagnosed: incorrect output"}, + "typeparam/typeswitch3.go": {category: generics, desc: "undiagnosed: incorrect output"}, + "typeparam/typeswitch5.go": {category: generics, desc: "undiagnosed: incorrect output"}, } type failCategory uint8 From 8f8d041b28d119e10550bd47ee786999fc31ae96 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Fri, 5 Jan 2024 17:23:13 +0000 Subject: [PATCH 09/28] Correctly detect type conversions to generic types. Type expressions for generic types contain an index expressions, which was previously impossible. With this change, we correctly recognize and check them. --- compiler/analysis/info.go | 4 ++-- compiler/astutil/astutil.go | 22 ++++++++++++++++++++++ tests/gorepo/run.go | 16 ++-------------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/compiler/analysis/info.go b/compiler/analysis/info.go index 07174a932..26008e172 100644 --- a/compiler/analysis/info.go +++ b/compiler/analysis/info.go @@ -343,8 +343,8 @@ func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor { return nil // No need to walk under this CallExpr, we already did it manually. default: if astutil.IsTypeExpr(f, fi.pkgInfo.Info) { - // This is a type assertion, not a call. Type assertion itself is not - // blocking, but we will visit the expression itself. + // 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. diff --git a/compiler/astutil/astutil.go b/compiler/astutil/astutil.go index dfa820438..9c8787304 100644 --- a/compiler/astutil/astutil.go +++ b/compiler/astutil/astutil.go @@ -31,7 +31,15 @@ func NewIdent(name string, t types.Type, info *types.Info, pkg *types.Package) * return ident } +// IsTypeExpr returns true if expr denotes a type. This can be used to +// distinguish between calls and type conversions. func IsTypeExpr(expr ast.Expr, info *types.Info) bool { + // Note that we could've used info.Types[expr].IsType() instead of doing our + // own analysis. However, that creates a problem because we synthesize some + // *ast.CallExpr nodes and, more importantly, *ast.Ident nodes that denote a + // type. Unfortunately, because the flag that controls + // types.TypeAndValue.IsType() return value is unexported we wouldn't be able + // to set it correctly. Thus, we can't rely on IsType(). switch e := expr.(type) { case *ast.ArrayType, *ast.ChanType, *ast.FuncType, *ast.InterfaceType, *ast.MapType, *ast.StructType: return true @@ -43,6 +51,20 @@ func IsTypeExpr(expr ast.Expr, info *types.Info) bool { case *ast.SelectorExpr: _, ok := info.Uses[e.Sel].(*types.TypeName) return ok + case *ast.IndexExpr: + ident, ok := e.X.(*ast.Ident) + if !ok { + return false + } + _, ok = info.Uses[ident].(*types.TypeName) + return ok + case *ast.IndexListExpr: + ident, ok := e.X.(*ast.Ident) + if !ok { + return false + } + _, ok = info.Uses[ident].(*types.TypeName) + return ok case *ast.ParenExpr: return IsTypeExpr(e.X, info) default: diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 513b52886..b52123031 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -158,31 +158,19 @@ var knownFails = map[string]failReason{ "typeparam/chans.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, "typeparam/dictionaryCapture.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, "typeparam/dottype.go": {category: generics, desc: "bug: type assertion: Cannot read property 'kind' of undefined"}, - "typeparam/eface.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, "typeparam/equal.go": {category: generics, desc: "bug: equal: Cannot read property 'kind' of undefined"}, "typeparam/issue44688.go": {category: generics, desc: "bug: Type$1 is not defined"}, "typeparam/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, "typeparam/issue47713.go": {category: generics, desc: "bug: Cannot create property '$ptr' on XXX"}, "typeparam/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, - "typeparam/issue47901.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, - "typeparam/issue47925b.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, - "typeparam/issue47925c.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, - "typeparam/issue47925d.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, "typeparam/issue48253.go": {category: generics, desc: "bug: Type$1 is not defined"}, - "typeparam/issue48598.go": {category: generics, desc: "bug: compiler panic: R has unexpected type *types.TypeParam at compiler/utils.go:884"}, - "typeparam/issue48602.go": {category: generics, desc: "bug: compiler panic: R has unexpected type *types.TypeParam at compiler/utils.go:884"}, - "typeparam/issue48645b.go": {category: generics, desc: "bug: compiler panic: R has unexpected type *types.TypeParam at compiler/utils.go:884"}, + "typeparam/issue48645b.go": {category: generics, desc: "bug: clone: Cannot read property 'zero' of undefined"}, "typeparam/issue49295.go": {category: generics, desc: "bug: compiler panic: interface conversion: interface {} is int64, not int at compiler/expressions.go:1458"}, "typeparam/issue49547.go": {category: generics, desc: "bug: incorrect type string"}, - "typeparam/issue50002.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, - "typeparam/issue50109b.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, + "typeparam/issue50109b.go": {category: generics, desc: "bug: Type$1 is not defined"}, "typeparam/issue50264.go": {category: generics, desc: "bug: clone: Cannot read property 'zero' of undefined"}, - "typeparam/issue51303.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, - "typeparam/issue53477.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, - "typeparam/list2.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, "typeparam/metrics.go": {category: generics, desc: "bug: append: Cannot read property '$array' of undefined"}, "typeparam/nested.go": {category: generics, desc: "bug: compiler panic: ast.Walk: unexpected node type "}, - "typeparam/ordered.go": {category: generics, desc: "bug: compiler panic: interface conversion: types.Type is *types.Interface, not *types.Signature at compiler/expressions.go:607"}, "typeparam/orderedmap.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, "typeparam/sets.go": {category: generics, desc: "bug: clone: Cannot read property 'zero' of undefined"}, "typeparam/shape1.go": {category: generics, desc: "bug: Cannot create property '$ptr' on XXX"}, From eff58dcbdc787044d0c753cd4152433fcfbe8006 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Fri, 5 Jan 2024 18:05:46 +0000 Subject: [PATCH 10/28] Correctly handle all integer types in funcContext.formatExpr(). Previously passing any type other than int would cause a type assertion failure and a panic. This bug could be triggered when evaluating a result of len() built-in for array types. However, it remained hidden in non-generic code, since the expression was determined to be constant, short-circuiting translation. Apparently, in generic code go/types can't prove the constant value, thus revealing the bug. --- compiler/expressions.go | 2 +- tests/gorepo/run.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/compiler/expressions.go b/compiler/expressions.go index 3cc2e2bd4..513d03430 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -1455,7 +1455,7 @@ func (fc *funcContext) formatExprInternal(format string, a []interface{}, parens } out.WriteString(a[n].(string)) case 'd': - out.WriteString(strconv.Itoa(a[n].(int))) + fmt.Fprintf(out, "%d", a[n]) case 't': out.WriteString(a[n].(token.Token).String()) case 'e': diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index b52123031..e616ae98d 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -165,7 +165,6 @@ var knownFails = map[string]failReason{ "typeparam/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, "typeparam/issue48253.go": {category: generics, desc: "bug: Type$1 is not defined"}, "typeparam/issue48645b.go": {category: generics, desc: "bug: clone: Cannot read property 'zero' of undefined"}, - "typeparam/issue49295.go": {category: generics, desc: "bug: compiler panic: interface conversion: interface {} is int64, not int at compiler/expressions.go:1458"}, "typeparam/issue49547.go": {category: generics, desc: "bug: incorrect type string"}, "typeparam/issue50109b.go": {category: generics, desc: "bug: Type$1 is not defined"}, "typeparam/issue50264.go": {category: generics, desc: "bug: clone: Cannot read property 'zero' of undefined"}, From 2ca8aabcda298d3f48d63365a4bb21acd0ddf49d Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Fri, 5 Jan 2024 19:18:17 +0000 Subject: [PATCH 11/28] Discover generic type declarations inside generic functions. Prior to this change, while scanning for seed generic instances we would not traverser generic function (or method) bodies, which lead to generic types declared inside them not being added to objMap. Later, when an instance of such type was discovered, we had no matching AST node to process for further instantiations. With this change, we do traverse generic function bodies, but only to populate objMap. We ignore potential type instantiations in this case because they would be defined in terms of a type param, which are not interesting to us. --- compiler/internal/typeparams/collect.go | 17 ++++++++++---- compiler/internal/typeparams/collect_test.go | 9 +++++++- internal/srctesting/srctesting.go | 24 ++++++++++++++++---- tests/gorepo/run.go | 2 +- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index db088b56d..2ec4e63b3 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -122,7 +122,8 @@ func (c *visitor) Visit(n ast.Node) (w ast.Visitor) { // - Collects an initial set of generic instantiations in the non-generic code. type seedVisitor struct { visitor - objMap map[types.Object]ast.Node + objMap map[types.Object]ast.Node + mapOnly bool // Only build up objMap, ignore any instances. } var _ ast.Visitor = &seedVisitor{} @@ -137,7 +138,11 @@ func (c *seedVisitor) Visit(n ast.Node) ast.Visitor { sig := obj.Type().(*types.Signature) if sig.TypeParams().Len() != 0 || sig.RecvTypeParams().Len() != 0 { c.objMap[obj] = n - return nil + return &seedVisitor{ + visitor: c.visitor, + objMap: c.objMap, + mapOnly: true, + } } case *ast.TypeSpec: obj := c.info.Defs[n.Name] @@ -151,9 +156,11 @@ func (c *seedVisitor) Visit(n ast.Node) ast.Visitor { } } - // Otherwise check for fully defined instantiations and descend further into - // the AST tree. - c.visitor.Visit(n) + if !c.mapOnly { + // Otherwise check for fully defined instantiations and descend further into + // the AST tree. + c.visitor.Visit(n) + } return c } diff --git a/compiler/internal/typeparams/collect_test.go b/compiler/internal/typeparams/collect_test.go index 778915c57..7081463ee 100644 --- a/compiler/internal/typeparams/collect_test.go +++ b/compiler/internal/typeparams/collect_test.go @@ -328,7 +328,12 @@ func TestCollector(t *testing.T) { src := `package test type typ[T any] int func (t typ[T]) method(arg T) { var _ typ[int]; fun[int8](0) } - func fun[T any](arg T) { var _ typ[int16] } + func fun[T any](arg T) { + var _ typ[int16] + + type nested[U any] struct{} + _ = nested[T]{} + } type ignore = int @@ -359,11 +364,13 @@ func TestCollector(t *testing.T) { 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]), } got := c.Instances.Values() if diff := cmp.Diff(want, got, instanceOpts()); diff != "" { diff --git a/internal/srctesting/srctesting.go b/internal/srctesting/srctesting.go index de3753efc..15b034d3f 100644 --- a/internal/srctesting/srctesting.go +++ b/internal/srctesting/srctesting.go @@ -86,11 +86,25 @@ func Format(t *testing.T, fset *token.FileSet, node any) string { // // Methods can be referred to as RecvTypeName.MethodName. func LookupObj(pkg *types.Package, name string) types.Object { - parts := strings.Split(name, ".") - obj := pkg.Scope().Lookup(parts[0]) - if len(parts) == 1 { - return obj + path := strings.Split(name, ".") + scope := pkg.Scope() + var obj types.Object + + for len(path) > 0 { + obj = scope.Lookup(path[0]) + path = path[1:] + + if fun, ok := obj.(*types.Func); ok { + scope = fun.Scope() + continue + } + + // If we are here, the latest object is a named type. If there are more path + // elements left, they must refer to field or method. + if len(path) > 0 { + obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), path[0]) + path = path[1:] + } } - obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), parts[1]) return obj } diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index e616ae98d..b20b663d8 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -169,7 +169,7 @@ var knownFails = map[string]failReason{ "typeparam/issue50109b.go": {category: generics, desc: "bug: Type$1 is not defined"}, "typeparam/issue50264.go": {category: generics, desc: "bug: clone: Cannot read property 'zero' of undefined"}, "typeparam/metrics.go": {category: generics, desc: "bug: append: Cannot read property '$array' of undefined"}, - "typeparam/nested.go": {category: generics, desc: "bug: compiler panic: ast.Walk: unexpected node type "}, + "typeparam/nested.go": {category: generics, desc: "bug: Cannot set property 'int' of undefined"}, "typeparam/orderedmap.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, "typeparam/sets.go": {category: generics, desc: "bug: clone: Cannot read property 'zero' of undefined"}, "typeparam/shape1.go": {category: generics, desc: "bug: Cannot create property '$ptr' on XXX"}, From aea4239e75f127fbb0a20441c73d8cf716922bf3 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 20 Jan 2024 20:04:52 +0000 Subject: [PATCH 12/28] Perform type substitution for method selection expressions. When calling a method on a type param or a type defined using a type param we replace the original selection from go/types.Info.Selections with a synthetic counterpart for the instantiated receiver type. Unfortunately, we can't construct instances of types.Selection due to its fields being private, so we create an interface-compatible implementation instead. --- compiler/expressions.go | 15 ++-- compiler/internal/typeparams/collect.go | 40 ++++++++++- compiler/internal/typeparams/collect_test.go | 72 +++++++++++++++++++- compiler/package.go | 37 +--------- compiler/statements.go | 2 +- compiler/typesutil/typesutil.go | 46 +++++++++++++ compiler/utils.go | 12 +++- tests/gorepo/run.go | 41 +++++------ 8 files changed, 194 insertions(+), 71 deletions(-) diff --git a/compiler/expressions.go b/compiler/expressions.go index 513d03430..ad6597789 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -247,7 +247,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } return fc.formatExpr(`(%1s || (%1s = new %2s(function() { return %3s; }, function($v) { %4s })))`, fc.varPtrName(obj), fc.typeName(exprType), fc.objectName(obj), fc.translateAssign(x, fc.newIdent("$v", elemType), false)) case *ast.SelectorExpr: - sel, ok := fc.pkgCtx.SelectionOf(x) + sel, ok := fc.selectionOf(x) if !ok { // qualified identifier obj := fc.pkgCtx.Uses[x.Sel].(*types.Var) @@ -567,7 +567,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } case *ast.SelectorExpr: - sel, ok := fc.pkgCtx.SelectionOf(e) + sel, ok := fc.selectionOf(e) if !ok { // qualified identifier return fc.formatExpr("%s", fc.instName(inst)) @@ -618,7 +618,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { return fc.translateCall(e, sig, fc.translateExpr(f)) case *ast.SelectorExpr: - sel, ok := fc.pkgCtx.SelectionOf(f) + sel, ok := fc.selectionOf(f) if !ok { // qualified identifier obj := fc.pkgCtx.Uses[f.Sel] @@ -902,7 +902,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, } func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression { - sel, _ := fc.pkgCtx.SelectionOf(e) + sel, _ := fc.selectionOf(e) if !sel.Obj().Exported() { fc.pkgCtx.dependencies[sel.Obj()] = true } @@ -919,12 +919,7 @@ func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression { } fakeSel := &ast.SelectorExpr{X: x, Sel: ast.NewIdent("o")} - fc.pkgCtx.additionalSelections[fakeSel] = &fakeSelection{ - kind: types.FieldVal, - recv: sel.Recv(), - index: sel.Index()[:len(sel.Index())-1], - typ: recvType, - } + fc.pkgCtx.additionalSelections[fakeSel] = typesutil.NewSelection(types.FieldVal, sel.Recv(), sel.Index()[:len(sel.Index())-1], nil, recvType) x = fc.setType(fakeSel, recvType) } diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index 2ec4e63b3..a6d7fc703 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -16,7 +16,8 @@ type Resolver struct { TContext *types.Context Map map[*types.TypeParam]types.Type - memo typesutil.Map[types.Type] + memo typesutil.Map[types.Type] + selMemo map[typesutil.Selection]typesutil.Selection } // NewResolver creates a new Resolver with tParams entries mapping to tArgs @@ -25,6 +26,7 @@ func NewResolver(tc *types.Context, tParams []*types.TypeParam, tArgs []types.Ty r := &Resolver{ TContext: tc, Map: map[*types.TypeParam]types.Type{}, + selMemo: map[typesutil.Selection]typesutil.Selection{}, } if len(tParams) != len(tArgs) { panic(fmt.Errorf("len(tParams)=%d not equal len(tArgs)=%d", len(tParams), len(tArgs))) @@ -59,6 +61,42 @@ func (r *Resolver) SubstituteAll(list *types.TypeList) []types.Type { 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 || r.Map == nil || sel == nil { + return sel // No substitutions to be made. + } + if concrete, ok := r.selMemo[sel]; ok { + return concrete + } + + switch sel.Kind() { + case types.FieldVal: + return sel + case types.MethodExpr, types.MethodVal: + recv := r.Substitute(sel.Recv()) + if 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()) + sig := obj.Type().(*types.Signature) + + if sel.Kind() == types.MethodExpr { + sig = typesutil.RecvAsFirstArg(sig) + } + concrete := typesutil.NewSelection(sel.Kind(), recv, index, obj, sig) + r.selMemo[sel] = concrete + return concrete + default: + panic(fmt.Errorf("unexpected selection kind %v: %v", sel.Kind(), sel)) + } +} + // ToSlice converts TypeParamList into a slice with the sale order of entries. func ToSlice(tpl *types.TypeParamList) []*types.TypeParam { result := make([]*types.TypeParam, tpl.Len()) diff --git a/compiler/internal/typeparams/collect_test.go b/compiler/internal/typeparams/collect_test.go index 7081463ee..64aa43778 100644 --- a/compiler/internal/typeparams/collect_test.go +++ b/compiler/internal/typeparams/collect_test.go @@ -328,7 +328,7 @@ func TestCollector(t *testing.T) { src := `package test type typ[T any] int func (t typ[T]) method(arg T) { var _ typ[int]; fun[int8](0) } - func fun[T any](arg T) { + func fun[T any](arg T) { var _ typ[int16] type nested[U any] struct{} @@ -377,3 +377,73 @@ func TestCollector(t *testing.T) { t.Errorf("Instances from initialSeeder contain diff (-want,+got):\n%s", diff) } } + +func TestResolver_SubstituteSelection(t *testing.T) { + tests := []struct { + descr string + src string + wantObj string + wantSig string + }{{ + descr: "type parameter method", + src: `package test + type stringer interface{ String() string } + + type x struct{} + func (_ x) String() string { return "" } + + type g[T stringer] struct{} + func (_ g[T]) Method(t T) string { + return t.String() + }`, + wantObj: "func (test.x).String() string", + wantSig: "func() string", + }, { + descr: "generic receiver type with type parameter", + src: `package test + type x struct{} + + type g[T any] struct{} + func (_ g[T]) Method(t T) string { + return g[T]{}.Method(t) + }`, + wantObj: "func (test.g[test.x]).Method(t test.x) string", + wantSig: "func(t test.x) string", + }, { + descr: "method expression", + src: `package test + type x struct{} + + type g[T any] struct{} + func (recv g[T]) Method(t T) string { + return g[T].Method(recv, t) + }`, + wantObj: "func (test.g[test.x]).Method(t test.x) string", + wantSig: "func(recv test.g[test.x], t test.x) string", + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + fset := token.NewFileSet() + file := srctesting.Parse(t, fset, test.src) + info, pkg := srctesting.Check(t, fset, 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()}) + + if l := len(info.Selections); l != 1 { + t.Fatalf("Got: %d selections. Want: 1", l) + } + for _, sel := range info.Selections { + gotObj := types.ObjectString(resolver.SubstituteSelection(sel).Obj(), nil) + if gotObj != test.wantObj { + t.Fatalf("Got: resolver.SubstituteSelection().Obj() = %q. Want: %q.", gotObj, test.wantObj) + } + gotSig := types.TypeString(resolver.SubstituteSelection(sel).Type(), nil) + if gotSig != test.wantSig { + t.Fatalf("Got: resolver.SubstituteSelection().Type() = %q. Want: %q.", gotSig, test.wantSig) + } + } + }) + } +} diff --git a/compiler/package.go b/compiler/package.go index 10732f84f..761b14e72 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -16,6 +16,7 @@ import ( "github.com/gopherjs/gopherjs/compiler/astutil" "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" + "github.com/gopherjs/gopherjs/compiler/typesutil" "github.com/neelance/astrewrite" "golang.org/x/tools/go/gcexportdata" "golang.org/x/tools/go/types/typeutil" @@ -24,7 +25,7 @@ import ( // pkgContext maintains compiler context for a specific package. type pkgContext struct { *analysis.Info - additionalSelections map[*ast.SelectorExpr]selection + additionalSelections map[*ast.SelectorExpr]typesutil.Selection typesCtx *types.Context typeNames []*types.TypeName @@ -40,38 +41,6 @@ type pkgContext struct { errList ErrorList } -func (p *pkgContext) SelectionOf(e *ast.SelectorExpr) (selection, bool) { - if sel, ok := p.Selections[e]; ok { - return sel, true - } - if sel, ok := p.additionalSelections[e]; ok { - return sel, true - } - return nil, false -} - -type selection interface { - Kind() types.SelectionKind - Recv() types.Type - Index() []int - Obj() types.Object - Type() types.Type -} - -type fakeSelection struct { - kind types.SelectionKind - recv types.Type - index []int - obj types.Object - typ types.Type -} - -func (sel *fakeSelection) Kind() types.SelectionKind { return sel.kind } -func (sel *fakeSelection) Recv() types.Type { return sel.recv } -func (sel *fakeSelection) Index() []int { return sel.index } -func (sel *fakeSelection) Obj() types.Object { return sel.obj } -func (sel *fakeSelection) Type() types.Type { return sel.typ } - // funcContext maintains compiler context for a specific function (lexical scope?). type funcContext struct { *analysis.FuncInfo @@ -253,7 +222,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor FuncInfo: pkgInfo.InitFuncInfo, pkgCtx: &pkgContext{ Info: pkgInfo, - additionalSelections: make(map[*ast.SelectorExpr]selection), + additionalSelections: make(map[*ast.SelectorExpr]typesutil.Selection), typesCtx: config.Context, pkgVars: make(map[string]string), diff --git a/compiler/statements.go b/compiler/statements.go index 0df5e72b6..a38d823fe 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -742,7 +742,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { case *ast.Ident: return fmt.Sprintf("%s = %s;", fc.objectName(fc.pkgCtx.ObjectOf(l)), rhsExpr) case *ast.SelectorExpr: - sel, ok := fc.pkgCtx.SelectionOf(l) + sel, ok := fc.selectionOf(l) if !ok { // qualified identifier return fmt.Sprintf("%s = %s;", fc.objectName(fc.pkgCtx.Uses[l.Sel]), rhsExpr) diff --git a/compiler/typesutil/typesutil.go b/compiler/typesutil/typesutil.go index 60728d3ae..91b174417 100644 --- a/compiler/typesutil/typesutil.go +++ b/compiler/typesutil/typesutil.go @@ -31,3 +31,49 @@ func RecvType(sig *types.Signature) *types.Named { return typ.(*types.Named) } + +// RecvAsFirstArg takes a method signature and returns a function +// signature with receiver as the first parameter. +func RecvAsFirstArg(sig *types.Signature) *types.Signature { + params := make([]*types.Var, 0, 1+sig.Params().Len()) + params = append(params, sig.Recv()) + for i := 0; i < sig.Params().Len(); i++ { + params = append(params, sig.Params().At(i)) + } + return types.NewSignatureType(nil, nil, nil, types.NewTuple(params...), sig.Results(), sig.Variadic()) +} + +// Selection is a common interface for go/types.Selection and our custom-constructed +// method and field selections. +type Selection interface { + Kind() types.SelectionKind + Recv() types.Type + Index() []int + Obj() types.Object + Type() types.Type +} + +// NewSelection creates a new selection. +func NewSelection(kind types.SelectionKind, recv types.Type, index []int, obj types.Object, typ types.Type) Selection { + return &selectionImpl{ + kind: kind, + recv: recv, + index: index, + obj: obj, + typ: typ, + } +} + +type selectionImpl struct { + kind types.SelectionKind + recv types.Type + index []int + obj types.Object + typ types.Type +} + +func (sel *selectionImpl) Kind() types.SelectionKind { return sel.kind } +func (sel *selectionImpl) Recv() types.Type { return sel.recv } +func (sel *selectionImpl) Index() []int { return sel.index } +func (sel *selectionImpl) Obj() types.Object { return sel.obj } +func (sel *selectionImpl) Type() types.Type { return sel.typ } diff --git a/compiler/utils.go b/compiler/utils.go index 54239bee8..c7e68d953 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -167,7 +167,7 @@ func (fc *funcContext) translateArgs(sig *types.Signature, argExprs []ast.Expr, return args } -func (fc *funcContext) translateSelection(sel selection, pos token.Pos) ([]string, string) { +func (fc *funcContext) translateSelection(sel typesutil.Selection, pos token.Pos) ([]string, string) { var fields []string t := sel.Recv() for _, index := range sel.Index() { @@ -442,6 +442,16 @@ func (fc *funcContext) typeOf(expr ast.Expr) types.Type { return fc.typeResolver.Substitute(fc.pkgCtx.TypeOf(expr)) } +func (fc *funcContext) selectionOf(e *ast.SelectorExpr) (typesutil.Selection, bool) { + if sel, ok := fc.pkgCtx.Selections[e]; ok { + return fc.typeResolver.SubstituteSelection(sel), true + } + if sel, ok := fc.pkgCtx.additionalSelections[e]; ok { + return sel, true + } + return nil, false +} + func (fc *funcContext) externalize(s string, t types.Type) string { if typesutil.IsJsObject(t) { return s diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index b20b663d8..0b898337f 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -154,29 +154,24 @@ var knownFails = map[string]failReason{ // 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/boundmethod.go": {category: generics, desc: "bug: method expression causes runtime error on invocation"}, - "typeparam/chans.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, - "typeparam/dictionaryCapture.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, - "typeparam/dottype.go": {category: generics, desc: "bug: type assertion: Cannot read property 'kind' of undefined"}, - "typeparam/equal.go": {category: generics, desc: "bug: equal: Cannot read property 'kind' of undefined"}, - "typeparam/issue44688.go": {category: generics, desc: "bug: Type$1 is not defined"}, - "typeparam/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, - "typeparam/issue47713.go": {category: generics, desc: "bug: Cannot create property '$ptr' on XXX"}, - "typeparam/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, - "typeparam/issue48253.go": {category: generics, desc: "bug: Type$1 is not defined"}, - "typeparam/issue48645b.go": {category: generics, desc: "bug: clone: Cannot read property 'zero' of undefined"}, - "typeparam/issue49547.go": {category: generics, desc: "bug: incorrect type string"}, - "typeparam/issue50109b.go": {category: generics, desc: "bug: Type$1 is not defined"}, - "typeparam/issue50264.go": {category: generics, desc: "bug: clone: Cannot read property 'zero' of undefined"}, - "typeparam/metrics.go": {category: generics, desc: "bug: append: Cannot read property '$array' of undefined"}, - "typeparam/nested.go": {category: generics, desc: "bug: Cannot set property 'int' of undefined"}, - "typeparam/orderedmap.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, - "typeparam/sets.go": {category: generics, desc: "bug: clone: Cannot read property 'zero' of undefined"}, - "typeparam/shape1.go": {category: generics, desc: "bug: Cannot create property '$ptr' on XXX"}, - "typeparam/struct.go": {category: generics, desc: "bug: Type$1 is not defined"}, - "typeparam/typeswitch2.go": {category: generics, desc: "undiagnosed: incorrect output"}, - "typeparam/typeswitch3.go": {category: generics, desc: "undiagnosed: incorrect output"}, - "typeparam/typeswitch5.go": {category: generics, desc: "undiagnosed: incorrect output"}, + "typeparam/chans.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, + "typeparam/dottype.go": {category: generics, desc: "bug: type assertion: Cannot read property 'kind' of undefined"}, + "typeparam/equal.go": {category: generics, desc: "bug: equal: Cannot read property 'kind' of undefined"}, + "typeparam/issue44688.go": {category: generics, desc: "bug: Type$1 is not defined"}, + "typeparam/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, + "typeparam/issue47713.go": {category: generics, desc: "bug: Cannot create property '$ptr' on XXX"}, + "typeparam/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, + "typeparam/issue48253.go": {category: generics, desc: "bug: Type$1 is not defined"}, + "typeparam/issue49547.go": {category: generics, desc: "bug: incorrect type string"}, + "typeparam/issue50109b.go": {category: generics, desc: "bug: Type$1 is not defined"}, + "typeparam/metrics.go": {category: generics, desc: "bug: append: Cannot read property '$array' of undefined"}, + "typeparam/nested.go": {category: generics, desc: "bug: Cannot set property 'int' of undefined"}, + "typeparam/orderedmap.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, + "typeparam/shape1.go": {category: generics, desc: "bug: Cannot create property '$ptr' on XXX"}, + "typeparam/struct.go": {category: generics, desc: "bug: Type$1 is not defined"}, + "typeparam/typeswitch2.go": {category: generics, desc: "undiagnosed: incorrect output"}, + "typeparam/typeswitch3.go": {category: generics, desc: "undiagnosed: incorrect output"}, + "typeparam/typeswitch5.go": {category: generics, desc: "undiagnosed: incorrect output"}, } type failCategory uint8 From 16af5b431d000390f75e92ba20d06b6ce23295c1 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 20 Jan 2024 21:22:24 +0000 Subject: [PATCH 13/28] Fix variable name allocation for inline types. Prior to generics an inline type was always first encountered by the compiler at its decl statement, where it reserved the variable name for it. With generics, it may be encountered before that when translating one of the instantiations of a generic type. With this change, variable name allocation will happen correctly regardless of where it would be encountered first. --- compiler/statements.go | 1 - compiler/utils.go | 15 ++++++++++++--- tests/gorepo/run.go | 2 -- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/compiler/statements.go b/compiler/statements.go index a38d823fe..dd40582b4 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -444,7 +444,6 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { for _, spec := range decl.Specs { o := fc.pkgCtx.Defs[spec.(*ast.TypeSpec).Name].(*types.TypeName) fc.pkgCtx.typeNames = append(fc.pkgCtx.typeNames, o) - fc.root().objectNames[o] = fc.newVariableWithLevel(o.Name(), true) fc.pkgCtx.dependencies[o] = true } case token.CONST: diff --git a/compiler/utils.go b/compiler/utils.go index c7e68d953..3c53c7c3d 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -328,7 +328,11 @@ func isVarOrConst(o types.Object) bool { } func isPkgLevel(o types.Object) bool { - return o.Parent() != nil && o.Parent().Parent() == types.Universe + // Note: named types are always assigned a variable at package level to be + // initialized with the rest of the package types, even the types declared + // in a statement inside a function. + _, isType := o.(*types.TypeName) + return (o.Parent() != nil && o.Parent().Parent() == types.Universe) || isType } // assignedObjectName checks if the object has been previously assigned a name @@ -359,8 +363,13 @@ func (fc *funcContext) objectName(o types.Object) string { name, ok := fc.assignedObjectName(o) if !ok { - name = fc.newVariableWithLevel(o.Name(), isPkgLevel(o)) - fc.objectNames[o] = name + pkgLevel := isPkgLevel(o) + name = fc.newVariableWithLevel(o.Name(), pkgLevel) + if pkgLevel { + fc.root().objectNames[o] = name + } else { + fc.objectNames[o] = name + } } if v, ok := o.(*types.Var); ok && fc.pkgCtx.escapingVars[v] { diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 0b898337f..e0529f064 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -155,8 +155,6 @@ var knownFails = map[string]failReason{ // should be emptied once https://github.com/gopherjs/gopherjs/issues/1013 is // fixed. "typeparam/chans.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, - "typeparam/dottype.go": {category: generics, desc: "bug: type assertion: Cannot read property 'kind' of undefined"}, - "typeparam/equal.go": {category: generics, desc: "bug: equal: Cannot read property 'kind' of undefined"}, "typeparam/issue44688.go": {category: generics, desc: "bug: Type$1 is not defined"}, "typeparam/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, "typeparam/issue47713.go": {category: generics, desc: "bug: Cannot create property '$ptr' on XXX"}, From b888728f0ce7ecd29ee6aed0ec1d2b3dc1194e18 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 21 Jan 2024 15:57:18 +0000 Subject: [PATCH 14/28] Fix pointer-taking of an escaping variable. Such variables are stored in a single-element arrays, and the pointer object is stored in the $ptr property of such an array. The earlier refactoring of object name assignment made the code generation to emit `varname[0].$ptr` instead of `varname.$ptr`, which broke it for primitive types. This change restores the original behavior. --- compiler/expressions.go | 7 ++++++- tests/gorepo/run.go | 2 -- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/compiler/expressions.go b/compiler/expressions.go index ad6597789..a2c4afacc 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -243,7 +243,12 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case *ast.Ident: obj := fc.pkgCtx.Uses[x].(*types.Var) if fc.pkgCtx.escapingVars[obj] { - return fc.formatExpr("(%1s.$ptr || (%1s.$ptr = new %2s(function() { return this.$target[0]; }, function($v) { this.$target[0] = $v; }, %1s)))", fc.objectName(obj), fc.typeName(exprType)) + name, ok := fc.assignedObjectName(obj) + if !ok { + // This should never happen. + panic(fmt.Errorf("escaping variable %s hasn't been assigned a JS name", obj)) + } + return fc.formatExpr("(%1s.$ptr || (%1s.$ptr = new %2s(function() { return this.$target[0]; }, function($v) { this.$target[0] = $v; }, %1s)))", name, fc.typeName(exprType)) } return fc.formatExpr(`(%1s || (%1s = new %2s(function() { return %3s; }, function($v) { %4s })))`, fc.varPtrName(obj), fc.typeName(exprType), fc.objectName(obj), fc.translateAssign(x, fc.newIdent("$v", elemType), false)) case *ast.SelectorExpr: diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index e0529f064..b72510863 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -157,7 +157,6 @@ var knownFails = map[string]failReason{ "typeparam/chans.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, "typeparam/issue44688.go": {category: generics, desc: "bug: Type$1 is not defined"}, "typeparam/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, - "typeparam/issue47713.go": {category: generics, desc: "bug: Cannot create property '$ptr' on XXX"}, "typeparam/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, "typeparam/issue48253.go": {category: generics, desc: "bug: Type$1 is not defined"}, "typeparam/issue49547.go": {category: generics, desc: "bug: incorrect type string"}, @@ -165,7 +164,6 @@ var knownFails = map[string]failReason{ "typeparam/metrics.go": {category: generics, desc: "bug: append: Cannot read property '$array' of undefined"}, "typeparam/nested.go": {category: generics, desc: "bug: Cannot set property 'int' of undefined"}, "typeparam/orderedmap.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, - "typeparam/shape1.go": {category: generics, desc: "bug: Cannot create property '$ptr' on XXX"}, "typeparam/struct.go": {category: generics, desc: "bug: Type$1 is not defined"}, "typeparam/typeswitch2.go": {category: generics, desc: "undiagnosed: incorrect output"}, "typeparam/typeswitch3.go": {category: generics, desc: "undiagnosed: incorrect output"}, From 77b52232caedcb2d11309a3bcadce1b53a48bcd5 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 21 Jan 2024 16:17:15 +0000 Subject: [PATCH 15/28] Re-triage remaining typeparam test failures. --- tests/gorepo/run.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index b72510863..95c4f5016 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -150,21 +150,21 @@ var knownFails = map[string]failReason{ "fixedbugs/issue48536.go": {category: usesUnsupportedPackage, desc: "https://github.com/gopherjs/gopherjs/issues/1130"}, "fixedbugs/issue53600.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, "typeparam/issue51733.go": {category: usesUnsupportedPackage, desc: "unsafe: uintptr to struct pointer conversion is unsupported"}, + "typeparam/chans.go": {category: neverTerminates, desc: "uses runtime.SetFinalizer() and runtime.GC()."}, // Failures related to the lack of generics support. Ideally, this section // should be emptied once https://github.com/gopherjs/gopherjs/issues/1013 is // fixed. - "typeparam/chans.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, - "typeparam/issue44688.go": {category: generics, desc: "bug: Type$1 is not defined"}, + "typeparam/issue44688.go": {category: generics, desc: "bug: C$1 is not defined"}, "typeparam/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, "typeparam/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, - "typeparam/issue48253.go": {category: generics, desc: "bug: Type$1 is not defined"}, + "typeparam/issue48253.go": {category: generics, desc: "bug: A$1 is not defined"}, "typeparam/issue49547.go": {category: generics, desc: "bug: incorrect type string"}, - "typeparam/issue50109b.go": {category: generics, desc: "bug: Type$1 is not defined"}, + "typeparam/issue50109b.go": {category: generics, desc: "bug: S2$1 is not defined"}, "typeparam/metrics.go": {category: generics, desc: "bug: append: Cannot read property '$array' of undefined"}, - "typeparam/nested.go": {category: generics, desc: "bug: Cannot set property 'int' of undefined"}, - "typeparam/orderedmap.go": {category: generics, desc: "bug: Cannot read property 'ptr' of undefined"}, - "typeparam/struct.go": {category: generics, desc: "bug: Type$1 is not defined"}, + "typeparam/nested.go": {category: generics, desc: "bug: Cannot set property 'main.Int' of undefined"}, + "typeparam/orderedmap.go": {category: generics, desc: "bug: Cannot set property 'key' of undefined"}, + "typeparam/struct.go": {category: generics, desc: "bug: E$1 is not defined"}, "typeparam/typeswitch2.go": {category: generics, desc: "undiagnosed: incorrect output"}, "typeparam/typeswitch3.go": {category: generics, desc: "undiagnosed: incorrect output"}, "typeparam/typeswitch5.go": {category: generics, desc: "undiagnosed: incorrect output"}, From ecc7d5fd9b671f74817629384f4125951575e000 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 21 Jan 2024 18:19:29 +0000 Subject: [PATCH 16/28] Fix instance discovery for generic types embedded in structs. Prior to this change the discovered instance would have referred to the *types.Var object representing the implicitly declared struct field. With this change, we extract the *types.TypeName that represents the instantiated type. --- compiler/internal/typeparams/collect.go | 12 ++++++++++++ tests/gorepo/run.go | 4 ---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index a6d7fc703..8ea8b1962 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -134,6 +134,18 @@ func (c *visitor) Visit(n ast.Node) (w ast.Visitor) { } obj := c.info.ObjectOf(ident) + + // For types embedded in structs, the object the identifier resolves to is a + // *types.Var representing the implicitly declared struct field. However, the + // instance relates to the *types.TypeName behind the field type, which we + // obtain here. + typ := obj.Type() + if ptr, ok := typ.(*types.Pointer); ok { + typ = ptr.Elem() + } + if t, ok := typ.(*types.Named); ok { + obj = t.Obj() + } c.instances.Add(Instance{ Object: obj, TArgs: c.resolver.SubstituteAll(instance.TypeArgs), diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 95c4f5016..36788f5fc 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -155,16 +155,12 @@ var knownFails = map[string]failReason{ // 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/issue44688.go": {category: generics, desc: "bug: C$1 is not defined"}, "typeparam/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, "typeparam/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, - "typeparam/issue48253.go": {category: generics, desc: "bug: A$1 is not defined"}, "typeparam/issue49547.go": {category: generics, desc: "bug: incorrect type string"}, - "typeparam/issue50109b.go": {category: generics, desc: "bug: S2$1 is not defined"}, "typeparam/metrics.go": {category: generics, desc: "bug: append: Cannot read property '$array' of undefined"}, "typeparam/nested.go": {category: generics, desc: "bug: Cannot set property 'main.Int' of undefined"}, "typeparam/orderedmap.go": {category: generics, desc: "bug: Cannot set property 'key' of undefined"}, - "typeparam/struct.go": {category: generics, desc: "bug: E$1 is not defined"}, "typeparam/typeswitch2.go": {category: generics, desc: "undiagnosed: incorrect output"}, "typeparam/typeswitch3.go": {category: generics, desc: "undiagnosed: incorrect output"}, "typeparam/typeswitch5.go": {category: generics, desc: "undiagnosed: incorrect output"}, From 7a384fd651514fbe02a0de38d812ed5bd48ce299 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 28 Jan 2024 17:39:46 +0000 Subject: [PATCH 17/28] Deduplicate collected typeNames within a package. Since we can now traverse the same AST subtree multiple types while processing different generic instances, type declaration statements may be visited more than once as well. The collected type list is then used to emit JS for type definitions, so duplicate *types.TypeName entries led to emitting duplicate code. Using set semantics prevents that. Note that we use ordered set to make sure code generation remains deterministic. --- compiler/package.go | 6 ++-- compiler/statements.go | 2 +- compiler/typesutil/typenames.go | 30 ++++++++++++++++++ compiler/typesutil/typenames_test.go | 46 ++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 compiler/typesutil/typenames.go create mode 100644 compiler/typesutil/typenames_test.go diff --git a/compiler/package.go b/compiler/package.go index 761b14e72..3cba5f623 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -28,7 +28,7 @@ type pkgContext struct { additionalSelections map[*ast.SelectorExpr]typesutil.Selection typesCtx *types.Context - typeNames []*types.TypeName + typeNames typesutil.TypeNames pkgVars map[string]string varPtrNames map[*types.Var]string anonTypes []*types.TypeName @@ -293,7 +293,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor case token.TYPE: for _, spec := range d.Specs { o := funcCtx.pkgCtx.Defs[spec.(*ast.TypeSpec).Name].(*types.TypeName) - funcCtx.pkgCtx.typeNames = append(funcCtx.pkgCtx.typeNames, o) + funcCtx.pkgCtx.typeNames.Add(o) funcCtx.objectName(o) // register toplevel name } case token.VAR: @@ -490,7 +490,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor // named types var typeDecls []*Decl - for _, o := range funcCtx.pkgCtx.typeNames { + for _, o := range funcCtx.pkgCtx.typeNames.Slice() { if o.IsAlias() { continue } diff --git a/compiler/statements.go b/compiler/statements.go index dd40582b4..e8068edb4 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -443,7 +443,7 @@ 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) - fc.pkgCtx.typeNames = append(fc.pkgCtx.typeNames, o) + fc.pkgCtx.typeNames.Add(o) fc.pkgCtx.dependencies[o] = true } case token.CONST: diff --git a/compiler/typesutil/typenames.go b/compiler/typesutil/typenames.go new file mode 100644 index 000000000..b02464f9f --- /dev/null +++ b/compiler/typesutil/typenames.go @@ -0,0 +1,30 @@ +package typesutil + +import "go/types" + +// TypeNames implements an ordered set of *types.TypeName pointers. +// +// The set is ordered to ensure deterministic behavior across compiler runs. +type TypeNames struct { + known map[*types.TypeName]struct{} + order []*types.TypeName +} + +// Add a type name to the test. If the type name has been previously added, +// this operation is a no-op. Two type names are considered equal iff they have +// the same memory address. +func (tn *TypeNames) Add(name *types.TypeName) { + if _, ok := tn.known[name]; ok { + return + } + if tn.known == nil { + tn.known = map[*types.TypeName]struct{}{} + } + tn.order = append(tn.order, name) + tn.known[name] = struct{}{} +} + +// Slice returns set elements in the order they were first added to the set. +func (tn *TypeNames) Slice() []*types.TypeName { + return tn.order +} diff --git a/compiler/typesutil/typenames_test.go b/compiler/typesutil/typenames_test.go new file mode 100644 index 000000000..332a5973f --- /dev/null +++ b/compiler/typesutil/typenames_test.go @@ -0,0 +1,46 @@ +package typesutil + +import ( + "go/token" + "go/types" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/gopherjs/gopherjs/internal/srctesting" +) + +func typeNameOpts() cmp.Options { + return cmp.Options{ + cmp.Transformer("TypeName", func(name *types.TypeName) string { + return types.ObjectString(name, nil) + }), + } +} + +func TestTypeNames(t *testing.T) { + src := `package test + + type A int + type B int + type C int + ` + fset := token.NewFileSet() + _, pkg := srctesting.Check(t, fset, srctesting.Parse(t, fset, src)) + A := srctesting.LookupObj(pkg, "A").(*types.TypeName) + B := srctesting.LookupObj(pkg, "B").(*types.TypeName) + C := srctesting.LookupObj(pkg, "C").(*types.TypeName) + + tn := TypeNames{} + tn.Add(A) + tn.Add(B) + tn.Add(A) + tn.Add(C) + tn.Add(B) + + got := tn.Slice() + want := []*types.TypeName{A, B, C} + + if diff := cmp.Diff(want, got, typeNameOpts()); diff != "" { + t.Errorf("tn.Slice() returned diff (-want,+got):\n%s", diff) + } +} From 13fa377f635fe1ed546b3e83453a7f4e42e72d04 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 3 Feb 2024 16:22:05 +0000 Subject: [PATCH 18/28] Use types.Object as a key when grouping instances by object. Previously used symbol.Name produces a collision when types with the same name declared in two different scopes, for example: ```go func f() { type E struct{} var e E _ = e } func g() { type E struct{} var e E _ = e } ``` In the example above, two declarations of `E` are actually two different objects, but their instances would have been mixed up. --- compiler/package.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/package.go b/compiler/package.go index 3cba5f623..9a7e87a77 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -212,9 +212,9 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor Instances: &typeparams.InstanceSet{}, } tc.Scan(simplifiedFiles...) - instancesByObj := map[symbol.Name][]typeparams.Instance{} + instancesByObj := map[types.Object][]typeparams.Instance{} for _, inst := range tc.Instances.Values() { - instancesByObj[symbol.New(inst.Object)] = append(instancesByObj[symbol.New(inst.Object)], inst) + instancesByObj[inst.Object] = append(instancesByObj[inst.Object], inst) } pkgInfo := analysis.AnalyzePkg(simplifiedFiles, fileSet, typesInfo, typesPkg, isBlocking) @@ -391,7 +391,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor var instances []typeparams.Instance if typeparams.SignatureTypeParams(sig) != nil { - instances = instancesByObj[symbol.New(o)] + instances = instancesByObj[o] } else { instances = []typeparams.Instance{{Object: o}} } @@ -497,7 +497,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor typ := o.Type().(*types.Named) var instances []typeparams.Instance if typ.TypeParams() != nil { - instances = instancesByObj[symbol.New(o)] + instances = instancesByObj[o] } else { instances = []typeparams.Instance{{Object: o}} } From fe2849a855f228d2e4519b96a0c1f9234870a50c Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 18 Feb 2024 17:36:45 +0000 Subject: [PATCH 19/28] Assign unique numeric IDs to generic instances in the generated code. This change fixes a class of generic instance collisions caused by type name shadowing by generating synthetic numeric keys for each type instance in the program instead of deriving a string-based key from type arguments. Each instance added to the InstanceSet is assigned a number which is guaranteed to be unique among all instances of the same object. This allows us to distinguish two different instances of `f` in the example below: ```go func f[T any]() {} func main() { type X int f[X]() { type X string // Shadows outer X. f[X]() } } ``` --- compiler/internal/typeparams/instance.go | 65 ++++++--- compiler/internal/typeparams/instance_test.go | 32 ++--- compiler/internal/typeparams/map.go | 135 ++++++++++++++++++ compiler/internal/typeparams/map_test.go | 111 ++++++++++++++ compiler/package.go | 2 + compiler/utils.go | 4 +- tests/gorepo/run.go | 2 +- 7 files changed, 314 insertions(+), 37 deletions(-) create mode 100644 compiler/internal/typeparams/map.go create mode 100644 compiler/internal/typeparams/map_test.go diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index 69b198d08..c5dc6892f 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -7,7 +7,6 @@ import ( "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/gopherjs/gopherjs/compiler/typesutil" - "golang.org/x/exp/maps" ) // Instance of a generic type or function. @@ -19,9 +18,11 @@ type Instance struct { TArgs []types.Type // Type params to instantiate with. } -// ID returns a string that uniquely identifies an instantiation of the generic -// object with the provided type arguments. -func (i *Instance) ID() string { +// 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 @@ -74,8 +75,9 @@ func (i *Instance) Recv() Instance { // Each Instance may be added to the set any number of times, but it will be // returned for processing exactly once. Processing order is not specified. type InstanceSet struct { - unprocessed []Instance - seen map[string]Instance + values []Instance + unprocessed int // Index in values for the next unprocessed element. + seen InstanceMap[int] // Maps instance to a unique numeric id. } // Add instances to the set. Instances that have been previously added to the @@ -83,19 +85,47 @@ type InstanceSet struct { // processed already. func (iset *InstanceSet) Add(instances ...Instance) *InstanceSet { for _, inst := range instances { - if _, ok := iset.seen[inst.ID()]; ok { + if iset.seen.Has(inst) { continue } - if iset.seen == nil { - iset.seen = map[string]Instance{} - } - iset.unprocessed = append(iset.unprocessed, inst) - - iset.seen[inst.ID()] = inst + iset.seen.Set(inst, iset.seen.Len()) + iset.values = append(iset.values, inst) } return iset } +// ID returns a unique numeric identifier assigned to an instance in the set. +// The ID is guaranteed to be unique among all instances of the same object +// within a given program. The ID will be consistent, as long as instances are +// added to the set in the same order. +// +// In order to have an ID assigned, the instance must have been previously added +// to the set. +// +// Note: we these ids are used in the generated code as keys to the specific +// type/function instantiation in the type/function object. Using this has two +// advantages: +// +// - More compact generated code compared to string keys derived from type args. +// +// - Collision avoidance in case of two different types having the same name due +// to shadowing. +// +// Here's an example where it's very difficult to assign non-colliding +// name-based keys to the two different types T: +// +// func foo() { +// type T int +// { type T string } // Code block creates a new nested scope allowing for shadowing. +// } +func (iset *InstanceSet) ID(inst Instance) int { + id, ok := iset.seen.get(inst) + if !ok { + panic(fmt.Errorf("requesting ID of instance %v that hasn't been added to the set", inst)) + } + return id +} + // next returns the next Instance to be processed. // // If there are no unprocessed instances, the second returned value will be false. @@ -103,16 +133,15 @@ func (iset *InstanceSet) next() (Instance, bool) { if iset.exhausted() { return Instance{}, false } - idx := len(iset.unprocessed) - 1 - next := iset.unprocessed[idx] - iset.unprocessed = iset.unprocessed[0:idx] + next := iset.values[iset.unprocessed] + iset.unprocessed++ return next, true } // exhausted returns true if there are no unprocessed instances in the set. -func (iset *InstanceSet) exhausted() bool { return len(iset.unprocessed) == 0 } +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 { - return maps.Values(iset.seen) + return iset.values } diff --git a/compiler/internal/typeparams/instance_test.go b/compiler/internal/typeparams/instance_test.go index 70ca7fd2a..e3ad51dec 100644 --- a/compiler/internal/typeparams/instance_test.go +++ b/compiler/internal/typeparams/instance_test.go @@ -14,17 +14,17 @@ import ( func instanceOpts() cmp.Options { return cmp.Options{ // Instances are represented by their IDs for diffing purposes. - cmp.Transformer("InstanceID", func(i Instance) string { - return i.ID() + cmp.Transformer("Instance", func(i Instance) string { + return i.String() }), // Order of instances in a slice doesn't matter, sort them by ID. cmpopts.SortSlices(func(a, b Instance) bool { - return a.ID() < b.ID() + return a.String() < b.String() }), } } -func TestInstanceKeyAndID(t *testing.T) { +func TestInstanceString(t *testing.T) { const src = `package testcase type Ints []int @@ -45,7 +45,7 @@ func TestInstanceKeyAndID(t *testing.T) { tests := []struct { descr string instance Instance - wantID string + wantStr string wantKey string }{{ descr: "exported type", @@ -53,7 +53,7 @@ func TestInstanceKeyAndID(t *testing.T) { Object: pkg.Scope().Lookup("Typ"), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - wantID: "test.Typ", + wantStr: "test.Typ", wantKey: "int, string", }, { descr: "exported method", @@ -61,7 +61,7 @@ func TestInstanceKeyAndID(t *testing.T) { Object: pkg.Scope().Lookup("Typ").Type().(*types.Named).Method(0), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - wantID: "test.Typ.Method", + wantStr: "test.Typ.Method", wantKey: "int, string", }, { descr: "exported function", @@ -69,7 +69,7 @@ func TestInstanceKeyAndID(t *testing.T) { Object: pkg.Scope().Lookup("Fun"), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - wantID: "test.Fun", + wantStr: "test.Fun", wantKey: "int, string", }, { descr: "unexported type", @@ -77,7 +77,7 @@ func TestInstanceKeyAndID(t *testing.T) { Object: pkg.Scope().Lookup("typ"), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - wantID: "test.typ", + wantStr: "test.typ", wantKey: "int, string", }, { descr: "unexported method", @@ -85,7 +85,7 @@ func TestInstanceKeyAndID(t *testing.T) { Object: pkg.Scope().Lookup("typ").Type().(*types.Named).Method(0), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - wantID: "test.typ.method", + wantStr: "test.typ.method", wantKey: "int, string", }, { descr: "unexported function", @@ -93,14 +93,14 @@ func TestInstanceKeyAndID(t *testing.T) { Object: pkg.Scope().Lookup("fun"), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - wantID: "test.fun", + wantStr: "test.fun", wantKey: "int, string", }, { descr: "no type params", instance: Instance{ Object: pkg.Scope().Lookup("Ints"), }, - wantID: "test.Ints", + wantStr: "test.Ints", wantKey: "", }, { descr: "complex parameter type", @@ -114,15 +114,15 @@ func TestInstanceKeyAndID(t *testing.T) { }, true)), }, }, - wantID: "test.fun<[]int, test.typ[int, string]>", + wantStr: "test.fun<[]int, test.typ[int, string]>", wantKey: "[]int, test.typ[int, string]", }} for _, test := range tests { t.Run(test.descr, func(t *testing.T) { - got := test.instance.ID() - if got != test.wantID { - t.Errorf("Got: instance ID %q. Want: %q.", got, test.wantID) + got := test.instance.String() + if got != test.wantStr { + t.Errorf("Got: instance string %q. Want: %q.", got, test.wantStr) } got = test.instance.Key() if got != test.wantKey { diff --git a/compiler/internal/typeparams/map.go b/compiler/internal/typeparams/map.go new file mode 100644 index 000000000..aa16130e2 --- /dev/null +++ b/compiler/internal/typeparams/map.go @@ -0,0 +1,135 @@ +package typeparams + +import ( + "go/types" + "sync" + + "golang.org/x/tools/go/types/typeutil" +) + +type ( + mapEntry[V any] struct { + key Instance + value V + } + mapBucket[V any] []*mapEntry[V] + mapBuckets[V any] map[uint32]mapBucket[V] +) + +// InstanceMap implements a map-like data structure keyed by instances. +// +// Zero value is an equivalent of an empty map. Methods are not thread-safe. +// +// Since Instance contains a slice and is not comparable, it can not be used as +// a regular map key, but we can compare its fields manually. When comparing +// 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. +type InstanceMap[V any] struct { + bootstrap sync.Once + data map[types.Object]mapBuckets[V] + len int + hasher typeutil.Hasher + zero V +} + +func (im *InstanceMap[V]) init() { + im.bootstrap.Do(func() { + im.data = map[types.Object]mapBuckets[V]{} + im.hasher = typeutil.MakeHasher() + }) +} + +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 + } + } + return im.zero, false +} + +// Get returns the stored value for the provided key. If the key is missing from +// the map, zero value is returned. +func (im *InstanceMap[V]) Get(key Instance) V { + val, _ := im.get(key) + return val +} + +// Has returns true if the given key is present in the map. +func (im *InstanceMap[V]) Has(key Instance) bool { + _, ok := im.get(key) + return ok +} + +// 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() + + if _, ok := im.data[key.Object]; !ok { + im.data[key.Object] = mapBuckets[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 + 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, + }) + im.len++ + return im.zero +} + +// Len returns the number of elements in the map. +func (im *InstanceMap[V]) Len() int { + return im.len +} + +// 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 { + var hash uint32 + for _, typ := range types { + hash ^= hasher.Hash(typ) + } + 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 new file mode 100644 index 000000000..5018ab0d8 --- /dev/null +++ b/compiler/internal/typeparams/map_test.go @@ -0,0 +1,111 @@ +package typeparams + +import ( + "go/token" + "go/types" + "testing" +) + +func TestInstanceMap(t *testing.T) { + i1 := Instance{ + Object: types.NewTypeName(token.NoPos, nil, "i1", nil), + TArgs: []types.Type{ + types.Typ[types.Int], + types.Typ[types.Int8], + }, + } + i1clone := Instance{ + Object: i1.Object, + TArgs: []types.Type{ + types.Typ[types.Int], + types.Typ[types.Int8], + }, + } + + i2 := Instance{ + Object: types.NewTypeName(token.NoPos, nil, "i2", nil), // Different pointer. + TArgs: []types.Type{ + types.Typ[types.Int], + types.Typ[types.Int8], + }, + } + i3 := Instance{ + Object: i1.Object, + TArgs: []types.Type{types.Typ[types.String]}, // Different type args. + } + + _ = i1 + _ = i1clone + _ = i3 + _ = i2 + + m := InstanceMap[string]{} + + // Check operations on a missing key. + t.Run("empty", func(t *testing.T) { + if got := m.Has(i1); got { + t.Errorf("Got: empty map contains %s. Want: empty map contains nothing.", i1) + } + if got := m.Get(i1); got != "" { + t.Errorf("Got: getting missing key returned %q. Want: zero value.", got) + } + if got := m.Len(); got != 0 { + t.Errorf("Got: empty map length %d. Want: 0.", got) + } + if got := m.Set(i1, "abc"); got != "" { + t.Errorf("Got: setting a new key returned old value %q. Want: zero value", got) + } + if got := m.Len(); got != 1 { + t.Errorf("Got: map length %d. Want: 1.", got) + } + }) + + // Check operations on the existing key. + t.Run("first key", func(t *testing.T) { + if got := m.Set(i1, "def"); got != "abc" { + t.Errorf(`Got: setting an existing key returned old value %q. Want: "abc".`, got) + } + if got := m.Len(); got != 1 { + t.Errorf("Got: map length %d. Want: 1.", got) + } + if got := m.Has(i1); !got { + t.Errorf("Got: set map key is reported as missing. Want: key present.") + } + if got := m.Get(i1); got != "def" { + t.Errorf(`Got: getting set key returned %q. Want: "def"`, got) + } + if got := m.Get(i1clone); got != "def" { + t.Errorf(`Got: getting set key returned %q. Want: "def"`, got) + } + }) + + // Check for key collisions. + 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) + } + if got := m.Set(i2, "123"); got != "" { + t.Errorf("Got: a new key %q overrode an old value %q. Want: zero value.", i2, got) + } + if got := m.Get(i2); got != "123" { + t.Errorf(`Got: getting set key %q returned: %q. Want: "123"`, i2, got) + } + if got := m.Len(); got != 2 { + t.Errorf("Got: map length %d. Want: 2.", got) + } + }) + 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) + } + if got := m.Set(i3, "456"); got != "" { + t.Errorf("Got: a new key %q overrode an old value %q. Want: zero value.", i3, got) + } + if got := m.Get(i3); got != "456" { + t.Errorf(`Got: getting set key %q returned: %q. Want: "456"`, i3, got) + } + if got := m.Len(); got != 3 { + t.Errorf("Got: map length %d. Want: 3.", got) + } + }) +} diff --git a/compiler/package.go b/compiler/package.go index 9a7e87a77..28721c337 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -39,6 +39,7 @@ type pkgContext struct { minify bool fileSet *token.FileSet errList ErrorList + instanceSet *typeparams.InstanceSet } // funcContext maintains compiler context for a specific function (lexical scope?). @@ -232,6 +233,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor dependencies: make(map[types.Object]bool), minify: minify, fileSet: fileSet, + instanceSet: tc.Instances, }, allVars: make(map[string]int), flowDatas: map[*types.Label]*flowData{nil: {}}, diff --git a/compiler/utils.go b/compiler/utils.go index 3c53c7c3d..3c5d76057 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -383,10 +383,10 @@ func (fc *funcContext) objectName(o types.Object) string { // zero type arguments. func (fc *funcContext) instName(inst typeparams.Instance) string { objName := fc.objectName(inst.Object) - if len(inst.TArgs) == 0 { + if inst.IsTrivial() { return objName } - return fmt.Sprintf("%s[%q]", objName, inst.Key()) + return fmt.Sprintf("%s[%d /* %v */]", objName, fc.pkgCtx.instanceSet.ID(inst), inst.Key()) } func (fc *funcContext) varPtrName(o *types.Var) string { diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 36788f5fc..f7fa25963 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -159,7 +159,7 @@ var knownFails = map[string]failReason{ "typeparam/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, "typeparam/issue49547.go": {category: generics, desc: "bug: incorrect type string"}, "typeparam/metrics.go": {category: generics, desc: "bug: append: Cannot read property '$array' of undefined"}, - "typeparam/nested.go": {category: generics, desc: "bug: Cannot set property 'main.Int' of undefined"}, + "typeparam/nested.go": {category: generics, desc: "incomplete support for generic types inside generic functions"}, "typeparam/orderedmap.go": {category: generics, desc: "bug: Cannot set property 'key' of undefined"}, "typeparam/typeswitch2.go": {category: generics, desc: "undiagnosed: incorrect output"}, "typeparam/typeswitch3.go": {category: generics, desc: "undiagnosed: incorrect output"}, From 49c773ad2913286573b46d418171bdbebe430a29 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 24 Feb 2024 14:22:24 +0000 Subject: [PATCH 20/28] Include type arguments into generic type strings. --- compiler/internal/typeparams/instance.go | 26 +++++---------- compiler/internal/typeparams/instance_test.go | 33 +++++++++---------- compiler/package.go | 2 +- compiler/typesutil/typelist.go | 20 +++++++++++ compiler/utils.go | 2 +- tests/gorepo/run.go | 2 -- 6 files changed, 46 insertions(+), 39 deletions(-) create mode 100644 compiler/typesutil/typelist.go diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index c5dc6892f..ea155801a 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -3,7 +3,6 @@ package typeparams import ( "fmt" "go/types" - "strings" "github.com/gopherjs/gopherjs/compiler/internal/symbol" "github.com/gopherjs/gopherjs/compiler/typesutil" @@ -14,8 +13,8 @@ import ( // Non-generic objects can be represented as an Instance with zero type params, // they are instances of themselves. type Instance struct { - Object types.Object // Object to be instantiated. - TArgs []types.Type // Type params to instantiate with. + Object types.Object // Object to be instantiated. + TArgs typesutil.TypeList // Type params to instantiate with. } // String returns a string representation of the Instance. @@ -28,23 +27,16 @@ func (i *Instance) String() string { return sym } - return fmt.Sprintf("%s<%s>", sym, i.Key()) + return fmt.Sprintf("%s<%s>", sym, i.TArgs) } -// Key returns a string that uniquely identifies this instance among other -// instances of this particular object. -// -// Although in practice it is derived from type arguments, no particular -// guarantees are made about format of content of the string. -func (i *Instance) Key() string { - buf := strings.Builder{} - for i, tArg := range i.TArgs { - if i != 0 { - buf.WriteString(", ") - } - buf.WriteString(types.TypeString(tArg, nil)) +// 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() + "]" } - return buf.String() + return fmt.Sprintf("%s.%s%s", i.Object.Pkg().Name(), i.Object.Name(), tArgs) } // IsTrivial returns true if this is an instance of a non-generic object. diff --git a/compiler/internal/typeparams/instance_test.go b/compiler/internal/typeparams/instance_test.go index e3ad51dec..d00b58beb 100644 --- a/compiler/internal/typeparams/instance_test.go +++ b/compiler/internal/typeparams/instance_test.go @@ -43,18 +43,18 @@ func TestInstanceString(t *testing.T) { mustType := testingx.Must[types.Type](t) tests := []struct { - descr string - instance Instance - wantStr string - wantKey string + descr string + instance Instance + wantStr string + wantTypeString string }{{ descr: "exported type", instance: Instance{ Object: pkg.Scope().Lookup("Typ"), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - wantStr: "test.Typ", - wantKey: "int, string", + wantStr: "test.Typ", + wantTypeString: "testcase.Typ[int, string]", }, { descr: "exported method", instance: Instance{ @@ -62,7 +62,6 @@ func TestInstanceString(t *testing.T) { TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, wantStr: "test.Typ.Method", - wantKey: "int, string", }, { descr: "exported function", instance: Instance{ @@ -70,15 +69,14 @@ func TestInstanceString(t *testing.T) { TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, wantStr: "test.Fun", - wantKey: "int, string", }, { descr: "unexported type", instance: Instance{ Object: pkg.Scope().Lookup("typ"), TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, - wantStr: "test.typ", - wantKey: "int, string", + wantStr: "test.typ", + wantTypeString: "testcase.typ[int, string]", }, { descr: "unexported method", instance: Instance{ @@ -86,7 +84,6 @@ func TestInstanceString(t *testing.T) { TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, wantStr: "test.typ.method", - wantKey: "int, string", }, { descr: "unexported function", instance: Instance{ @@ -94,14 +91,13 @@ func TestInstanceString(t *testing.T) { TArgs: []types.Type{types.Typ[types.Int], types.Typ[types.String]}, }, wantStr: "test.fun", - wantKey: "int, string", }, { descr: "no type params", instance: Instance{ Object: pkg.Scope().Lookup("Ints"), }, - wantStr: "test.Ints", - wantKey: "", + wantStr: "test.Ints", + wantTypeString: "testcase.Ints", }, { descr: "complex parameter type", instance: Instance{ @@ -115,7 +111,6 @@ func TestInstanceString(t *testing.T) { }, }, wantStr: "test.fun<[]int, test.typ[int, string]>", - wantKey: "[]int, test.typ[int, string]", }} for _, test := range tests { @@ -124,9 +119,11 @@ func TestInstanceString(t *testing.T) { if got != test.wantStr { t.Errorf("Got: instance string %q. Want: %q.", got, test.wantStr) } - got = test.instance.Key() - if got != test.wantKey { - t.Errorf("Got: instance key %q. Want: %q.", got, test.wantKey) + if test.wantTypeString != "" { + got = test.instance.TypeString() + if got != test.wantTypeString { + t.Errorf("Got: instance type string %q. Want: %q.", got, test.wantTypeString) + } } }) } diff --git a/compiler/package.go b/compiler/package.go index 28721c337..e46f16016 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -563,7 +563,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor constructor = "$arrayPtrCtor()" } } - funcCtx.Printf(`%s = $newType(%d, %s, "%s.%s", %t, "%s", %t, %s);`, funcCtx.instName(inst), size, typeKind(typ), o.Pkg().Name(), o.Name(), o.Name() != "", o.Pkg().Path(), o.Exported(), constructor) + funcCtx.Printf(`%s = $newType(%d, %s, %q, %t, "%s", %t, %s);`, funcCtx.instName(inst), size, typeKind(typ), inst.TypeString(), o.Name() != "", o.Pkg().Path(), o.Exported(), constructor) }) d.MethodListCode = funcCtx.CatchOutput(0, func() { if _, ok := underlying.(*types.Interface); ok { diff --git a/compiler/typesutil/typelist.go b/compiler/typesutil/typelist.go new file mode 100644 index 000000000..04d0d6869 --- /dev/null +++ b/compiler/typesutil/typelist.go @@ -0,0 +1,20 @@ +package typesutil + +import ( + "go/types" + "strings" +) + +// TypeList an ordered list of types. +type TypeList []types.Type + +func (tl TypeList) String() string { + buf := strings.Builder{} + for i, typ := range tl { + if i != 0 { + buf.WriteString(", ") + } + buf.WriteString(types.TypeString(typ, nil)) + } + return buf.String() +} diff --git a/compiler/utils.go b/compiler/utils.go index 3c5d76057..bd00ffe30 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -386,7 +386,7 @@ func (fc *funcContext) instName(inst typeparams.Instance) string { if inst.IsTrivial() { return objName } - return fmt.Sprintf("%s[%d /* %v */]", objName, fc.pkgCtx.instanceSet.ID(inst), inst.Key()) + return fmt.Sprintf("%s[%d /* %v */]", objName, fc.pkgCtx.instanceSet.ID(inst), inst.TArgs) } func (fc *funcContext) varPtrName(o *types.Var) string { diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index f7fa25963..3f26adb8b 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -155,9 +155,7 @@ var knownFails = map[string]failReason{ // 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/issue46591.go": {category: generics, desc: "undiagnosed: len() returns an invalid value when parameterized types are involved"}, "typeparam/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, - "typeparam/issue49547.go": {category: generics, desc: "bug: incorrect type string"}, "typeparam/metrics.go": {category: generics, desc: "bug: append: Cannot read property '$array' of undefined"}, "typeparam/nested.go": {category: generics, desc: "incomplete support for generic types inside generic functions"}, "typeparam/orderedmap.go": {category: generics, desc: "bug: Cannot set property 'key' of undefined"}, From 5a7c2d0fb9b88b9c7f2a1e71622dfc8d43a74ac8 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 24 Feb 2024 17:35:39 +0000 Subject: [PATCH 21/28] Fix variable name assignment for result variables. Prior to this change, when a generic function had named result variables, we had an identity mismatch when accessing result variables through the instantiated function signature vs through the function body AST. The latter references the types.Var instance defined in terms of type parameters, whereas the former produced synthetic instantiated versions of the same types.Var objects. As a result, two different names were assigned to what should be the same object, depending on how it was reached. With this change, we preserve the original, uninstantiated signature, which result variables match the ones referenced in the body AST. Whenever we need access to the instantiated signature types we perform type substitution ad-hoc. --- compiler/package.go | 15 ++++++++++----- compiler/statements.go | 2 +- tests/gorepo/run.go | 2 -- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/compiler/package.go b/compiler/package.go index e46f16016..fb71491d1 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -45,8 +45,12 @@ type pkgContext struct { // funcContext maintains compiler context for a specific function (lexical scope?). type funcContext struct { *analysis.FuncInfo - pkgCtx *pkgContext - parent *funcContext + pkgCtx *pkgContext + parent *funcContext + // Signature of the function this context corresponds to or nil. For generic + // functions it is the original generic signature to make sure result variable + // identity in the signature matches the variable objects referenced in the + // function body. sig *types.Signature allVars map[string]int localVars []string @@ -774,6 +778,7 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, labelCases: make(map[*types.Label]int), typeResolver: outerContext.typeResolver, objectNames: map[types.Object]string{}, + sig: sig, } for k, v := range outerContext.allVars { c.allVars[k] = v @@ -788,7 +793,6 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, if c.objectNames == nil { c.objectNames = map[types.Object]string{} } - c.sig = c.typeResolver.Substitute(sig).(*types.Signature) var params []string for _, param := range typ.Params.List { @@ -815,10 +819,11 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, c.resultNames = make([]ast.Expr, c.sig.Results().Len()) for i := 0; i < c.sig.Results().Len(); i++ { result := c.sig.Results().At(i) - c.Printf("%s = %s;", c.objectName(result), c.translateExpr(c.zeroValue(result.Type())).String()) + typ := c.typeResolver.Substitute(result.Type()) + c.Printf("%s = %s;", c.objectName(result), c.translateExpr(c.zeroValue(typ)).String()) id := ast.NewIdent("") c.pkgCtx.Uses[id] = result - c.resultNames[i] = c.setType(id, result.Type()) + c.resultNames[i] = c.setType(id, typ) } } diff --git a/compiler/statements.go b/compiler/statements.go index e8068edb4..ed5352a9c 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -772,7 +772,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { } func (fc *funcContext) translateResults(results []ast.Expr) string { - tuple := fc.sig.Results() + tuple := fc.typeResolver.Substitute(fc.sig).(*types.Signature).Results() switch tuple.Len() { case 0: return "" diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 3f26adb8b..9e158372c 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -156,9 +156,7 @@ var knownFails = map[string]failReason{ // should be emptied once https://github.com/gopherjs/gopherjs/issues/1013 is // fixed. "typeparam/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, - "typeparam/metrics.go": {category: generics, desc: "bug: append: Cannot read property '$array' of undefined"}, "typeparam/nested.go": {category: generics, desc: "incomplete support for generic types inside generic functions"}, - "typeparam/orderedmap.go": {category: generics, desc: "bug: Cannot set property 'key' of undefined"}, "typeparam/typeswitch2.go": {category: generics, desc: "undiagnosed: incorrect output"}, "typeparam/typeswitch3.go": {category: generics, desc: "undiagnosed: incorrect output"}, "typeparam/typeswitch5.go": {category: generics, desc: "undiagnosed: incorrect output"}, From 61de589eca56ab29b27c02f5567464924f6d3e31 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 24 Feb 2024 18:32:32 +0000 Subject: [PATCH 22/28] Perform type substitution in the type switch expression. Prior to this change, we did not substitute type parameters with instantiated counterparts, which led to the incorrect treatment of the type param types as interfaces and invalid type assertion results. This change corrects the mistake. Although typeswitch2.go and typeswitch5.go generate a different output from upstream Go, it is semantically equivalent and the difference is down to console.log() behaving slightly differently than println() in native Go. --- compiler/statements.go | 9 +++++---- tests/gorepo/run.go | 21 ++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/compiler/statements.go b/compiler/statements.go index ed5352a9c..74ba3d2cc 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -146,16 +146,17 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { clause := cc.(*ast.CaseClause) var bodyPrefix []ast.Stmt if implicit := fc.pkgCtx.Implicits[clause]; implicit != nil { + typ := fc.typeResolver.Substitute(implicit.Type()) value := refVar - if typesutil.IsJsObject(implicit.Type().Underlying()) { + if typesutil.IsJsObject(typ.Underlying()) { value += ".$val.object" - } else if _, ok := implicit.Type().Underlying().(*types.Interface); !ok { + } else if _, ok := typ.Underlying().(*types.Interface); !ok { value += ".$val" } bodyPrefix = []ast.Stmt{&ast.AssignStmt{ - Lhs: []ast.Expr{fc.newIdent(fc.objectName(implicit), implicit.Type())}, + Lhs: []ast.Expr{fc.newIdent(fc.objectName(implicit), typ)}, Tok: token.DEFINE, - Rhs: []ast.Expr{fc.newIdent(value, implicit.Type())}, + Rhs: []ast.Expr{fc.newIdent(value, typ)}, }} } c := &ast.CaseClause{ diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 9e158372c..7c1725ac9 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -145,21 +145,20 @@ var knownFails = map[string]failReason{ "fixedbugs/issue50854.go": {category: lowLevelRuntimeDifference, desc: "negative int32 overflow behaves differently in JS"}, // These are new tests in Go 1.18 - "fixedbugs/issue47928.go": {category: notApplicable, desc: "//go:nointerface is a part of GOEXPERIMENT=fieldtrack and is not supported by GopherJS"}, - "fixedbugs/issue48898.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1128"}, - "fixedbugs/issue48536.go": {category: usesUnsupportedPackage, desc: "https://github.com/gopherjs/gopherjs/issues/1130"}, - "fixedbugs/issue53600.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, - "typeparam/issue51733.go": {category: usesUnsupportedPackage, desc: "unsafe: uintptr to struct pointer conversion is unsupported"}, - "typeparam/chans.go": {category: neverTerminates, desc: "uses runtime.SetFinalizer() and runtime.GC()."}, + "fixedbugs/issue47928.go": {category: notApplicable, desc: "//go:nointerface is a part of GOEXPERIMENT=fieldtrack and is not supported by GopherJS"}, + "fixedbugs/issue48898.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1128"}, + "fixedbugs/issue48536.go": {category: usesUnsupportedPackage, desc: "https://github.com/gopherjs/gopherjs/issues/1130"}, + "fixedbugs/issue53600.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, + "typeparam/issue51733.go": {category: usesUnsupportedPackage, desc: "unsafe: uintptr to struct pointer conversion is unsupported"}, + "typeparam/chans.go": {category: neverTerminates, desc: "uses runtime.SetFinalizer() and runtime.GC()."}, + "typeparam/typeswitch2.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, + "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/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, - "typeparam/nested.go": {category: generics, desc: "incomplete support for generic types inside generic functions"}, - "typeparam/typeswitch2.go": {category: generics, desc: "undiagnosed: incorrect output"}, - "typeparam/typeswitch3.go": {category: generics, desc: "undiagnosed: incorrect output"}, - "typeparam/typeswitch5.go": {category: generics, desc: "undiagnosed: incorrect output"}, + "typeparam/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, + "typeparam/nested.go": {category: generics, desc: "incomplete support for generic types inside generic functions"}, } type failCategory uint8 From 0a51510745ad4882ebd4677ff5c9a18e9ea57660 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 24 Feb 2024 20:27:39 +0000 Subject: [PATCH 23/28] Implement builtins Sizeof(), Alignof() and Offsetof(). Despite being defined in the unsafe package, these functions actually are implemented as builtins. For non-generic types go/types automatically computes constant values for corresponding AST nodes, but without type substitution it can't do the same for generic code. To make it work, we explicitly implement these builtins in our code, where type substitution takes effect, using go/types.Sizes to provide actual byte values. --- compiler/expressions.go | 10 ++++++++ compiler/internal/typeparams/collect.go | 10 ++++---- compiler/typesutil/typesutil.go | 32 ++++++++++++++++++++++++- tests/gorepo/run.go | 3 +-- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/compiler/expressions.go b/compiler/expressions.go index a2c4afacc..94b28df9a 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -627,6 +627,9 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if !ok { // qualified identifier obj := fc.pkgCtx.Uses[f.Sel] + if o, ok := obj.(*types.Builtin); ok { + return fc.translateBuiltin(o.Name(), sig, e.Args, e.Ellipsis.IsValid()) + } if typesutil.IsJsPackage(obj.Pkg()) { switch obj.Name() { case "Debugger": @@ -1049,6 +1052,13 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args return fc.formatExpr("$recover()") case "close": return fc.formatExpr(`$close(%e)`, args[0]) + case "Sizeof": + return fc.formatExpr("%d", sizes32.Sizeof(fc.typeOf(args[0]))) + case "Alignof": + return fc.formatExpr("%d", sizes32.Alignof(fc.typeOf(args[0]))) + case "Offsetof": + sel, _ := fc.selectionOf(astutil.RemoveParens(args[0]).(*ast.SelectorExpr)) + return fc.formatExpr("%d", typesutil.OffsetOf(sizes32, sel)) default: panic(fmt.Sprintf("Unhandled builtin: %s\n", name)) } diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index 8ea8b1962..b8962a616 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -73,9 +73,7 @@ func (r *Resolver) SubstituteSelection(sel typesutil.Selection) typesutil.Select } switch sel.Kind() { - case types.FieldVal: - return sel - case types.MethodExpr, types.MethodVal: + case types.MethodExpr, types.MethodVal, types.FieldVal: recv := r.Substitute(sel.Recv()) if recv == sel.Recv() { return sel // Non-generic receiver, no substitution necessary. @@ -84,12 +82,12 @@ func (r *Resolver) SubstituteSelection(sel typesutil.Selection) typesutil.Select // Look up the method on the instantiated receiver. pkg := sel.Obj().Pkg() obj, index, _ := types.LookupFieldOrMethod(recv, true, pkg, sel.Obj().Name()) - sig := obj.Type().(*types.Signature) + typ := obj.Type() if sel.Kind() == types.MethodExpr { - sig = typesutil.RecvAsFirstArg(sig) + typ = typesutil.RecvAsFirstArg(typ.(*types.Signature)) } - concrete := typesutil.NewSelection(sel.Kind(), recv, index, obj, sig) + concrete := typesutil.NewSelection(sel.Kind(), recv, index, obj, typ) r.selMemo[sel] = concrete return concrete default: diff --git a/compiler/typesutil/typesutil.go b/compiler/typesutil/typesutil.go index 91b174417..1434c23c5 100644 --- a/compiler/typesutil/typesutil.go +++ b/compiler/typesutil/typesutil.go @@ -1,6 +1,9 @@ package typesutil -import "go/types" +import ( + "fmt" + "go/types" +) func IsJsPackage(pkg *types.Package) bool { return pkg != nil && pkg.Path() == "github.com/gopherjs/gopherjs/js" @@ -77,3 +80,30 @@ func (sel *selectionImpl) Recv() types.Type { return sel.recv } func (sel *selectionImpl) Index() []int { return sel.index } func (sel *selectionImpl) Obj() types.Object { return sel.obj } func (sel *selectionImpl) Type() types.Type { return sel.typ } + +func fieldsOf(s *types.Struct) []*types.Var { + fields := make([]*types.Var, s.NumFields()) + for i := 0; i < s.NumFields(); i++ { + fields[i] = s.Field(i) + } + return fields +} + +// OffsetOf returns byte offset of a struct field specified by the provided +// selection. +// +// Adapted from go/types.Config.offsetof(). +func OffsetOf(sizes types.Sizes, sel Selection) int64 { + if sel.Kind() != types.FieldVal { + panic(fmt.Errorf("byte offsets are only defined for struct fields")) + } + typ := sel.Recv() + var o int64 + for _, idx := range sel.Index() { + s := typ.Underlying().(*types.Struct) + o += sizes.Offsetsof(fieldsOf(s))[idx] + typ = s.Field(idx).Type() + } + + return o +} diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 7c1725ac9..3e8744e9d 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -157,8 +157,7 @@ var knownFails = map[string]failReason{ // 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/issue47716.go": {category: generics, desc: "bug: failed to call unsafe.Sizeof"}, - "typeparam/nested.go": {category: generics, desc: "incomplete support for generic types inside generic functions"}, + "typeparam/nested.go": {category: generics, desc: "incomplete support for generic types inside generic functions"}, } type failCategory uint8 From 0343336f9565d58cbe69b106b42098752dcdf088 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 24 Feb 2024 22:47:04 +0000 Subject: [PATCH 24/28] Borrow type substitution logic from x/tools/go/ssa package. Previously we've been using similar functionality from go/types package. Since it was unexported, we've used a //go:linkname directive to gain access to it. However, upgrade to go1.19 changed arguments of the function in ways that are difficult to replicate, so I switched to an equivalent reimplementation from the go/ssa package. This version is implemented in terms of public go/types interface, so it's easy to vendor and update. To simplify code updates in future I kept all original code intact and added a separate source file that exports the necessary functions for gopherjs use. --- compiler/internal/typeparams/collect.go | 37 +- compiler/internal/typeparams/subst.go | 13 - compiler/internal/typeparams/subst/export.go | 49 ++ compiler/internal/typeparams/subst/subst.go | 475 ++++++++++++++++++ .../internal/typeparams/subst/subst_test.go | 103 ++++ compiler/internal/typeparams/subst/util.go | 23 + compiler/statements.go | 2 +- compiler/utils.go | 10 +- 8 files changed, 674 insertions(+), 38 deletions(-) delete mode 100644 compiler/internal/typeparams/subst.go create mode 100644 compiler/internal/typeparams/subst/export.go create mode 100644 compiler/internal/typeparams/subst/subst.go create mode 100644 compiler/internal/typeparams/subst/subst_test.go create mode 100644 compiler/internal/typeparams/subst/util.go diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index b8962a616..b9df1492b 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -3,9 +3,9 @@ package typeparams import ( "fmt" "go/ast" - "go/token" "go/types" + "github.com/gopherjs/gopherjs/compiler/internal/typeparams/subst" "github.com/gopherjs/gopherjs/compiler/typesutil" "golang.org/x/exp/typeparams" ) @@ -13,10 +13,7 @@ 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 { - TContext *types.Context - Map map[*types.TypeParam]types.Type - - memo typesutil.Map[types.Type] + subster *subst.Subster selMemo map[typesutil.Selection]typesutil.Selection } @@ -24,31 +21,22 @@ type Resolver struct { // entries with the same index. func NewResolver(tc *types.Context, tParams []*types.TypeParam, tArgs []types.Type) *Resolver { r := &Resolver{ - TContext: tc, - Map: map[*types.TypeParam]types.Type{}, - selMemo: map[typesutil.Selection]typesutil.Selection{}, + subster: subst.New(tc, tParams, tArgs), + selMemo: map[typesutil.Selection]typesutil.Selection{}, } if len(tParams) != len(tArgs) { panic(fmt.Errorf("len(tParams)=%d not equal len(tArgs)=%d", len(tParams), len(tArgs))) } - for i := range tParams { - r.Map[tParams[i]] = tArgs[i] - } return r } // 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.Map == nil || typ == nil { + if r == nil || r.subster == nil || typ == nil { return typ // No substitutions to be made. } - if concrete := r.memo.At(typ); concrete != nil { - return concrete - } - concrete := goTypesCheckerSubst((*types.Checker)(nil), token.NoPos, typ, substMap(r.Map), r.TContext) - r.memo.Set(typ, concrete) - return concrete + return r.subster.Type(typ) } // SubstituteAll same as Substitute, but accepts a TypeList are returns @@ -65,7 +53,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.Map == nil || sel == nil { + if r == nil || r.subster == nil || sel == nil { return sel // No substitutions to be made. } if concrete, ok := r.selMemo[sel]; ok { @@ -75,13 +63,16 @@ func (r *Resolver) SubstituteSelection(sel typesutil.Selection) typesutil.Select switch sel.Kind() { case types.MethodExpr, types.MethodVal, types.FieldVal: recv := r.Substitute(sel.Recv()) - if recv == 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 { @@ -165,9 +156,9 @@ func (c *visitor) Visit(n ast.Node) (w ast.Visitor) { // kickstart generic instantiation discovery. // // It serves double duty: -// - Builds a map from types.Object instances representing generic types, -// methods and functions to AST nodes that define them. -// - Collects an initial set of generic instantiations in the non-generic code. +// - Builds a map from types.Object instances representing generic types, +// methods and functions to AST nodes that define them. +// - Collects an initial set of generic instantiations in the non-generic code. type seedVisitor struct { visitor objMap map[types.Object]ast.Node diff --git a/compiler/internal/typeparams/subst.go b/compiler/internal/typeparams/subst.go deleted file mode 100644 index ad1be7c48..000000000 --- a/compiler/internal/typeparams/subst.go +++ /dev/null @@ -1,13 +0,0 @@ -package typeparams - -import ( - "go/token" - "go/types" - - _ "unsafe" // For go:linkname. -) - -type substMap map[*types.TypeParam]types.Type - -//go:linkname goTypesCheckerSubst go/types.(*Checker).subst -func goTypesCheckerSubst(check *types.Checker, pos token.Pos, typ types.Type, smap substMap, ctxt *types.Context) types.Type diff --git a/compiler/internal/typeparams/subst/export.go b/compiler/internal/typeparams/subst/export.go new file mode 100644 index 000000000..38e394bda --- /dev/null +++ b/compiler/internal/typeparams/subst/export.go @@ -0,0 +1,49 @@ +// Package subst is an excerpt from x/tools/go/ssa responsible for performing +// type substitution in types defined in terms of type parameters with provided +// type arguments. +package subst + +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 +// original code for us to use. + +// Subster performs type parameter substitution. +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.TypeParam, tArgs []types.Type) *Subster { + assert(len(tParams) == len(tArgs), "New() argument count must match") + + if len(tParams) == 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, + } +} + +// 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) +} diff --git a/compiler/internal/typeparams/subst/subst.go b/compiler/internal/typeparams/subst/subst.go new file mode 100644 index 000000000..9020e94f9 --- /dev/null +++ b/compiler/internal/typeparams/subst/subst.go @@ -0,0 +1,475 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package subst + +import ( + "go/types" +) + +// Type substituter for a fixed set of replacement types. +// +// A nil *subster is an 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. +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 + // 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 { + assert(tparams.Len() == len(targs), "makeSubster argument count must match") + + subst := &subster{ + replacements: make(map[*types.TypeParam]types.Type, tparams.Len()), + cache: make(map[types.Type]types.Type), + 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) { + if subst == nil { + return t // A nil subst is type preserving. + } + if r, ok := subst.cache[t]; ok { + return r + } + defer func() { + 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: + r := subst.replacements[t] + assert(r != nil, "type param without replacement encountered") + return r + + case *types.Basic: + return t + + case *types.Array: + if r := subst.typ(t.Elem()); r != t.Elem() { + return types.NewArray(r, t.Len()) + } + return t + + case *types.Slice: + if r := subst.typ(t.Elem()); r != t.Elem() { + return types.NewSlice(r) + } + return t + + case *types.Pointer: + if r := subst.typ(t.Elem()); r != t.Elem() { + return types.NewPointer(r) + } + return t + + case *types.Tuple: + return subst.tuple(t) + + case *types.Struct: + return subst.struct_(t) + + case *types.Map: + key := subst.typ(t.Key()) + elem := subst.typ(t.Elem()) + if key != t.Key() || elem != t.Elem() { + return types.NewMap(key, elem) + } + return t + + case *types.Chan: + if elem := subst.typ(t.Elem()); elem != t.Elem() { + return types.NewChan(t.Dir(), elem) + } + return t + + case *types.Signature: + return subst.signature(t) + + case *types.Union: + return subst.union(t) + + case *types.Interface: + return subst.interface_(t) + + case *types.Named: + return subst.named(t) + + default: + panic("unreachable") + } +} + +// types returns the result of {subst.typ(ts[i])}. +func (subst *subster) types(ts []types.Type) []types.Type { + res := make([]types.Type, len(ts)) + for i := range ts { + res[i] = subst.typ(ts[i]) + } + return res +} + +func (subst *subster) tuple(t *types.Tuple) *types.Tuple { + if t != nil { + if vars := subst.varlist(t); vars != nil { + return types.NewTuple(vars...) + } + } + return t +} + +type varlist interface { + At(i int) *types.Var + Len() int +} + +// fieldlist is an adapter for structs for the varlist interface. +type fieldlist struct { + str *types.Struct +} + +func (fl fieldlist) At(i int) *types.Var { return fl.str.Field(i) } +func (fl fieldlist) Len() int { return fl.str.NumFields() } + +func (subst *subster) struct_(t *types.Struct) *types.Struct { + if t != nil { + if fields := subst.varlist(fieldlist{t}); fields != nil { + tags := make([]string, t.NumFields()) + for i, n := 0, t.NumFields(); i < n; i++ { + tags[i] = t.Tag(i) + } + return types.NewStruct(fields, tags) + } + } + return t +} + +// varlist reutrns 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++ { + v := in.At(i) + w := subst.var_(v) + if v != w && out == nil { + out = make([]*types.Var, n) + for j := 0; j < i; j++ { + out[j] = in.At(j) + } + } + if out != nil { + out[i] = w + } + } + return out +} + +func (subst *subster) var_(v *types.Var) *types.Var { + if v != nil { + if typ := subst.typ(v.Type()); typ != v.Type() { + 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 v +} + +func (subst *subster) union(u *types.Union) *types.Union { + var out []*types.Term // nil => no updates + + for i, n := 0, u.Len(); i < n; i++ { + t := u.Term(i) + r := subst.typ(t.Type()) + if r != t.Type() && out == nil { + out = make([]*types.Term, n) + for j := 0; j < i; j++ { + out[j] = u.Term(j) + } + } + if out != nil { + out[i] = types.NewTerm(t.Tilde(), r) + } + } + + if out != nil { + return types.NewUnion(out) + } + return u +} + +func (subst *subster) interface_(iface *types.Interface) *types.Interface { + if iface == nil { + return nil + } + + // methods for the interface. Initially nil if there is no known change needed. + // Signatures for the method where recv is nil. NewInterfaceType fills in the receivers. + var methods []*types.Func + initMethods := func(n int) { // copy first n explicit methods + methods = make([]*types.Func, iface.NumExplicitMethods()) + for i := 0; i < n; i++ { + f := iface.ExplicitMethod(i) + norecv := changeRecv(f.Type().(*types.Signature), nil) + methods[i] = types.NewFunc(f.Pos(), f.Pkg(), f.Name(), norecv) + } + } + for i := 0; i < iface.NumExplicitMethods(); i++ { + f := iface.ExplicitMethod(i) + // On interfaces, we need to cycle break on anonymous interface types + // being in a cycle with their signatures being in cycles with their receivers + // that do not go through a Named. + norecv := changeRecv(f.Type().(*types.Signature), nil) + sig := subst.typ(norecv) + if sig != norecv && methods == nil { + initMethods(i) + } + if methods != nil { + methods[i] = types.NewFunc(f.Pos(), f.Pkg(), f.Name(), sig.(*types.Signature)) + } + } + + var embeds []types.Type + initEmbeds := func(n int) { // copy first n embedded types + embeds = make([]types.Type, iface.NumEmbeddeds()) + for i := 0; i < n; i++ { + embeds[i] = iface.EmbeddedType(i) + } + } + for i := 0; i < iface.NumEmbeddeds(); i++ { + e := iface.EmbeddedType(i) + r := subst.typ(e) + if e != r && embeds == nil { + initEmbeds(i) + } + if embeds != nil { + embeds[i] = r + } + } + + if methods == nil && embeds == nil { + return iface + } + if methods == nil { + initMethods(iface.NumExplicitMethods()) + } + if embeds == nil { + initEmbeds(iface.NumEmbeddeds()) + } + return types.NewInterfaceType(methods, embeds).Complete() +} + +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 + } + + // 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. + // + // 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). + // + // Example: + // func foo[T any]() { + // type linkedlist struct { + // next *linkedlist + // val T + // } + // } + // + // 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 + } + 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 + } + r, err := types.Instantiate(subst.ctxt, t.Origin(), insts, false) + assert(err == nil, "failed to Instantiate Named type") + return r +} + +func (subst *subster) signature(t *types.Signature) types.Type { + tparams := t.TypeParams() + + // We are choosing not to support tparams.Len() > 0 until a need has been observed in practice. + // + // There are some known usages for types.Types coming from types.{Eval,CheckExpr}. + // To support tparams.Len() > 0, we just need to do the following [psuedocode]: + // targs := {subst.replacements[tparams[i]]]}; Instantiate(ctxt, t, targs, false) + + assert(tparams.Len() == 0, "Substituting types.Signatures with generic functions are currently unsupported.") + + // Either: + // (1)non-generic function. + // no type params to substitute + // (2)generic method and recv needs to be substituted. + + // Receivers can be either: + // named + // pointer to named + // interface + // nil + // interface is the problematic case. We need to cycle break there! + recv := subst.var_(t.Recv()) + params := subst.tuple(t.Params()) + results := subst.tuple(t.Results()) + if recv != t.Recv() || params != t.Params() || results != t.Results() { + return types.NewSignatureType(recv, nil, nil, params, results, t.Variadic()) + } + return t +} + +// reaches returns true if a type t reaches any type t' s.t. c[t'] == true. +// It updates c to cache results. +// +// reaches is currently only part of the wellFormed debug logic, and +// in practice c is initially only type parameters. It is not currently +// relied on in production. +func reaches(t types.Type, c map[types.Type]bool) (res bool) { + if c, ok := c[t]; ok { + return c + } + + // c is populated with temporary false entries as types are visited. + // This avoids repeat visits and break cycles. + c[t] = false + defer func() { + c[t] = res + }() + + switch t := t.(type) { + case *types.TypeParam, *types.Basic: + return false + case *types.Array: + return reaches(t.Elem(), c) + case *types.Slice: + return reaches(t.Elem(), c) + case *types.Pointer: + return reaches(t.Elem(), c) + case *types.Tuple: + for i := 0; i < t.Len(); i++ { + if reaches(t.At(i).Type(), c) { + return true + } + } + case *types.Struct: + for i := 0; i < t.NumFields(); i++ { + if reaches(t.Field(i).Type(), c) { + return true + } + } + case *types.Map: + return reaches(t.Key(), c) || reaches(t.Elem(), c) + case *types.Chan: + return reaches(t.Elem(), c) + case *types.Signature: + if t.Recv() != nil && reaches(t.Recv().Type(), c) { + return true + } + return reaches(t.Params(), c) || reaches(t.Results(), c) + case *types.Union: + for i := 0; i < t.Len(); i++ { + if reaches(t.Term(i).Type(), c) { + return true + } + } + case *types.Interface: + for i := 0; i < t.NumEmbeddeds(); i++ { + if reaches(t.Embedded(i), c) { + return true + } + } + for i := 0; i < t.NumExplicitMethods(); i++ { + if reaches(t.ExplicitMethod(i).Type(), c) { + return true + } + } + case *types.Named: + return reaches(t.Underlying(), c) + default: + panic("unreachable") + } + return false +} diff --git a/compiler/internal/typeparams/subst/subst_test.go b/compiler/internal/typeparams/subst/subst_test.go new file mode 100644 index 000000000..53fadbcf0 --- /dev/null +++ b/compiler/internal/typeparams/subst/subst_test.go @@ -0,0 +1,103 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package subst + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" +) + +func TestSubst(t *testing.T) { + const source = ` +package P + +type t0 int +func (t0) f() +type t1 interface{ f() } +type t2 interface{ g() } +type t3 interface{ ~int } + +func Fn0[T t1](x T) T { + x.f() + return x +} + +type A[T any] [4]T +type B[T any] []T +type C[T, S any] []struct{s S; t T} +type D[T, S any] *struct{s S; t *T} +type E[T, S any] interface{ F() (T, S) } +type F[K comparable, V any] map[K]V +type G[T any] chan *T +type H[T any] func() T +type I[T any] struct{x, y, z int; t T} +type J[T any] interface{ t1 } +type K[T any] interface{ t1; F() T } +type L[T any] interface{ F() T; J[T] } + +var _ L[int] = Fn0[L[int]](nil) +` + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "hello.go", source, 0) + if err != nil { + t.Fatal(err) + } + + var conf types.Config + pkg, err := conf.Check("P", fset, []*ast.File{f}, nil) + if err != nil { + t.Fatal(err) + } + + for _, test := range []struct { + expr string // type expression of Named parameterized type + args []string // type expressions of args for named + want string // expected underlying value after substitution + }{ + {"A", []string{"string"}, "[4]string"}, + {"A", []string{"int"}, "[4]int"}, + {"B", []string{"int"}, "[]int"}, + {"B", []string{"int8"}, "[]int8"}, + {"C", []string{"int8", "string"}, "[]struct{s string; t int8}"}, + {"C", []string{"string", "int8"}, "[]struct{s int8; t string}"}, + {"D", []string{"int16", "string"}, "*struct{s string; t *int16}"}, + {"E", []string{"int32", "string"}, "interface{F() (int32, string)}"}, + {"F", []string{"int64", "string"}, "map[int64]string"}, + {"G", []string{"uint64"}, "chan *uint64"}, + {"H", []string{"uintptr"}, "func() uintptr"}, + {"I", []string{"t0"}, "struct{x int; y int; z int; t P.t0}"}, + {"J", []string{"t0"}, "interface{P.t1}"}, + {"K", []string{"t0"}, "interface{F() P.t0; P.t1}"}, + {"L", []string{"t0"}, "interface{F() P.t0; P.J[P.t0]}"}, + {"L", []string{"L[t0]"}, "interface{F() P.L[P.t0]; P.J[P.L[P.t0]]}"}, + } { + // Eval() expr for its type. + tv, err := types.Eval(fset, pkg, 0, test.expr) + if err != nil { + t.Fatalf("Eval(%s) failed: %v", test.expr, err) + } + // Eval() test.args[i] to get the i'th type arg. + var targs []types.Type + for _, astr := range test.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) + } + + T := tv.Type.(*types.Named) + + subst := makeSubster(types.NewContext(), nil, 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/compiler/internal/typeparams/subst/util.go b/compiler/internal/typeparams/subst/util.go new file mode 100644 index 000000000..22072e39f --- /dev/null +++ b/compiler/internal/typeparams/subst/util.go @@ -0,0 +1,23 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +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. +func assert(p bool, msg string) { + if !p { + panic(msg) + } +} + +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/compiler/statements.go b/compiler/statements.go index a852dadd9..225cd9cdb 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -773,7 +773,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { } func (fc *funcContext) translateResults(results []ast.Expr) string { - tuple := fc.typeResolver.Substitute(fc.sig).(*types.Signature).Results() + tuple := fc.typeResolver.Substitute(fc.sig.Results()).(*types.Tuple) switch tuple.Len() { case 0: return "" diff --git a/compiler/utils.go b/compiler/utils.go index 477e0c4f7..7063e41d2 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -448,7 +448,15 @@ func (fc *funcContext) instanceOf(ident *ast.Ident) typeparams.Instance { // defined in terms of type parameters, it will substitute type parameters with // concrete types from the current set of type arguments. func (fc *funcContext) typeOf(expr ast.Expr) types.Type { - return fc.typeResolver.Substitute(fc.pkgCtx.TypeOf(expr)) + typ := fc.pkgCtx.TypeOf(expr) + // If the expression is referring to an instance of a generic type or function, + // we want the instantiated type. + if ident, ok := expr.(*ast.Ident); ok { + if inst, ok := fc.pkgCtx.Instances[ident]; ok { + typ = inst.Type + } + } + return fc.typeResolver.Substitute(typ) } func (fc *funcContext) selectionOf(e *ast.SelectorExpr) (typesutil.Selection, bool) { From fb19faeecc36552c88a66c76bba247828c4ce922 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 24 Feb 2024 23:41:00 +0000 Subject: [PATCH 25/28] Update run.go to match the current set of passing tests. - typeswitch5.go now uses fmt.Println() and passes. - Most generics-related tests added in 1.19 also pass now. --- tests/gorepo/run.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index fdcfeaddb..9a1de8087 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -145,14 +145,12 @@ var knownFails = map[string]failReason{ "fixedbugs/issue50854.go": {category: lowLevelRuntimeDifference, desc: "negative int32 overflow behaves differently in JS"}, // These are new tests in Go 1.18 - "fixedbugs/issue46938.go": {category: notApplicable, desc: "tests -d=checkptr compiler mode, which GopherJS doesn't support"}, "fixedbugs/issue47928.go": {category: notApplicable, desc: "//go:nointerface is a part of GOEXPERIMENT=fieldtrack and is not supported by GopherJS"}, "fixedbugs/issue48536.go": {category: usesUnsupportedPackage, desc: "https://github.com/gopherjs/gopherjs/issues/1130"}, "fixedbugs/issue48898.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1128"}, "fixedbugs/issue53600.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, "typeparam/chans.go": {category: neverTerminates, desc: "uses runtime.SetFinalizer() and runtime.GC()."}, "typeparam/issue51733.go": {category: usesUnsupportedPackage, desc: "unsafe: uintptr to struct pointer conversion is unsupported"}, - "typeparam/typeswitch2.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, "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 @@ -161,10 +159,8 @@ var knownFails = map[string]failReason{ "typeparam/nested.go": {category: usesUnsupportedGenerics, desc: "incomplete support for generic types inside generic functions"}, // These are new tests in Go 1.19 + "typeparam/issue51521.go": {category: lowLevelRuntimeDifference, desc: "different panic message when calling a method on nil interface"}, "fixedbugs/issue50672.go": {category: usesUnsupportedGenerics, desc: "Checking function nesting with one function having a type parameter."}, - "fixedbugs/issue53137.go": {category: usesUnsupportedGenerics, desc: "Checking setting type parameter of struct in parameter of a generic function."}, - "fixedbugs/issue53309.go": {category: usesUnsupportedGenerics, desc: "Checking unused type parameter in method call to interface"}, - "fixedbugs/issue53635.go": {category: usesUnsupportedGenerics, desc: "Checking switch type against nil type with unsupported type parameters"}, "fixedbugs/issue53653.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format of int64 is different from Go's"}, } From deb26d08ede098f5e468f9c82dd92018b7138c62 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 25 Feb 2024 00:58:37 +0000 Subject: [PATCH 26/28] Diagnose fixedbugs/issue50672.go issue. Previously it was masked by missing generics support, but now it revealed another latent bug: https://github.com/gopherjs/gopherjs/issues/1271. --- tests/gorepo/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 9a1de8087..2b21eadb5 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -160,7 +160,7 @@ var knownFails = map[string]failReason{ // These are new tests in Go 1.19 "typeparam/issue51521.go": {category: lowLevelRuntimeDifference, desc: "different panic message when calling a method on nil interface"}, - "fixedbugs/issue50672.go": {category: usesUnsupportedGenerics, desc: "Checking function nesting with one function having a type parameter."}, + "fixedbugs/issue50672.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1271"}, "fixedbugs/issue53653.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format of int64 is different from Go's"}, } From 8a80f06b800aaa571731945bf9f9c9d30abfdeba Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 25 Feb 2024 01:45:33 +0000 Subject: [PATCH 27/28] Gofumpt the files to keep the linter happy. --- compiler/internal/typeparams/instance.go | 8 ++++---- go.mod | 5 +---- go.sum | 2 -- internal/testingx/must.go | 6 +++--- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index ea155801a..024821b45 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -106,10 +106,10 @@ func (iset *InstanceSet) Add(instances ...Instance) *InstanceSet { // Here's an example where it's very difficult to assign non-colliding // name-based keys to the two different types T: // -// func foo() { -// type T int -// { type T string } // Code block creates a new nested scope allowing for shadowing. -// } +// func foo() { +// type T int +// { type T string } // Code block creates a new nested scope allowing for shadowing. +// } func (iset *InstanceSet) ID(inst Instance) int { id, ok := iset.seen.get(inst) if !ok { diff --git a/go.mod b/go.mod index 03879b3f3..ccb130f48 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,4 @@ require ( golang.org/x/tools v0.16.0 ) -require ( - github.com/inconshreveable/mousetrap v1.0.0 // indirect - golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 -) +require github.com/inconshreveable/mousetrap v1.0.0 // indirect diff --git a/go.sum b/go.sum index 1288a90dd..8e69980d0 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 v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= -golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= 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= diff --git a/internal/testingx/must.go b/internal/testingx/must.go index 9b289f5b0..76caafebb 100644 --- a/internal/testingx/must.go +++ b/internal/testingx/must.go @@ -11,9 +11,9 @@ import "testing" // to check for test case conditions themselves because it provides a generic, // nondescript test error message. // -// func startServer(addr string) (*server, err) -// mustServer := testingx.Must[*server](t) -// mustServer(startServer(":8080")) +// func startServer(addr string) (*server, err) +// mustServer := testingx.Must[*server](t) +// mustServer(startServer(":8080")) func Must[T any](t *testing.T) func(v T, err error) T { return func(v T, err error) T { if err != nil { From b288e83fcb0f5aa881cfaa0755604ad05567f713 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 2 Mar 2024 14:19:51 +0000 Subject: [PATCH 28/28] Address pull request comments. I tried to fixup original comments to keep the history tidier, but that generated far too many merge conflicts than it's worth. Let it be my lesson for why pull requests shall be of a modest size. --- compiler/internal/typeparams/collect.go | 7 ++----- compiler/internal/typeparams/instance.go | 2 +- compiler/package.go | 2 +- compiler/typesutil/typenames.go | 2 +- compiler/utils.go | 2 +- internal/testingx/must.go | 4 ++-- tests/gorepo/run.go | 25 ++++++++++++------------ 7 files changed, 20 insertions(+), 24 deletions(-) diff --git a/compiler/internal/typeparams/collect.go b/compiler/internal/typeparams/collect.go index b9df1492b..6f0da9973 100644 --- a/compiler/internal/typeparams/collect.go +++ b/compiler/internal/typeparams/collect.go @@ -24,9 +24,6 @@ func NewResolver(tc *types.Context, tParams []*types.TypeParam, tArgs []types.Ty subster: subst.New(tc, tParams, tArgs), selMemo: map[typesutil.Selection]typesutil.Selection{}, } - if len(tParams) != len(tArgs) { - panic(fmt.Errorf("len(tParams)=%d not equal len(tArgs)=%d", len(tParams), len(tArgs))) - } return r } @@ -86,7 +83,7 @@ func (r *Resolver) SubstituteSelection(sel typesutil.Selection) typesutil.Select } } -// ToSlice converts TypeParamList into a slice with the sale order of entries. +// 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 { @@ -214,7 +211,7 @@ func (c *seedVisitor) Visit(n ast.Node) ast.Visitor { // InstanceSet may contain unprocessed instances of generic types and functions, // which will be also scanned, for example found in depending packages. // -// Note that instanced of generic type methods are automatically added to the +// Note that instances of generic type methods are automatically added to the // set whenever their receiver type instance is encountered. type Collector struct { TContext *types.Context diff --git a/compiler/internal/typeparams/instance.go b/compiler/internal/typeparams/instance.go index 024821b45..a64e3be8a 100644 --- a/compiler/internal/typeparams/instance.go +++ b/compiler/internal/typeparams/instance.go @@ -94,7 +94,7 @@ func (iset *InstanceSet) Add(instances ...Instance) *InstanceSet { // In order to have an ID assigned, the instance must have been previously added // to the set. // -// Note: we these ids are used in the generated code as keys to the specific +// Note: these ids are used in the generated code as keys to the specific // type/function instantiation in the type/function object. Using this has two // advantages: // diff --git a/compiler/package.go b/compiler/package.go index c5e31b3f6..112a16af7 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -399,7 +399,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor 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 of all + // TODO(nevkontakte): Set DCE attributes such that it is eliminated if all // instances are dead. varDecl := Decl{} varDecl.Vars = []string{funcCtx.objectName(o)} diff --git a/compiler/typesutil/typenames.go b/compiler/typesutil/typenames.go index b02464f9f..2f5ac6186 100644 --- a/compiler/typesutil/typenames.go +++ b/compiler/typesutil/typenames.go @@ -10,7 +10,7 @@ type TypeNames struct { order []*types.TypeName } -// Add a type name to the test. If the type name has been previously added, +// Add a type name to the set. If the type name has been previously added, // this operation is a no-op. Two type names are considered equal iff they have // the same memory address. func (tn *TypeNames) Add(name *types.TypeName) { diff --git a/compiler/utils.go b/compiler/utils.go index 7063e41d2..46794fe64 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -336,7 +336,7 @@ func isPkgLevel(o types.Object) bool { } // assignedObjectName checks if the object has been previously assigned a name -// in this or one of the parent contexts. If not, found will be false false. +// in this or one of the parent contexts. If not, found will be false. func (fc *funcContext) assignedObjectName(o types.Object) (name string, found bool) { if fc == nil { return "", false diff --git a/internal/testingx/must.go b/internal/testingx/must.go index 76caafebb..62d27dce8 100644 --- a/internal/testingx/must.go +++ b/internal/testingx/must.go @@ -3,12 +3,12 @@ package testingx import "testing" -// Must provides a concise way to handle handle returned error in tests that +// Must provides a concise way to handle returned error in cases that // "should never happen"©. // // This function can be used in test case setup that can be presumed to be // correct, but technically may return an error. This function MUST NOT be used -// to check for test case conditions themselves because it provides a generic, +// to check for test case conditions themselves because it generates a generic, // nondescript test error message. // // func startServer(addr string) (*server, err) diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 2b21eadb5..1259a42c3 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -24,7 +24,6 @@ import ( "fmt" "hash/fnv" "io" - "io/ioutil" "log" "os" "os/exec" @@ -464,8 +463,8 @@ func (t *test) goDirName() string { return filepath.Join(t.dir, strings.Replace(t.gofile, ".go", ".dir", -1)) } -func goDirFiles(longdir string) (filter []os.FileInfo, err error) { - files, dirErr := ioutil.ReadDir(longdir) +func goDirFiles(longdir string) (filter []os.DirEntry, err error) { + files, dirErr := os.ReadDir(longdir) if dirErr != nil { return nil, dirErr } @@ -488,7 +487,7 @@ func goDirPackages(longdir string) ([][]string, error) { m := make(map[string]int) for _, file := range files { name := file.Name() - data, err := ioutil.ReadFile(filepath.Join(longdir, name)) + data, err := os.ReadFile(filepath.Join(longdir, name)) if err != nil { return nil, err } @@ -600,7 +599,7 @@ func (t *test) run() { return } - srcBytes, err := ioutil.ReadFile(t.goFileName()) + srcBytes, err := os.ReadFile(t.goFileName()) if err != nil { t.err = err return @@ -693,7 +692,7 @@ func (t *test) run() { t.makeTempDir() defer os.RemoveAll(t.tempDir) - err = ioutil.WriteFile(filepath.Join(t.tempDir, t.gofile), srcBytes, 0o644) + err = os.WriteFile(filepath.Join(t.tempDir, t.gofile), srcBytes, 0o644) check(err) // A few tests (of things like the environment) require these to be set. @@ -878,7 +877,7 @@ func (t *test) run() { return } tfile := filepath.Join(t.tempDir, "tmp__.go") - if err := ioutil.WriteFile(tfile, out, 0o666); err != nil { + if err := os.WriteFile(tfile, out, 0o666); err != nil { t.err = fmt.Errorf("write tempfile:%s", err) return } @@ -899,7 +898,7 @@ func (t *test) run() { return } tfile := filepath.Join(t.tempDir, "tmp__.go") - err = ioutil.WriteFile(tfile, out, 0o666) + err = os.WriteFile(tfile, out, 0o666) if err != nil { t.err = fmt.Errorf("write tempfile:%s", err) return @@ -947,7 +946,7 @@ func (t *test) String() string { func (t *test) makeTempDir() { var err error - t.tempDir, err = ioutil.TempDir("", "") + t.tempDir, err = os.MkdirTemp("", "") check(err) } @@ -955,7 +954,7 @@ func (t *test) expectedOutput() string { filename := filepath.Join(t.dir, t.gofile) filename = filename[:len(filename)-len(".go")] filename += ".out" - b, _ := ioutil.ReadFile(filename) + b, _ := os.ReadFile(filename) return string(b) } @@ -1047,7 +1046,7 @@ func (t *test) errorCheck(outStr string, fullshort ...string) (err error) { func (t *test) updateErrors(out string, file string) { // Read in source file. - src, err := ioutil.ReadFile(file) + src, err := os.ReadFile(file) if err != nil { fmt.Fprintln(os.Stderr, err) return @@ -1102,7 +1101,7 @@ func (t *test) updateErrors(out string, file string) { } } // Write new file. - err = ioutil.WriteFile(file, []byte(strings.Join(lines, "\n")), 0o640) + err = os.WriteFile(file, []byte(strings.Join(lines, "\n")), 0o640) if err != nil { fmt.Fprintln(os.Stderr, err) return @@ -1159,7 +1158,7 @@ var ( func (t *test) wantedErrors(file, short string) (errs []wantedError) { cache := make(map[string]*regexp.Regexp) - src, _ := ioutil.ReadFile(file) + src, _ := os.ReadFile(file) for i, line := range strings.Split(string(src), "\n") { lineNum := i + 1 if strings.Contains(line, "////") {