// 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 ssa_test import ( "bytes" "fmt" "go/parser" "go/token" "reflect" "sort" "testing" "golang.org/x/tools/go/expect" "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" ) // TestGenericBodies tests that bodies of generic functions and methods containing // different constructs can be built in BuilderMode(0). // // Each test specifies the contents of package containing a single go file. // Each call print(arg0, arg1, ...) to the builtin print function // in ssa is correlated a comment at the end of the line of the form: // // //@ types(a, b, c) // // where a, b and c are the types of the arguments to the print call // serialized using go/types.Type.String(). // See x/tools/go/expect for details on the syntax. func TestGenericBodies(t *testing.T) { for _, contents := range []string{ ` package p00 func f(x int) { var i interface{} print(i, 0) //@ types("interface{}", int) print() //@ types() print(x) //@ types(int) } `, ` package p01 func f[T any](x T) { print(x) //@ types(T) } `, ` package p02 func f[T ~int]() { var x T print(x) //@ types(T) } `, ` package p03 func a[T ~[4]byte](x T) { for k, v := range x { print(x, k, v) //@ types(T, int, byte) } } func b[T ~*[4]byte](x T) { for k, v := range x { print(x, k, v) //@ types(T, int, byte) } } func c[T ~[]byte](x T) { for k, v := range x { print(x, k, v) //@ types(T, int, byte) } } func d[T ~string](x T) { for k, v := range x { print(x, k, v) //@ types(T, int, rune) } } func e[T ~map[int]string](x T) { for k, v := range x { print(x, k, v) //@ types(T, int, string) } } func f[T ~chan string](x T) { for v := range x { print(x, v) //@ types(T, string) } } func From() { type A [4]byte print(a[A]) //@ types("func(x p03.A)") type B *[4]byte print(b[B]) //@ types("func(x p03.B)") type C []byte print(c[C]) //@ types("func(x p03.C)") type D string print(d[D]) //@ types("func(x p03.D)") type E map[int]string print(e[E]) //@ types("func(x p03.E)") type F chan string print(f[F]) //@ types("func(x p03.F)") } `, ` package p05 func f[S any, T ~chan S](x T) { for v := range x { print(x, v) //@ types(T, S) } } func From() { type F chan string print(f[string, F]) //@ types("func(x p05.F)") } `, ` package p06 func fibonacci[T ~chan int](c, quit T) { x, y := 0, 1 for { select { case c <- x: x, y = y, x+y case <-quit: print(c, quit, x, y) //@ types(T, T, int, int) return } } } func start[T ~chan int](c, quit T) { go func() { for i := 0; i < 10; i++ { print(<-c) //@ types(int) } quit <- 0 }() } func From() { type F chan int c := make(F) quit := make(F) print(start[F], c, quit) //@ types("func(c p06.F, quit p06.F)", "p06.F", "p06.F") print(fibonacci[F], c, quit) //@ types("func(c p06.F, quit p06.F)", "p06.F", "p06.F") } `, ` package p07 func f[T ~struct{ x int; y string }](i int) T { u := []T{ T{0, "lorem"}, T{1, "ipsum"}} return u[i] } func From() { type S struct{ x int; y string } print(f[S]) //@ types("func(i int) p07.S") } `, ` package p08 func f[T ~[4]int8](x T, l, h int) []int8 { return x[l:h] } func g[T ~*[4]int16](x T, l, h int) []int16 { return x[l:h] } func h[T ~[]int32](x T, l, h int) T { return x[l:h] } func From() { type F [4]int8 type G *[4]int16 type H []int32 print(f[F](F{}, 0, 0)) //@ types("[]int8") print(g[G](nil, 0, 0)) //@ types("[]int16") print(h[H](nil, 0, 0)) //@ types("p08.H") } `, ` package p09 func h[E any, T ~[]E](x T, l, h int) []E { s := x[l:h] print(s) //@ types("T") return s } func From() { type H []int32 print(h[int32, H](nil, 0, 0)) //@ types("[]int32") } `, ` package p10 // Test "make" builtin with different forms on core types and // when capacities are constants or variable. func h[E any, T ~[]E](m, n int) { print(make(T, 3)) //@ types(T) print(make(T, 3, 5)) //@ types(T) print(make(T, m)) //@ types(T) print(make(T, m, n)) //@ types(T) } func i[K comparable, E any, T ~map[K]E](m int) { print(make(T)) //@ types(T) print(make(T, 5)) //@ types(T) print(make(T, m)) //@ types(T) } func j[E any, T ~chan E](m int) { print(make(T)) //@ types(T) print(make(T, 6)) //@ types(T) print(make(T, m)) //@ types(T) } func From() { type H []int32 h[int32, H](3, 4) type I map[int8]H i[int8, H, I](5) type J chan I j[I, J](6) } `, ` package p11 func h[T ~[4]int](x T) { print(len(x), cap(x)) //@ types(int, int) } func i[T ~[4]byte | []int | ~chan uint8](x T) { print(len(x), cap(x)) //@ types(int, int) } func j[T ~[4]int | any | map[string]int]() { print(new(T)) //@ types("*T") } func k[T ~[4]int | any | map[string]int](x T) { print(x) //@ types(T) panic(x) } `, ` package p12 func f[E any, F ~func() E](x F) { print(x, x()) //@ types(F, E) } func From() { type T func() int f[int, T](func() int { return 0 }) f[int, func() int](func() int { return 1 }) } `, ` package p13 func f[E any, M ~map[string]E](m M) { y, ok := m["lorem"] print(m, y, ok) //@ types(M, E, bool) } func From() { type O map[string][]int f(O{"lorem": []int{0, 1, 2, 3}}) } `, ` package p14 func a[T interface{ []int64 | [5]int64 }](x T) int64 { print(x, x[2], x[3]) //@ types(T, int64, int64) x[2] = 5 return x[3] } func b[T interface{ []byte | string }](x T) byte { print(x, x[3]) //@ types(T, byte) return x[3] } func c[T interface{ []byte }](x T) byte { print(x, x[2], x[3]) //@ types(T, byte, byte) x[2] = 'b' return x[3] } func d[T interface{ map[int]int64 }](x T) int64 { print(x, x[2], x[3]) //@ types(T, int64, int64) x[2] = 43 return x[3] } func e[T ~string](t T) { print(t, t[0]) //@ types(T, uint8) } func f[T ~string|[]byte](t T) { print(t, t[0]) //@ types(T, uint8) } func g[T []byte](t T) { print(t, t[0]) //@ types(T, byte) } func h[T ~[4]int|[]int](t T) { print(t, t[0]) //@ types(T, int) } func i[T ~[4]int|*[4]int|[]int](t T) { print(t, t[0]) //@ types(T, int) } func j[T ~[4]int|*[4]int|[]int](t T) { print(t, &t[0]) //@ types(T, "*int") } `, ` package p15 type MyInt int type Other int type MyInterface interface{ foo() } // ChangeType tests func ct0(x int) { v := MyInt(x); print(x, v) /*@ types(int, "p15.MyInt")*/ } func ct1[T MyInt | Other, S int ](x S) { v := T(x); print(x, v) /*@ types(S, T)*/ } func ct2[T int, S MyInt | int ](x S) { v := T(x); print(x, v) /*@ types(S, T)*/ } func ct3[T MyInt | Other, S MyInt | int ](x S) { v := T(x) ; print(x, v) /*@ types(S, T)*/ } // Convert tests func co0[T int | int8](x MyInt) { v := T(x); print(x, v) /*@ types("p15.MyInt", T)*/} func co1[T int | int8](x T) { v := MyInt(x); print(x, v) /*@ types(T, "p15.MyInt")*/ } func co2[S, T int | int8](x T) { v := S(x); print(x, v) /*@ types(T, S)*/ } // MakeInterface tests func mi0[T MyInterface](x T) { v := MyInterface(x); print(x, v) /*@ types(T, "p15.MyInterface")*/ } // NewConst tests func nc0[T any]() { v := (*T)(nil); print(v) /*@ types("*T")*/} // SliceToArrayPointer func sl0[T *[4]int | *[2]int](x []int) { v := T(x); print(x, v) /*@ types("[]int", T)*/ } func sl1[T *[4]int | *[2]int, S []int](x S) { v := T(x); print(x, v) /*@ types(S, T)*/ } `, ` package p16 func c[T interface{ foo() string }](x T) { print(x, x.foo, x.foo()) /*@ types(T, "func() string", string)*/ } `, ` package p17 func eq[T comparable](t T, i interface{}) bool { return t == i } `, // TODO(59983): investigate why writing g.c panics in (*FieldAddr).String. ` package p18 type S struct{ f int } func c[P *S]() []P { return []P{{f: 1}} } `, ` package p19 func sign[bytes []byte | string](s bytes) (bool, bool) { neg := false if len(s) > 0 && (s[0] == '-' || s[0] == '+') { neg = s[0] == '-' s = s[1:] } return !neg, len(s) > 0 } `, `package p20 func digits[bytes []byte | string](s bytes) bool { for _, c := range []byte(s) { if c < '0' || '9' < c { return false } } return true } `, ` package p21 type E interface{} func Foo[T E, PT interface{ *T }]() T { pt := PT(new(T)) x := *pt print(x) /*@ types(T)*/ return x } `, ` package p22 func f[M any, PM *M](p PM) { var m M *p = m print(m) /*@ types(M)*/ print(p) /*@ types(PM)*/ } `, ` package p23 type A struct{int} func (*A) Marker() {} type B struct{string} func (*B) Marker() {} type C struct{float32} func (*C) Marker() {} func process[T interface { *A *B *C Marker() }](v T) { v.Marker() a := *(any(v).(*A)); print(a) /*@ types("p23.A")*/ b := *(any(v).(*B)); print(b) /*@ types("p23.B")*/ c := *(any(v).(*C)); print(c) /*@ types("p23.C")*/ } `, ` package p24 func a[T any](f func() [4]T) { x := len(f()) print(x) /*@ types("int")*/ } func b[T [4]any](f func() T) { x := len(f()) print(x) /*@ types("int")*/ } func c[T any](f func() *[4]T) { x := len(f()) print(x) /*@ types("int")*/ } func d[T *[4]any](f func() T) { x := len(f()) print(x) /*@ types("int")*/ } `, ` package p25 func a[T any]() { var f func() [4]T for i, v := range f() { print(i, v) /*@ types("int", "T")*/ } } func b[T [4]any](f func() T) { for i, v := range f() { print(i, v) /*@ types("int", "any")*/ } } func c[T any](f func() *[4]T) { for i, v := range f() { print(i, v) /*@ types("int", "T")*/ } } func d[T *[4]any](f func() T) { for i, v := range f() { print(i, v) /*@ types("int", "any")*/ } } `, ` package issue64324 type bar[T any] interface { Bar(int) T } type foo[T any] interface { bar[[]T] *T } func Foo[T any, F foo[T]](d int) { m := new(T) f := F(m) print(f.Bar(d)) /*@ types("[]T")*/ } `, ` package issue64324b type bar[T any] interface { Bar(int) T } type baz[T any] interface { bar[*int] *int } func Baz[I baz[string]](d int) { m := new(int) f := I(m) print(f.Bar(d)) /*@ types("*int")*/ } `, } { contents := contents pkgname := packageName(t, contents) t.Run(pkgname, func(t *testing.T) { // Parse conf := loader.Config{ParserMode: parser.ParseComments} f, err := conf.ParseFile("file.go", contents) if err != nil { t.Fatalf("parse: %v", err) } conf.CreateFromFiles(pkgname, f) // Load lprog, err := conf.Load() if err != nil { t.Fatalf("Load: %v", err) } // Create and build SSA prog := ssa.NewProgram(lprog.Fset, ssa.SanityCheckFunctions) for _, info := range lprog.AllPackages { if info.TransitivelyErrorFree { prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable) } } p := prog.Package(lprog.Package(pkgname).Pkg) p.Build() // Collect all notes in f, i.e. comments starting with "//@ types". notes, err := expect.ExtractGo(prog.Fset, f) if err != nil { t.Errorf("expect.ExtractGo: %v", err) } // Collect calls to the builtin print function. probes := callsTo(p, "print") expectations := matchNotes(prog.Fset, notes, probes) for call := range probes { if expectations[call] == nil { t.Errorf("Unmatched call: %v", call) } } // Check each expectation. for call, note := range expectations { var args []string for _, a := range call.Args { args = append(args, a.Type().String()) } if got, want := fmt.Sprint(args), fmt.Sprint(note.Args); got != want { t.Errorf("Arguments to print() were expected to be %q. got %q", want, got) logFunction(t, probes[call]) } } }) } } // callsTo finds all calls to an SSA value named fname, // and returns a map from each call site to its enclosing function. func callsTo(p *ssa.Package, fname string) map[*ssa.CallCommon]*ssa.Function { callsites := make(map[*ssa.CallCommon]*ssa.Function) for _, mem := range p.Members { if fn, ok := mem.(*ssa.Function); ok { for _, bb := range fn.Blocks { for _, i := range bb.Instrs { if i, ok := i.(ssa.CallInstruction); ok { call := i.Common() if call.Value.Name() == fname { callsites[call] = fn } } } } } } return callsites } // matchNodes returns a mapping from call sites (found by callsTo) // to the first "//@ note" comment on the same line. func matchNotes(fset *token.FileSet, notes []*expect.Note, calls map[*ssa.CallCommon]*ssa.Function) map[*ssa.CallCommon]*expect.Note { // Matches each probe with a note that has the same line. sameLine := func(x, y token.Pos) bool { xp := fset.Position(x) yp := fset.Position(y) return xp.Filename == yp.Filename && xp.Line == yp.Line } expectations := make(map[*ssa.CallCommon]*expect.Note) for call := range calls { for _, note := range notes { if sameLine(call.Pos(), note.Pos) { expectations[call] = note break // first match is good enough. } } } return expectations } // TestInstructionString tests serializing instructions via Instruction.String(). func TestInstructionString(t *testing.T) { // Tests (ssa.Instruction).String(). Instructions are from a single go file. // The Instructions tested are those that match a comment of the form: // // //@ instrs(f, kind, strs...) // // where f is the name of the function, kind is the type of the instructions matched // within the function, and tests that the String() value for all of the instructions // matched of String() is strs (in some order). // See x/tools/go/expect for details on the syntax. const contents = ` package p //@ instrs("f0", "*ssa.TypeAssert") //@ instrs("f0", "*ssa.Call", "print(nil:interface{}, 0:int)") func f0(x int) { // non-generic smoke test. var i interface{} print(i, 0) } //@ instrs("f1", "*ssa.Alloc", "local T (u)") //@ instrs("f1", "*ssa.FieldAddr", "&t0.x [#0]") func f1[T ~struct{ x string }]() T { u := T{"lorem"} return u } //@ instrs("f1b", "*ssa.Alloc", "new T (complit)") //@ instrs("f1b", "*ssa.FieldAddr", "&t0.x [#0]") func f1b[T ~struct{ x string }]() *T { u := &T{"lorem"} return u } //@ instrs("f2", "*ssa.TypeAssert", "typeassert t0.(interface{})") //@ instrs("f2", "*ssa.Call", "invoke x.foo()") func f2[T interface{ foo() string }](x T) { _ = x.foo _ = x.foo() } //@ instrs("f3", "*ssa.TypeAssert", "typeassert t0.(interface{})") //@ instrs("f3", "*ssa.Call", "invoke x.foo()") func f3[T interface{ foo() string; comparable }](x T) { _ = x.foo _ = x.foo() } //@ instrs("f4", "*ssa.BinOp", "t1 + 1:int", "t2 < 4:int") //@ instrs("f4", "*ssa.Call", "f()", "print(t2, t4)") func f4[T [4]string](f func() T) { for i, v := range f() { print(i, v) } } //@ instrs("f5", "*ssa.Call", "nil:func()()") func f5() { var f func() f() } type S struct{ f int } //@ instrs("f6", "*ssa.Alloc", "new [1]P (slicelit)", "new S (complit)") //@ instrs("f6", "*ssa.IndexAddr", "&t0[0:int]") //@ instrs("f6", "*ssa.FieldAddr", "&t2.f [#0]") func f6[P *S]() []P { return []P{{f: 1}} } //@ instrs("f7", "*ssa.Alloc", "local S (complit)") //@ instrs("f7", "*ssa.FieldAddr", "&t0.f [#0]") func f7[T any, S struct{f T}](x T) S { return S{f: x} } //@ instrs("f8", "*ssa.Alloc", "new [1]P (slicelit)", "new struct{f T} (complit)") //@ instrs("f8", "*ssa.IndexAddr", "&t0[0:int]") //@ instrs("f8", "*ssa.FieldAddr", "&t2.f [#0]") func f8[T any, P *struct{f T}](x T) []P { return []P{{f: x}} } //@ instrs("f9", "*ssa.Alloc", "new [1]PS (slicelit)", "new S (complit)") //@ instrs("f9", "*ssa.IndexAddr", "&t0[0:int]") //@ instrs("f9", "*ssa.FieldAddr", "&t2.f [#0]") func f9[T any, S struct{f T}, PS *S](x T) { _ = []PS{{f: x}} } //@ instrs("f10", "*ssa.FieldAddr", "&t0.x [#0]") //@ instrs("f10", "*ssa.Store", "*t0 = *new(T):T", "*t1 = 4:int") func f10[T ~struct{ x, y int }]() T { var u T u = T{x: 4} return u } //@ instrs("f11", "*ssa.FieldAddr", "&t1.y [#1]") //@ instrs("f11", "*ssa.Store", "*t1 = *new(T):T", "*t2 = 5:int") func f11[T ~struct{ x, y int }, PT *T]() PT { var u PT = new(T) *u = T{y: 5} return u } //@ instrs("f12", "*ssa.Alloc", "new struct{f T} (complit)") //@ instrs("f12", "*ssa.MakeMap", "make map[P]bool 1:int") func f12[T any, P *struct{f T}](x T) map[P]bool { return map[P]bool{{}: true} } //@ instrs("f13", "*ssa.IndexAddr", "&v[0:int]") //@ instrs("f13", "*ssa.Store", "*t0 = 7:int", "*v = *new(A):A") func f13[A [3]int, PA *A](v PA) { *v = A{7} } //@ instrs("f14", "*ssa.Call", "invoke t1.Set(0:int)") func f14[T any, PT interface { Set(int) *T }]() { var t T p := PT(&t) p.Set(0) } //@ instrs("f15", "*ssa.MakeClosure", "make closure (interface{Set(int); *T}).Set$bound [t1]") func f15[T any, PT interface { Set(int) *T }]() func(int) { var t T p := PT(&t) return p.Set } ` // Parse conf := loader.Config{ParserMode: parser.ParseComments} const fname = "p.go" f, err := conf.ParseFile(fname, contents) if err != nil { t.Fatalf("parse: %v", err) } conf.CreateFromFiles("p", f) // Load lprog, err := conf.Load() if err != nil { t.Fatalf("Load: %v", err) } // Create and build SSA prog := ssa.NewProgram(lprog.Fset, ssa.SanityCheckFunctions) for _, info := range lprog.AllPackages { if info.TransitivelyErrorFree { prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable) } } p := prog.Package(lprog.Package("p").Pkg) p.Build() // Collect all notes in f, i.e. comments starting with "//@ instr". notes, err := expect.ExtractGo(prog.Fset, f) if err != nil { t.Errorf("expect.ExtractGo: %v", err) } // Expectation is a {function, type string} -> {want, matches} // where matches is all Instructions.String() that match the key. // Each expecation is that some permutation of matches is wants. type expKey struct { function string kind string } type expValue struct { wants []string matches []string } expectations := make(map[expKey]*expValue) for _, note := range notes { if note.Name == "instrs" { if len(note.Args) < 2 { t.Error("Had @instrs annotation without at least 2 arguments") continue } fn, kind := fmt.Sprint(note.Args[0]), fmt.Sprint(note.Args[1]) var wants []string for _, arg := range note.Args[2:] { wants = append(wants, fmt.Sprint(arg)) } expectations[expKey{fn, kind}] = &expValue{wants, nil} } } // Collect all Instructions that match the expectations. for _, mem := range p.Members { if fn, ok := mem.(*ssa.Function); ok { for _, bb := range fn.Blocks { for _, i := range bb.Instrs { kind := fmt.Sprintf("%T", i) if e := expectations[expKey{fn.Name(), kind}]; e != nil { e.matches = append(e.matches, i.String()) } } } } } // Check each expectation. for key, value := range expectations { fn, ok := p.Members[key.function].(*ssa.Function) if !ok { t.Errorf("Expectation on %s does not match a member in %s", key.function, p.Pkg.Name()) } got, want := value.matches, value.wants sort.Strings(got) sort.Strings(want) if !reflect.DeepEqual(want, got) { t.Errorf("Within %s wanted instructions of kind %s: %q. got %q", key.function, key.kind, want, got) logFunction(t, fn) } } } // packageName is a test helper to extract the package name from a string // containing the content of a go file. func packageName(t testing.TB, content string) string { f, err := parser.ParseFile(token.NewFileSet(), "", content, parser.PackageClauseOnly) if err != nil { t.Fatalf("parsing the file %q failed with error: %s", content, err) } return f.Name.Name } func logFunction(t testing.TB, fn *ssa.Function) { // TODO: Consider adding a ssa.Function.GoString() so this can be logged to t via '%#v'. var buf bytes.Buffer ssa.WriteFunction(&buf, fn) t.Log(buf.String()) }