From 924ff9133ede27cfd4855b3f12857a41d40e3210 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:05:40 -0700 Subject: [PATCH 1/4] compiler: make getTypeCodeName a method on compilerContext --- compiler/interface.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/compiler/interface.go b/compiler/interface.go index 5f7e7e345b..7fe7b4f0f2 100644 --- a/compiler/interface.go +++ b/compiler/interface.go @@ -165,7 +165,7 @@ func (c *compilerContext) getTypeCode(typ types.Type) llvm.Value { } } - typeCodeName, isLocal := getTypeCodeName(typ) + typeCodeName, isLocal := c.getTypeCodeName(typ) globalName := "reflect/types.type:" + typeCodeName var global llvm.Value if isLocal { @@ -580,7 +580,7 @@ var basicTypeNames = [...]string{ // getTypeCodeName returns a name for this type that can be used in the // interface lowering pass to assign type codes as expected by the reflect // package. See getTypeCodeNum. -func getTypeCodeName(t types.Type) (string, bool) { +func (c *compilerContext) getTypeCodeName(t types.Type) (string, bool) { switch t := types.Unalias(t).(type) { case *types.Named: if t.Obj().Parent() != t.Obj().Pkg().Scope() { @@ -588,12 +588,12 @@ func getTypeCodeName(t types.Type) (string, bool) { } return "named:" + t.String(), false case *types.Array: - s, isLocal := getTypeCodeName(t.Elem()) + s, isLocal := c.getTypeCodeName(t.Elem()) return "array:" + strconv.FormatInt(t.Len(), 10) + ":" + s, isLocal case *types.Basic: return "basic:" + basicTypeNames[t.Kind()], false case *types.Chan: - s, isLocal := getTypeCodeName(t.Elem()) + s, isLocal := c.getTypeCodeName(t.Elem()) var dir string switch t.Dir() { case types.SendOnly: @@ -613,7 +613,7 @@ func getTypeCodeName(t types.Type) (string, bool) { if !token.IsExported(name) { name = t.Method(i).Pkg().Path() + "." + name } - s, local := getTypeCodeName(t.Method(i).Type()) + s, local := c.getTypeCodeName(t.Method(i).Type()) if local { isLocal = true } @@ -621,17 +621,17 @@ func getTypeCodeName(t types.Type) (string, bool) { } return "interface:" + "{" + strings.Join(methods, ",") + "}", isLocal case *types.Map: - keyType, keyLocal := getTypeCodeName(t.Key()) - elemType, elemLocal := getTypeCodeName(t.Elem()) + keyType, keyLocal := c.getTypeCodeName(t.Key()) + elemType, elemLocal := c.getTypeCodeName(t.Elem()) return "map:" + "{" + keyType + "," + elemType + "}", keyLocal || elemLocal case *types.Pointer: - s, isLocal := getTypeCodeName(t.Elem()) + s, isLocal := c.getTypeCodeName(t.Elem()) return "pointer:" + s, isLocal case *types.Signature: isLocal := false params := make([]string, t.Params().Len()) for i := 0; i < t.Params().Len(); i++ { - s, local := getTypeCodeName(t.Params().At(i).Type()) + s, local := c.getTypeCodeName(t.Params().At(i).Type()) if local { isLocal = true } @@ -639,7 +639,7 @@ func getTypeCodeName(t types.Type) (string, bool) { } results := make([]string, t.Results().Len()) for i := 0; i < t.Results().Len(); i++ { - s, local := getTypeCodeName(t.Results().At(i).Type()) + s, local := c.getTypeCodeName(t.Results().At(i).Type()) if local { isLocal = true } @@ -647,7 +647,7 @@ func getTypeCodeName(t types.Type) (string, bool) { } return "func:" + "{" + strings.Join(params, ",") + "}{" + strings.Join(results, ",") + "}", isLocal case *types.Slice: - s, isLocal := getTypeCodeName(t.Elem()) + s, isLocal := c.getTypeCodeName(t.Elem()) return "slice:" + s, isLocal case *types.Struct: elems := make([]string, t.NumFields()) @@ -657,7 +657,7 @@ func getTypeCodeName(t types.Type) (string, bool) { if t.Field(i).Embedded() { embedded = "#" } - s, local := getTypeCodeName(t.Field(i).Type()) + s, local := c.getTypeCodeName(t.Field(i).Type()) if local { isLocal = true } @@ -769,7 +769,7 @@ func (b *builder) createTypeAssert(expr *ssa.TypeAssert) llvm.Value { commaOk = b.createInterfaceTypeAssert(intf, actualTypeNum) } } else { - name, _ := getTypeCodeName(expr.AssertedType) + name, _ := b.getTypeCodeName(expr.AssertedType) globalName := "reflect/types.typeid:" + name assertedTypeCodeGlobal := b.mod.NamedGlobal(globalName) if assertedTypeCodeGlobal.IsNil() { @@ -857,7 +857,7 @@ func (c *compilerContext) getMethodSetValue(methods []*types.Func) llvm.Value { if !token.IsExported(name) { name = method.Pkg().Path() + "." + name } - s, _ := getTypeCodeName(method.Type()) + s, _ := c.getTypeCodeName(method.Type()) globalName := "reflect/types.signature:" + name + ":" + s value := c.mod.NamedGlobal(globalName) if value.IsNil() { @@ -901,7 +901,7 @@ func (c *compilerContext) getMethodSetValue(methods []*types.Func) llvm.Value { // thunk is declared, not defined: it will be defined by the interface lowering // pass. func (c *compilerContext) getInvokeFunction(instr *ssa.CallCommon) llvm.Value { - s, _ := getTypeCodeName(instr.Value.Type().Underlying()) + s, _ := c.getTypeCodeName(instr.Value.Type().Underlying()) fnName := s + "." + instr.Method.Name() + "$invoke" llvmFn := c.mod.NamedFunction(fnName) if llvmFn.IsNil() { @@ -926,7 +926,7 @@ func (c *compilerContext) getInvokeFunction(instr *ssa.CallCommon) llvm.Value { // by the interface lowering pass as a type-ID comparison chain, avoiding the // need for runtime.typeImplementsMethodSet at compile time. func (b *builder) createInterfaceTypeAssert(intf *types.Interface, actualType llvm.Value) llvm.Value { - s, _ := getTypeCodeName(intf) + s, _ := b.getTypeCodeName(intf) fnName := s + ".$typeassert" llvmFn := b.mod.NamedFunction(fnName) if llvmFn.IsNil() { From 292f3141b8df408694d2db12dfdc7220d9c37951 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:22:05 -0700 Subject: [PATCH 2/4] testdata: add regression tests for function-local named types --- main_test.go | 2 + testdata/localtypes.go | 229 +++++++++++++++++++++++++++++++ testdata/localtypes.txt | 43 ++++++ testdata/localtypes/go.mod | 3 + testdata/localtypes/lib/lib.go | 28 ++++ testdata/localtypes/lib2/lib2.go | 5 + testdata/localtypes/main.go | 40 ++++++ testdata/localtypes/out.txt | 12 ++ 8 files changed, 362 insertions(+) create mode 100644 testdata/localtypes.go create mode 100644 testdata/localtypes.txt create mode 100644 testdata/localtypes/go.mod create mode 100644 testdata/localtypes/lib/lib.go create mode 100644 testdata/localtypes/lib2/lib2.go create mode 100644 testdata/localtypes/main.go create mode 100644 testdata/localtypes/out.txt diff --git a/main_test.go b/main_test.go index 55eb678910..f7d827eb58 100644 --- a/main_test.go +++ b/main_test.go @@ -76,6 +76,8 @@ func TestBuild(t *testing.T) { "init_multi.go", "interface.go", "json.go", + "localtypes/", + "localtypes.go", "map.go", "math.go", "oldgo/", diff --git a/testdata/localtypes.go b/testdata/localtypes.go new file mode 100644 index 0000000000..0ea9f80c7b --- /dev/null +++ b/testdata/localtypes.go @@ -0,0 +1,229 @@ +package main + +import "reflect" + +type checker = func(any) bool + +func siblingA() (any, checker) { + type Foo struct{ V int } + return Foo{V: 1}, func(x any) bool { _, ok := x.(Foo); return ok } +} + +func siblingB() (any, checker) { + type Foo struct{ V int } + return Foo{V: 2}, func(x any) bool { _, ok := x.(Foo); return ok } +} + +func nestedScopes() (any, any, any, checker, checker, checker) { + var av, bv, cv any + var ac, bc, cc checker + { + type Bar struct{ X int } + av = Bar{X: 10} + ac = func(x any) bool { _, ok := x.(Bar); return ok } + } + { + type Bar struct{ X int } + bv = Bar{X: 20} + bc = func(x any) bool { _, ok := x.(Bar); return ok } + } + { + type Bar struct{ X int } + cv = Bar{X: 30} + cc = func(x any) bool { _, ok := x.(Bar); return ok } + } + return av, bv, cv, ac, bc, cc +} + +func genericWithLocals[T any]() (any, any, checker, checker) { + type UsesT struct{ V T } + type NoT struct{ V int } + var z T + return UsesT{V: z}, NoT{V: 42}, + func(x any) bool { _, ok := x.(UsesT); return ok }, + func(x any) bool { _, ok := x.(NoT); return ok } +} + +func siblingClosures() (any, any, checker, checker) { + var av, bv any + var ac, bc checker + func() { + type C struct{ V int } + av = C{V: 1} + ac = func(x any) bool { _, ok := x.(C); return ok } + }() + func() { + type C struct{ V int } + bv = C{V: 2} + bc = func(x any) bool { _, ok := x.(C); return ok } + }() + return av, bv, ac, bc +} + +func closureInGenericUsesT[T any]() (any, checker) { + var v any + var c checker + func() { + type X struct{ V T } + var z T + v = X{V: z} + c = func(x any) bool { _, ok := x.(X); return ok } + }() + return v, c +} + +func closureInGenericNoT[T any]() (any, checker) { + var v any + var c checker + func() { + type X struct{ V int } + v = X{V: 7} + c = func(x any) bool { _, ok := x.(X); return ok } + }() + return v, c +} + +func siblingClosuresInGeneric[T any]() (any, any, checker, checker) { + var av, bv any + var ac, bc checker + func() { + type C struct{ V T } + var z T + av = C{V: z} + ac = func(x any) bool { _, ok := x.(C); return ok } + }() + func() { + type C struct{ V T } + var z T + bv = C{V: z} + bc = func(x any) bool { _, ok := x.(C); return ok } + }() + return av, bv, ac, bc +} + +func doublyNestedInGeneric[T any]() (any, checker) { + var v any + var c checker + func() { + func() { + type Y struct{ V T } + var z T + v = Y{V: z} + c = func(x any) bool { _, ok := x.(Y); return ok } + }() + }() + return v, c +} + +func expect(name string, ok bool) { + if ok { + println("ok:", name) + } else { + println("BUG:", name) + } +} + +// issue5180Copy1 and issue5180CopyIgnoreNilMembers are the original +// repro from https://github.com/tinygo-org/tinygo/issues/5180. Each +// declares its own local Foo with a different field type, then uses +// reflect.New to construct a value via the runtime type and asserts it +// back. Without distinct local-type names the two Foos collide and the +// second assertion panics. +func issue5180Copy1() bool { + type Foo struct{ A int } + f1 := &Foo{} + dst := reflect.New(reflect.TypeOf(f1).Elem()).Interface() + _, ok := dst.(*Foo) + return ok +} + +func issue5180CopyIgnoreNilMembers() (ok bool) { + type Foo struct{ A *int } + defer func() { + if r := recover(); r != nil { + ok = false + } + }() + f1 := &Foo{} + dst := reflect.New(reflect.TypeOf(f1).Elem()).Interface() + _, ok = dst.(*Foo) + return ok +} + +func main() { + expect("issue5180 TestCopy1", issue5180Copy1()) + expect("issue5180 TestCopyIgnoreNilMembers", issue5180CopyIgnoreNilMembers()) + + // Two siblings with same-named local types are distinct. + aV, aC := siblingA() + bV, bC := siblingB() + expect("siblingA.Foo accepts own", aC(aV)) + expect("siblingB.Foo accepts own", bC(bV)) + expect("siblingA.Foo rejects siblingB.Foo", !aC(bV)) + expect("siblingB.Foo rejects siblingA.Foo", !bC(aV)) + + // Three sibling-scope locals in one function are mutually distinct. + n1V, n2V, n3V, n1C, n2C, n3C := nestedScopes() + expect("nestedScopes.Bar#1 accepts own", n1C(n1V)) + expect("nestedScopes.Bar#2 accepts own", n2C(n2V)) + expect("nestedScopes.Bar#3 accepts own", n3C(n3V)) + expect("nestedScopes.Bar#1 rejects #2", !n1C(n2V)) + expect("nestedScopes.Bar#2 rejects #3", !n2C(n3V)) + expect("nestedScopes.Bar#1 rejects #3", !n1C(n3V)) + + // Generic instantiations are distinct from each other but match + // themselves across calls. + iU, iN, iUC, iNC := genericWithLocals[int]() + sU, sN, sUC, sNC := genericWithLocals[string]() + iU2, iN2, _, _ := genericWithLocals[int]() + expect("genericWithLocals[int].UsesT accepts own", iUC(iU)) + expect("genericWithLocals[int].NoT accepts own", iNC(iN)) + expect("genericWithLocals[string].UsesT accepts own", sUC(sU)) + expect("genericWithLocals[string].NoT accepts own", sNC(sN)) + expect("genericWithLocals[int].UsesT rejects [string].UsesT", !iUC(sU)) + expect("genericWithLocals[int].NoT rejects [string].NoT", !iNC(sN)) + expect("genericWithLocals[string].UsesT rejects [int].UsesT", !sUC(iU)) + expect("genericWithLocals[string].NoT rejects [int].NoT", !sNC(iN)) + expect("genericWithLocals[int].UsesT matches across calls", iUC(iU2)) + expect("genericWithLocals[int].NoT matches across calls", iNC(iN2)) + + // Sibling closures in a non-generic function. + scA, scB, scAC, scBC := siblingClosures() + expect("siblingClosures.C#1 accepts own", scAC(scA)) + expect("siblingClosures.C#2 accepts own", scBC(scB)) + expect("siblingClosures.C#1 rejects C#2", !scAC(scB)) + expect("siblingClosures.C#2 rejects C#1", !scBC(scA)) + + // Closure inside generic function, type uses T. + cgi, cgiC := closureInGenericUsesT[int]() + cgs, cgsC := closureInGenericUsesT[string]() + expect("closureInGenericUsesT[int].X accepts own", cgiC(cgi)) + expect("closureInGenericUsesT[string].X accepts own", cgsC(cgs)) + expect("closureInGenericUsesT[int].X rejects [string].X", !cgiC(cgs)) + expect("closureInGenericUsesT[string].X rejects [int].X", !cgsC(cgi)) + + // Closure inside generic function, type does not use T. + cni, cniC := closureInGenericNoT[int]() + cns, cnsC := closureInGenericNoT[string]() + expect("closureInGenericNoT[int].X accepts own", cniC(cni)) + expect("closureInGenericNoT[string].X accepts own", cnsC(cns)) + expect("closureInGenericNoT[int].X rejects [string].X", !cniC(cns)) + expect("closureInGenericNoT[string].X rejects [int].X", !cnsC(cni)) + + // Sibling closures inside a generic function. + sgIA, sgIB, sgIAC, sgIBC := siblingClosuresInGeneric[int]() + sgSA, _, sgSAC, _ := siblingClosuresInGeneric[string]() + expect("siblingClosuresInGeneric[int].C#1 accepts own", sgIAC(sgIA)) + expect("siblingClosuresInGeneric[int].C#2 accepts own", sgIBC(sgIB)) + expect("siblingClosuresInGeneric[int].C#1 rejects C#2", !sgIAC(sgIB)) + expect("siblingClosuresInGeneric[int].C#1 rejects [string].C#1", !sgIAC(sgSA)) + expect("siblingClosuresInGeneric[string].C#1 rejects [int].C#1", !sgSAC(sgIA)) + + // Doubly-nested closure inside a generic function. + dni, dniC := doublyNestedInGeneric[int]() + dns, dnsC := doublyNestedInGeneric[string]() + expect("doublyNestedInGeneric[int].Y accepts own", dniC(dni)) + expect("doublyNestedInGeneric[string].Y accepts own", dnsC(dns)) + expect("doublyNestedInGeneric[int].Y rejects [string].Y", !dniC(dns)) + expect("doublyNestedInGeneric[string].Y rejects [int].Y", !dnsC(dni)) +} diff --git a/testdata/localtypes.txt b/testdata/localtypes.txt new file mode 100644 index 0000000000..a010ad6405 --- /dev/null +++ b/testdata/localtypes.txt @@ -0,0 +1,43 @@ +BUG: issue5180 TestCopy1 +BUG: issue5180 TestCopyIgnoreNilMembers +ok: siblingA.Foo accepts own +BUG: siblingB.Foo accepts own +ok: siblingA.Foo rejects siblingB.Foo +BUG: siblingB.Foo rejects siblingA.Foo +ok: nestedScopes.Bar#1 accepts own +BUG: nestedScopes.Bar#2 accepts own +BUG: nestedScopes.Bar#3 accepts own +ok: nestedScopes.Bar#1 rejects #2 +ok: nestedScopes.Bar#2 rejects #3 +ok: nestedScopes.Bar#1 rejects #3 +ok: genericWithLocals[int].UsesT accepts own +ok: genericWithLocals[int].NoT accepts own +BUG: genericWithLocals[string].UsesT accepts own +BUG: genericWithLocals[string].NoT accepts own +ok: genericWithLocals[int].UsesT rejects [string].UsesT +ok: genericWithLocals[int].NoT rejects [string].NoT +BUG: genericWithLocals[string].UsesT rejects [int].UsesT +BUG: genericWithLocals[string].NoT rejects [int].NoT +ok: genericWithLocals[int].UsesT matches across calls +ok: genericWithLocals[int].NoT matches across calls +ok: siblingClosures.C#1 accepts own +BUG: siblingClosures.C#2 accepts own +ok: siblingClosures.C#1 rejects C#2 +BUG: siblingClosures.C#2 rejects C#1 +ok: closureInGenericUsesT[int].X accepts own +BUG: closureInGenericUsesT[string].X accepts own +ok: closureInGenericUsesT[int].X rejects [string].X +BUG: closureInGenericUsesT[string].X rejects [int].X +BUG: closureInGenericNoT[int].X accepts own +BUG: closureInGenericNoT[string].X accepts own +ok: closureInGenericNoT[int].X rejects [string].X +ok: closureInGenericNoT[string].X rejects [int].X +BUG: siblingClosuresInGeneric[int].C#1 accepts own +BUG: siblingClosuresInGeneric[int].C#2 accepts own +ok: siblingClosuresInGeneric[int].C#1 rejects C#2 +ok: siblingClosuresInGeneric[int].C#1 rejects [string].C#1 +ok: siblingClosuresInGeneric[string].C#1 rejects [int].C#1 +ok: doublyNestedInGeneric[int].Y accepts own +BUG: doublyNestedInGeneric[string].Y accepts own +ok: doublyNestedInGeneric[int].Y rejects [string].Y +BUG: doublyNestedInGeneric[string].Y rejects [int].Y diff --git a/testdata/localtypes/go.mod b/testdata/localtypes/go.mod new file mode 100644 index 0000000000..fe996758fa --- /dev/null +++ b/testdata/localtypes/go.mod @@ -0,0 +1,3 @@ +module github.com/tinygo-org/tinygo/testdata/localtypes + +go 1.21 diff --git a/testdata/localtypes/lib/lib.go b/testdata/localtypes/lib/lib.go new file mode 100644 index 0000000000..404328a192 --- /dev/null +++ b/testdata/localtypes/lib/lib.go @@ -0,0 +1,28 @@ +package lib + +type Checker = func(any) bool + +func GenericWithLocals[T any]() (any, any, Checker, Checker) { + type UsesT struct{ V T } + type NoT struct{ V int } + var z T + return UsesT{V: z}, NoT{V: 42}, + func(x any) bool { _, ok := x.(UsesT); return ok }, + func(x any) bool { _, ok := x.(NoT); return ok } +} + +func SiblingClosures() (any, any, Checker, Checker) { + var av, bv any + var ac, bc Checker + func() { + type Foo struct{ V int } + av = Foo{V: 1} + ac = func(x any) bool { _, ok := x.(Foo); return ok } + }() + func() { + type Foo struct{ V int } + bv = Foo{V: 2} + bc = func(x any) bool { _, ok := x.(Foo); return ok } + }() + return av, bv, ac, bc +} diff --git a/testdata/localtypes/lib2/lib2.go b/testdata/localtypes/lib2/lib2.go new file mode 100644 index 0000000000..5aecb2316b --- /dev/null +++ b/testdata/localtypes/lib2/lib2.go @@ -0,0 +1,5 @@ +package lib2 + +import "github.com/tinygo-org/tinygo/testdata/localtypes/lib" + +func IntPair() (any, any, lib.Checker, lib.Checker) { return lib.GenericWithLocals[int]() } diff --git a/testdata/localtypes/main.go b/testdata/localtypes/main.go new file mode 100644 index 0000000000..880e6dc29a --- /dev/null +++ b/testdata/localtypes/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "github.com/tinygo-org/tinygo/testdata/localtypes/lib" + "github.com/tinygo-org/tinygo/testdata/localtypes/lib2" +) + +func expect(name string, ok bool) { + if ok { + println("ok:", name) + } else { + println("BUG:", name) + } +} + +func main() { + mainU, mainN, mainUC, mainNC := lib.GenericWithLocals[int]() + libU, libN, libUC, libNC := lib2.IntPair() + + // Same instance compiled in two packages: each package's checker + // must accept the other package's value. + expect("main GenericWithLocals[int].UsesT accepts lib2's value", mainUC(libU)) + expect("main GenericWithLocals[int].NoT accepts lib2's value", mainNC(libN)) + expect("lib2 GenericWithLocals[int].UsesT accepts main's value", libUC(mainU)) + expect("lib2 GenericWithLocals[int].NoT accepts main's value", libNC(mainN)) + + // Different instantiations: distinct types. + stringU, stringN, stringUC, stringNC := lib.GenericWithLocals[string]() + expect("GenericWithLocals[int].UsesT rejects [string].UsesT", !mainUC(stringU)) + expect("GenericWithLocals[int].NoT rejects [string].NoT", !mainNC(stringN)) + expect("GenericWithLocals[string].UsesT rejects [int].UsesT", !stringUC(mainU)) + expect("GenericWithLocals[string].NoT rejects [int].NoT", !stringNC(mainN)) + + // Sibling closures inside one function (declared in lib). + scA, scB, scAC, scBC := lib.SiblingClosures() + expect("lib.SiblingClosures.Foo#1 accepts own", scAC(scA)) + expect("lib.SiblingClosures.Foo#2 accepts own", scBC(scB)) + expect("lib.SiblingClosures.Foo#1 rejects Foo#2", !scAC(scB)) + expect("lib.SiblingClosures.Foo#2 rejects Foo#1", !scBC(scA)) +} diff --git a/testdata/localtypes/out.txt b/testdata/localtypes/out.txt new file mode 100644 index 0000000000..12a49b1c87 --- /dev/null +++ b/testdata/localtypes/out.txt @@ -0,0 +1,12 @@ +ok: main GenericWithLocals[int].UsesT accepts lib2's value +ok: main GenericWithLocals[int].NoT accepts lib2's value +ok: lib2 GenericWithLocals[int].UsesT accepts main's value +ok: lib2 GenericWithLocals[int].NoT accepts main's value +ok: GenericWithLocals[int].UsesT rejects [string].UsesT +ok: GenericWithLocals[int].NoT rejects [string].NoT +BUG: GenericWithLocals[string].UsesT rejects [int].UsesT +BUG: GenericWithLocals[string].NoT rejects [int].NoT +ok: lib.SiblingClosures.Foo#1 accepts own +BUG: lib.SiblingClosures.Foo#2 accepts own +ok: lib.SiblingClosures.Foo#1 rejects Foo#2 +BUG: lib.SiblingClosures.Foo#2 rejects Foo#1 From d05e0b73560a1b45d3e2bea72f505f508c0ff5ca Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:22:57 -0700 Subject: [PATCH 3/4] compiler: disambiguate function-local named types --- compiler/compiler.go | 23 ++- compiler/interface.go | 290 +++++++++++++++++++++++++++++++++++- testdata/localtypes.txt | 40 ++--- testdata/localtypes/out.txt | 8 +- 4 files changed, 325 insertions(+), 36 deletions(-) diff --git a/compiler/compiler.go b/compiler/compiler.go index 45e6c8a54b..353a1799f4 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -92,20 +92,22 @@ type compilerContext struct { pkg *types.Package packageDir string // directory for this package runtimePkg *types.Package + localTypeNames map[*types.TypeName]string } // newCompilerContext returns a new compiler context ready for use, most // importantly with a newly created LLVM context and module. func newCompilerContext(moduleName string, machine llvm.TargetMachine, config *Config, dumpSSA bool) *compilerContext { c := &compilerContext{ - Config: config, - DumpSSA: dumpSSA, - difiles: make(map[string]llvm.Metadata), - ditypes: make(map[types.Type]llvm.Metadata), - machine: machine, - targetData: machine.CreateTargetData(), - functionInfos: map[*ssa.Function]functionInfo{}, - astComments: map[string]*ast.CommentGroup{}, + Config: config, + DumpSSA: dumpSSA, + difiles: make(map[string]llvm.Metadata), + ditypes: make(map[types.Type]llvm.Metadata), + machine: machine, + targetData: machine.CreateTargetData(), + functionInfos: map[*ssa.Function]functionInfo{}, + astComments: map[string]*ast.CommentGroup{}, + localTypeNames: map[*types.TypeName]string{}, } c.ctx = llvm.NewContext() @@ -300,6 +302,11 @@ func CompilePackage(moduleName string, pkg *loader.Package, ssaPkg *ssa.Package, // Convert AST to SSA. ssaPkg.Build() + // Assign names to function-local named types before compiling the + // package, so that types declared in different functions (or in + // different instantiations of a generic function) do not collide. + c.scanLocalTypes(ssaPkg) + // Initialize debug information. if c.Debug { c.cu = c.dibuilder.CreateCompileUnit(llvm.DICompileUnit{ diff --git a/compiler/interface.go b/compiler/interface.go index 7fe7b4f0f2..83034ae56e 100644 --- a/compiler/interface.go +++ b/compiler/interface.go @@ -580,13 +580,27 @@ var basicTypeNames = [...]string{ // getTypeCodeName returns a name for this type that can be used in the // interface lowering pass to assign type codes as expected by the reflect // package. See getTypeCodeNum. -func (c *compilerContext) getTypeCodeName(t types.Type) (string, bool) { +// +// isLocal is true when the type is declared inside a function body. +// Such types need a per-declaration (or per instantiation) suffix +// because their printed names are not unique; scanLocalTypes assigns +// the suffix and stores the result in c.localTypeNames. +func (c *compilerContext) getTypeCodeName(t types.Type) (name string, isLocal bool) { switch t := types.Unalias(t).(type) { case *types.Named: - if t.Obj().Parent() != t.Obj().Pkg().Scope() { - return "named:" + t.String() + "$local", true + tn := t.Obj() + if tn.Pkg() == nil || tn.Parent() == tn.Pkg().Scope() { + // Package-scope or builtin: the printed name is unique. + return "named:" + t.String(), false + } + // Function-local type. Both ordinary locals (Parent() != nil) + // and synthetic locals from generic instantiation + // (Parent() == nil) are pre-registered by scanLocalTypes. + n, ok := c.localTypeNames[tn] + if !ok { + panic("compiler: local type " + tn.Name() + " was not registered by scanLocalTypes") } - return "named:" + t.String(), false + return "named:" + n, true case *types.Array: s, isLocal := c.getTypeCodeName(t.Elem()) return "array:" + strconv.FormatInt(t.Len(), 10) + ":" + s, isLocal @@ -672,6 +686,274 @@ func (c *compilerContext) getTypeCodeName(t types.Type) (string, bool) { } } +// scanLocalTypes assigns names to every function-local named type in +// the package and stores them in c.localTypeNames. Two flavors are +// handled: +// +// 1. Synthetic TypeNames produced by generic instantiation +// (TypeName.Parent() == nil). Two instantiations of the same +// generic function (e.g. F[int] and F[string]) produce TypeNames +// with the same printed name and the same source position, so +// each one is named with the enclosing instance's RelString as +// prefix. RelString encodes the type arguments, matching Go's +// runtime behavior, where F[int].Inner and F[string].Inner are +// distinct types even when Inner does not mention the type +// parameter. +// +// 2. Ordinary function-local TypeNames (TypeName.Parent() != nil). +// Each one is named with its declaring function's RelString plus +// a per-function counter assigned in source order. This mirrors +// the ·N suffix the standard Go compiler uses for such types and +// is robust against //line directives that would otherwise make a +// file:line:column suffix non-unique. +// +// Names depend only on intrinsic SSA properties (RelString and the +// raw token.Pos used as a sort key), so any package compiling the +// same function or instance produces the same identifier. +func (c *compilerContext) scanLocalTypes(ssaPkg *ssa.Package) { + // Pass 1: locate every generic instance reachable from this + // package (including instances declared in imported packages and + // any function reached through an instance subtree). Synthetic + // TypeNames are produced by instantiation, so we need the call + // graph to find them all. + var instances []*ssa.Function + seenInstWalk := map[*ssa.Function]bool{} + var instWalk func(fn *ssa.Function, inInstance bool) + instWalk = func(fn *ssa.Function, inInstance bool) { + if fn == nil || seenInstWalk[fn] { + return + } + // fn belongs to an instance subtree if it is itself an + // instantiation or if we reached it from one. + // + // len(TypeArgs()) is used instead of fn.Origin() because + // Origin() may call Build() on fn's declaring package, which + // would defeat per-package compilation. + isInstanceRoot := len(fn.TypeArgs()) > 0 + if !isInstanceRoot && !inInstance && fn.Pkg != ssaPkg { + return + } + if fn.Blocks == nil && fn.AnonFuncs == nil { + return + } + seenInstWalk[fn] = true + isInInstance := inInstance || isInstanceRoot + if isInInstance { + instances = append(instances, fn) + } + for _, anon := range fn.AnonFuncs { + instWalk(anon, isInInstance) + } + var ops [10]*ssa.Value + for _, b := range fn.Blocks { + for _, instr := range b.Instrs { + for _, op := range instr.Operands(ops[:0]) { + if op == nil || *op == nil { + continue + } + if callee, ok := (*op).(*ssa.Function); ok { + instWalk(callee, isInInstance) + } + } + } + } + } + for _, member := range ssaPkg.Members { + switch m := member.(type) { + case *ssa.Function: + instWalk(m, false) + case *ssa.Type: + mset := c.program.MethodSets.MethodSet(m.Type()) + for i := 0; i < mset.Len(); i++ { + instWalk(c.program.MethodValue(mset.At(i)), false) + } + pmset := c.program.MethodSets.MethodSet(types.NewPointer(m.Type())) + for i := 0; i < pmset.Len(); i++ { + instWalk(c.program.MethodValue(pmset.At(i)), false) + } + } + } + + // Pass 2: collect every non-instance function defined in this + // package together with its closures. Ordinary function-local + // TypeNames are scoped to their declaring function (and visible + // in nested closures only), so the declaring function is always + // somewhere in this lexical tree. Following callees here would + // be wrong: an instantiated function whose substituted signature + // mentions the local type would otherwise race with the actual + // declaring function for ownership. + var packageFuncs []*ssa.Function + var collect func(fn *ssa.Function) + collect = func(fn *ssa.Function) { + if fn == nil || fn.Pkg != ssaPkg { + return + } + if len(fn.TypeArgs()) > 0 { + // Generic instances are handled by pass 1 (their local + // types are synthetic). + return + } + if fn.Blocks == nil && fn.AnonFuncs == nil { + return + } + packageFuncs = append(packageFuncs, fn) + for _, anon := range fn.AnonFuncs { + collect(anon) + } + } + for _, member := range ssaPkg.Members { + switch m := member.(type) { + case *ssa.Function: + collect(m) + case *ssa.Type: + mset := c.program.MethodSets.MethodSet(m.Type()) + for i := 0; i < mset.Len(); i++ { + collect(c.program.MethodValue(mset.At(i))) + } + pmset := c.program.MethodSets.MethodSet(types.NewPointer(m.Type())) + for i := 0; i < pmset.Len(); i++ { + collect(c.program.MethodValue(pmset.At(i))) + } + } + } + + // Registration is first-writer-wins, so visit each list in a + // deterministic order. Pos() is a defensive tiebreaker. + sortFns := func(fns []*ssa.Function) { + sort.Slice(fns, func(i, j int) bool { + ri, rj := fns[i].RelString(nil), fns[j].RelString(nil) + if ri != rj { + return ri < rj + } + return fns[i].Pos() < fns[j].Pos() + }) + } + sortFns(instances) + sortFns(packageFuncs) + for _, fn := range instances { + c.registerLocalTypes(fn, true) + } + for _, fn := range packageFuncs { + c.registerLocalTypes(fn, false) + } +} + +// registerLocalTypes walks every type reachable from fn's body and +// records each function-local TypeName whose Parent() matches the +// synthetic flag (Parent() == nil for synthetic, != nil otherwise) in +// c.localTypeNames. Each TypeName is named with fn.RelString as the +// owning function plus a per-function counter assigned in source order. +// +// First-writer-wins: a TypeName already present in c.localTypeNames +// is left alone. The slot is reserved with an empty string during +// collection so later registerLocalTypes calls (within the same +// scanLocalTypes invocation) skip it; the final name is filled in +// after sorting, before scanLocalTypes returns and any +// getTypeCodeName lookups happen. +func (c *compilerContext) registerLocalTypes(fn *ssa.Function, synthetic bool) { + var found []*types.TypeName + seen := map[types.Type]bool{} + var visit func(t types.Type) + visit = func(t types.Type) { + if t == nil || seen[t] { + return + } + seen[t] = true + switch t := t.(type) { + case *types.Alias: + visit(types.Unalias(t)) + case *types.Named: + tn := t.Obj() + if tn.Pkg() != nil && (tn.Parent() == nil) == synthetic { + if _, ok := c.localTypeNames[tn]; !ok { + c.localTypeNames[tn] = "" + found = append(found, tn) + } + } + targs := t.TypeArgs() + for i := 0; i < targs.Len(); i++ { + visit(targs.At(i)) + } + visit(t.Underlying()) + case *types.Pointer: + visit(t.Elem()) + case *types.Slice: + visit(t.Elem()) + case *types.Array: + visit(t.Elem()) + case *types.Chan: + visit(t.Elem()) + case *types.Map: + visit(t.Key()) + visit(t.Elem()) + case *types.Struct: + for i := 0; i < t.NumFields(); i++ { + visit(t.Field(i).Type()) + } + case *types.Signature: + if p := t.Params(); p != nil { + for i := 0; i < p.Len(); i++ { + visit(p.At(i).Type()) + } + } + if r := t.Results(); r != nil { + for i := 0; i < r.Len(); i++ { + visit(r.At(i).Type()) + } + } + case *types.Tuple: + for i := 0; i < t.Len(); i++ { + visit(t.At(i).Type()) + } + case *types.Interface: + // A local type can be reachable only through a local + // interface's method signature, so descend into them. + // getTypeCodeName encodes those signatures into the + // interface's identifier, and the seen map breaks + // cycles formed by methods that mention the interface + // itself. + for i := 0; i < t.NumMethods(); i++ { + visit(t.Method(i).Type()) + } + } + } + for _, p := range fn.Params { + visit(p.Type()) + } + for _, fv := range fn.FreeVars { + visit(fv.Type()) + } + for _, l := range fn.Locals { + visit(l.Type()) + } + var ops [10]*ssa.Value + for _, b := range fn.Blocks { + for _, instr := range b.Instrs { + if v, ok := instr.(ssa.Value); ok { + visit(v.Type()) + } + for _, op := range instr.Operands(ops[:0]) { + if op != nil && *op != nil { + visit((*op).Type()) + } + } + } + } + if len(found) == 0 { + return + } + // Sort by raw token.Pos: this gives a total order on declarations + // that is stable across builds and unaffected by //line directives + // (which only adjust the human-facing position from Fset.Position). + sort.Slice(found, func(i, j int) bool { + return found[i].Pos() < found[j].Pos() + }) + enclosing := fn.RelString(nil) + for i, tn := range found { + c.localTypeNames[tn] = enclosing + "." + tn.Name() + "$" + strconv.Itoa(i+1) + } +} + // getTypeMethodSet returns a reference (GEP) to a global method set. This // method set should be unreferenced after the interface lowering pass. func (c *compilerContext) getTypeMethodSet(typ types.Type) llvm.Value { diff --git a/testdata/localtypes.txt b/testdata/localtypes.txt index a010ad6405..f9253c32d1 100644 --- a/testdata/localtypes.txt +++ b/testdata/localtypes.txt @@ -1,43 +1,43 @@ -BUG: issue5180 TestCopy1 -BUG: issue5180 TestCopyIgnoreNilMembers +ok: issue5180 TestCopy1 +ok: issue5180 TestCopyIgnoreNilMembers ok: siblingA.Foo accepts own -BUG: siblingB.Foo accepts own +ok: siblingB.Foo accepts own ok: siblingA.Foo rejects siblingB.Foo -BUG: siblingB.Foo rejects siblingA.Foo +ok: siblingB.Foo rejects siblingA.Foo ok: nestedScopes.Bar#1 accepts own -BUG: nestedScopes.Bar#2 accepts own -BUG: nestedScopes.Bar#3 accepts own +ok: nestedScopes.Bar#2 accepts own +ok: nestedScopes.Bar#3 accepts own ok: nestedScopes.Bar#1 rejects #2 ok: nestedScopes.Bar#2 rejects #3 ok: nestedScopes.Bar#1 rejects #3 ok: genericWithLocals[int].UsesT accepts own ok: genericWithLocals[int].NoT accepts own -BUG: genericWithLocals[string].UsesT accepts own -BUG: genericWithLocals[string].NoT accepts own +ok: genericWithLocals[string].UsesT accepts own +ok: genericWithLocals[string].NoT accepts own ok: genericWithLocals[int].UsesT rejects [string].UsesT ok: genericWithLocals[int].NoT rejects [string].NoT -BUG: genericWithLocals[string].UsesT rejects [int].UsesT -BUG: genericWithLocals[string].NoT rejects [int].NoT +ok: genericWithLocals[string].UsesT rejects [int].UsesT +ok: genericWithLocals[string].NoT rejects [int].NoT ok: genericWithLocals[int].UsesT matches across calls ok: genericWithLocals[int].NoT matches across calls ok: siblingClosures.C#1 accepts own -BUG: siblingClosures.C#2 accepts own +ok: siblingClosures.C#2 accepts own ok: siblingClosures.C#1 rejects C#2 -BUG: siblingClosures.C#2 rejects C#1 +ok: siblingClosures.C#2 rejects C#1 ok: closureInGenericUsesT[int].X accepts own -BUG: closureInGenericUsesT[string].X accepts own +ok: closureInGenericUsesT[string].X accepts own ok: closureInGenericUsesT[int].X rejects [string].X -BUG: closureInGenericUsesT[string].X rejects [int].X -BUG: closureInGenericNoT[int].X accepts own -BUG: closureInGenericNoT[string].X accepts own +ok: closureInGenericUsesT[string].X rejects [int].X +ok: closureInGenericNoT[int].X accepts own +ok: closureInGenericNoT[string].X accepts own ok: closureInGenericNoT[int].X rejects [string].X ok: closureInGenericNoT[string].X rejects [int].X -BUG: siblingClosuresInGeneric[int].C#1 accepts own -BUG: siblingClosuresInGeneric[int].C#2 accepts own +ok: siblingClosuresInGeneric[int].C#1 accepts own +ok: siblingClosuresInGeneric[int].C#2 accepts own ok: siblingClosuresInGeneric[int].C#1 rejects C#2 ok: siblingClosuresInGeneric[int].C#1 rejects [string].C#1 ok: siblingClosuresInGeneric[string].C#1 rejects [int].C#1 ok: doublyNestedInGeneric[int].Y accepts own -BUG: doublyNestedInGeneric[string].Y accepts own +ok: doublyNestedInGeneric[string].Y accepts own ok: doublyNestedInGeneric[int].Y rejects [string].Y -BUG: doublyNestedInGeneric[string].Y rejects [int].Y +ok: doublyNestedInGeneric[string].Y rejects [int].Y diff --git a/testdata/localtypes/out.txt b/testdata/localtypes/out.txt index 12a49b1c87..52266e57b3 100644 --- a/testdata/localtypes/out.txt +++ b/testdata/localtypes/out.txt @@ -4,9 +4,9 @@ ok: lib2 GenericWithLocals[int].UsesT accepts main's value ok: lib2 GenericWithLocals[int].NoT accepts main's value ok: GenericWithLocals[int].UsesT rejects [string].UsesT ok: GenericWithLocals[int].NoT rejects [string].NoT -BUG: GenericWithLocals[string].UsesT rejects [int].UsesT -BUG: GenericWithLocals[string].NoT rejects [int].NoT +ok: GenericWithLocals[string].UsesT rejects [int].UsesT +ok: GenericWithLocals[string].NoT rejects [int].NoT ok: lib.SiblingClosures.Foo#1 accepts own -BUG: lib.SiblingClosures.Foo#2 accepts own +ok: lib.SiblingClosures.Foo#2 accepts own ok: lib.SiblingClosures.Foo#1 rejects Foo#2 -BUG: lib.SiblingClosures.Foo#2 rejects Foo#1 +ok: lib.SiblingClosures.Foo#2 rejects Foo#1 From d0cf28b0fe3ea875fb6c2098d40e6d841bd5cd28 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 24 Apr 2026 09:23:10 -0700 Subject: [PATCH 4/4] Kill off the second pass --- compiler/compiler.go | 19 ++-- compiler/interface.go | 225 ++++++++++++++++++------------------------ 2 files changed, 106 insertions(+), 138 deletions(-) diff --git a/compiler/compiler.go b/compiler/compiler.go index 353a1799f4..c537b6c1c2 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -92,22 +92,21 @@ type compilerContext struct { pkg *types.Package packageDir string // directory for this package runtimePkg *types.Package - localTypeNames map[*types.TypeName]string + localTypeNames typeutil.Map // *types.Named (synthetic local from generic instantiation) -> string } // newCompilerContext returns a new compiler context ready for use, most // importantly with a newly created LLVM context and module. func newCompilerContext(moduleName string, machine llvm.TargetMachine, config *Config, dumpSSA bool) *compilerContext { c := &compilerContext{ - Config: config, - DumpSSA: dumpSSA, - difiles: make(map[string]llvm.Metadata), - ditypes: make(map[types.Type]llvm.Metadata), - machine: machine, - targetData: machine.CreateTargetData(), - functionInfos: map[*ssa.Function]functionInfo{}, - astComments: map[string]*ast.CommentGroup{}, - localTypeNames: map[*types.TypeName]string{}, + Config: config, + DumpSSA: dumpSSA, + difiles: make(map[string]llvm.Metadata), + ditypes: make(map[types.Type]llvm.Metadata), + machine: machine, + targetData: machine.CreateTargetData(), + functionInfos: map[*ssa.Function]functionInfo{}, + astComments: map[string]*ast.CommentGroup{}, } c.ctx = llvm.NewContext() diff --git a/compiler/interface.go b/compiler/interface.go index 83034ae56e..8d6e2a1d97 100644 --- a/compiler/interface.go +++ b/compiler/interface.go @@ -10,6 +10,7 @@ import ( "fmt" "go/token" "go/types" + "path/filepath" "sort" "strconv" "strings" @@ -583,8 +584,19 @@ var basicTypeNames = [...]string{ // // isLocal is true when the type is declared inside a function body. // Such types need a per-declaration (or per instantiation) suffix -// because their printed names are not unique; scanLocalTypes assigns -// the suffix and stores the result in c.localTypeNames. +// because their printed names are not unique. +// +// Ordinary function-local types (TypeName.Parent() != nil) are +// disambiguated lazily from their declaration position: every +// declaration in the package has a distinct (file, line, column) +// triple, and the position is taken un-//line-adjusted so it is +// stable across builds. Such types are only nameable inside their +// declaring package, so the name does not need to agree with anything +// computed in another package. +// +// Synthetic locals (TypeName.Parent() == nil), produced by generic +// instantiation, are pre-registered by scanLocalTypes because their +// names must agree across packages that materialize the same instance. func (c *compilerContext) getTypeCodeName(t types.Type) (name string, isLocal bool) { switch t := types.Unalias(t).(type) { case *types.Named: @@ -593,14 +605,19 @@ func (c *compilerContext) getTypeCodeName(t types.Type) (name string, isLocal bo // Package-scope or builtin: the printed name is unique. return "named:" + t.String(), false } - // Function-local type. Both ordinary locals (Parent() != nil) - // and synthetic locals from generic instantiation - // (Parent() == nil) are pre-registered by scanLocalTypes. - n, ok := c.localTypeNames[tn] - if !ok { - panic("compiler: local type " + tn.Name() + " was not registered by scanLocalTypes") + if tn.Parent() != nil { + // Ordinary function-local type. Use the un-//line-adjusted + // declaration position as the disambiguator. + pos := c.program.Fset.PositionFor(tn.Pos(), false) + return "named:" + t.String() + "$" + filepath.Base(pos.Filename) + ":" + strconv.Itoa(pos.Line) + ":" + strconv.Itoa(pos.Column), true } - return "named:" + n, true + // Synthetic local from generic instantiation: must have been + // pre-registered by scanLocalTypes. + v := c.localTypeNames.At(t) + if v == nil { + panic("compiler: synthetic local type " + tn.Name() + " was not registered by scanLocalTypes") + } + return "named:" + v.(string), true case *types.Array: s, isLocal := c.getTypeCodeName(t.Elem()) return "array:" + strconv.FormatInt(t.Len(), 10) + ":" + s, isLocal @@ -686,41 +703,40 @@ func (c *compilerContext) getTypeCodeName(t types.Type) (name string, isLocal bo } } -// scanLocalTypes assigns names to every function-local named type in -// the package and stores them in c.localTypeNames. Two flavors are -// handled: +// scanLocalTypes assigns names to every synthetic *types.TypeName +// (TypeName.Parent() == nil) reachable from this package and stores +// them in c.localTypeNames. // -// 1. Synthetic TypeNames produced by generic instantiation -// (TypeName.Parent() == nil). Two instantiations of the same -// generic function (e.g. F[int] and F[string]) produce TypeNames -// with the same printed name and the same source position, so -// each one is named with the enclosing instance's RelString as -// prefix. RelString encodes the type arguments, matching Go's -// runtime behavior, where F[int].Inner and F[string].Inner are -// distinct types even when Inner does not mention the type -// parameter. +// Synthetic TypeNames are produced by generic instantiation: two +// instantiations of the same generic function (e.g. F[int] and +// F[string]) produce TypeNames with the same printed name and the +// same source position, so each is named with the enclosing +// instance's RelString as prefix. RelString encodes the type +// arguments, matching Go's runtime behavior, where F[int].Inner and +// F[string].Inner are distinct types even when Inner does not mention +// the type parameter. // -// 2. Ordinary function-local TypeNames (TypeName.Parent() != nil). -// Each one is named with its declaring function's RelString plus -// a per-function counter assigned in source order. This mirrors -// the ·N suffix the standard Go compiler uses for such types and -// is robust against //line directives that would otherwise make a -// file:line:column suffix non-unique. +// A given instance may be materialized by several packages (the body +// of F[int] is compiled in every package that calls F[int]); its +// reflect/types.type:* global has LinkOnceODRLinkage and is merged by +// name at link time. The chosen name therefore depends only on +// intrinsic SSA properties (RelString and the raw token.Pos used as a +// sort key), so any package compiling the same instance produces the +// same identifier. // -// Names depend only on intrinsic SSA properties (RelString and the -// raw token.Pos used as a sort key), so any package compiling the -// same function or instance produces the same identifier. +// Ordinary function-local TypeNames (TypeName.Parent() != nil) are +// not handled here: they are nameable only inside their declaring +// package, and getTypeCodeName derives a stable per-declaration name +// for them directly from their source position. func (c *compilerContext) scanLocalTypes(ssaPkg *ssa.Package) { - // Pass 1: locate every generic instance reachable from this - // package (including instances declared in imported packages and - // any function reached through an instance subtree). Synthetic - // TypeNames are produced by instantiation, so we need the call - // graph to find them all. + // Locate every generic instance reachable from this package + // (including instances declared in imported packages and any + // function reached through an instance subtree). var instances []*ssa.Function - seenInstWalk := map[*ssa.Function]bool{} - var instWalk func(fn *ssa.Function, inInstance bool) - instWalk = func(fn *ssa.Function, inInstance bool) { - if fn == nil || seenInstWalk[fn] { + seen := map[*ssa.Function]bool{} + var walk func(fn *ssa.Function, inInstance bool) + walk = func(fn *ssa.Function, inInstance bool) { + if fn == nil || seen[fn] { return } // fn belongs to an instance subtree if it is itself an @@ -736,13 +752,13 @@ func (c *compilerContext) scanLocalTypes(ssaPkg *ssa.Package) { if fn.Blocks == nil && fn.AnonFuncs == nil { return } - seenInstWalk[fn] = true + seen[fn] = true isInInstance := inInstance || isInstanceRoot if isInInstance { instances = append(instances, fn) } for _, anon := range fn.AnonFuncs { - instWalk(anon, isInInstance) + walk(anon, isInInstance) } var ops [10]*ssa.Value for _, b := range fn.Blocks { @@ -752,7 +768,7 @@ func (c *compilerContext) scanLocalTypes(ssaPkg *ssa.Package) { continue } if callee, ok := (*op).(*ssa.Function); ok { - instWalk(callee, isInInstance) + walk(callee, isInInstance) } } } @@ -761,97 +777,46 @@ func (c *compilerContext) scanLocalTypes(ssaPkg *ssa.Package) { for _, member := range ssaPkg.Members { switch m := member.(type) { case *ssa.Function: - instWalk(m, false) + walk(m, false) case *ssa.Type: mset := c.program.MethodSets.MethodSet(m.Type()) for i := 0; i < mset.Len(); i++ { - instWalk(c.program.MethodValue(mset.At(i)), false) + walk(c.program.MethodValue(mset.At(i)), false) } pmset := c.program.MethodSets.MethodSet(types.NewPointer(m.Type())) for i := 0; i < pmset.Len(); i++ { - instWalk(c.program.MethodValue(pmset.At(i)), false) + walk(c.program.MethodValue(pmset.At(i)), false) } } } - // Pass 2: collect every non-instance function defined in this - // package together with its closures. Ordinary function-local - // TypeNames are scoped to their declaring function (and visible - // in nested closures only), so the declaring function is always - // somewhere in this lexical tree. Following callees here would - // be wrong: an instantiated function whose substituted signature - // mentions the local type would otherwise race with the actual - // declaring function for ownership. - var packageFuncs []*ssa.Function - var collect func(fn *ssa.Function) - collect = func(fn *ssa.Function) { - if fn == nil || fn.Pkg != ssaPkg { - return - } - if len(fn.TypeArgs()) > 0 { - // Generic instances are handled by pass 1 (their local - // types are synthetic). - return - } - if fn.Blocks == nil && fn.AnonFuncs == nil { - return - } - packageFuncs = append(packageFuncs, fn) - for _, anon := range fn.AnonFuncs { - collect(anon) - } - } - for _, member := range ssaPkg.Members { - switch m := member.(type) { - case *ssa.Function: - collect(m) - case *ssa.Type: - mset := c.program.MethodSets.MethodSet(m.Type()) - for i := 0; i < mset.Len(); i++ { - collect(c.program.MethodValue(mset.At(i))) - } - pmset := c.program.MethodSets.MethodSet(types.NewPointer(m.Type())) - for i := 0; i < pmset.Len(); i++ { - collect(c.program.MethodValue(pmset.At(i))) - } - } - } - - // Registration is first-writer-wins, so visit each list in a + // Registration is first-writer-wins (a synthetic TypeName may be + // reachable from several instances), so visit instances in a // deterministic order. Pos() is a defensive tiebreaker. - sortFns := func(fns []*ssa.Function) { - sort.Slice(fns, func(i, j int) bool { - ri, rj := fns[i].RelString(nil), fns[j].RelString(nil) - if ri != rj { - return ri < rj - } - return fns[i].Pos() < fns[j].Pos() - }) - } - sortFns(instances) - sortFns(packageFuncs) + sort.Slice(instances, func(i, j int) bool { + ri, rj := instances[i].RelString(nil), instances[j].RelString(nil) + if ri != rj { + return ri < rj + } + return instances[i].Pos() < instances[j].Pos() + }) for _, fn := range instances { - c.registerLocalTypes(fn, true) - } - for _, fn := range packageFuncs { - c.registerLocalTypes(fn, false) + c.registerSyntheticLocalTypes(fn) } } -// registerLocalTypes walks every type reachable from fn's body and -// records each function-local TypeName whose Parent() matches the -// synthetic flag (Parent() == nil for synthetic, != nil otherwise) in -// c.localTypeNames. Each TypeName is named with fn.RelString as the -// owning function plus a per-function counter assigned in source order. +// registerSyntheticLocalTypes walks every type reachable from fn's +// body and records each synthetic *types.Named (TypeName.Parent() == +// nil) in c.localTypeNames. Each is named with fn.RelString as the +// owning function plus a per-function counter assigned in source +// order. // -// First-writer-wins: a TypeName already present in c.localTypeNames -// is left alone. The slot is reserved with an empty string during -// collection so later registerLocalTypes calls (within the same -// scanLocalTypes invocation) skip it; the final name is filled in -// after sorting, before scanLocalTypes returns and any -// getTypeCodeName lookups happen. -func (c *compilerContext) registerLocalTypes(fn *ssa.Function, synthetic bool) { - var found []*types.TypeName +// First-writer-wins: a *types.Named already present in +// c.localTypeNames is left alone, so a synthetic type reachable from +// several instances keeps the name assigned by the first (in +// scanLocalTypes' deterministic order). +func (c *compilerContext) registerSyntheticLocalTypes(fn *ssa.Function) { + var found []*types.Named seen := map[types.Type]bool{} var visit func(t types.Type) visit = func(t types.Type) { @@ -864,10 +829,14 @@ func (c *compilerContext) registerLocalTypes(fn *ssa.Function, synthetic bool) { visit(types.Unalias(t)) case *types.Named: tn := t.Obj() - if tn.Pkg() != nil && (tn.Parent() == nil) == synthetic { - if _, ok := c.localTypeNames[tn]; !ok { - c.localTypeNames[tn] = "" - found = append(found, tn) + if tn.Pkg() != nil && tn.Parent() == nil { + if c.localTypeNames.At(t) == nil { + // Reserve the slot so later calls within this + // scanLocalTypes invocation skip it; the final + // name is filled in after sorting, before any + // getTypeCodeName lookups happen. + c.localTypeNames.Set(t, "") + found = append(found, t) } } targs := t.TypeArgs() @@ -906,10 +875,10 @@ func (c *compilerContext) registerLocalTypes(fn *ssa.Function, synthetic bool) { visit(t.At(i).Type()) } case *types.Interface: - // A local type can be reachable only through a local - // interface's method signature, so descend into them. - // getTypeCodeName encodes those signatures into the - // interface's identifier, and the seen map breaks + // A synthetic local type can be reachable only through a + // local interface's method signature, so descend into + // them. getTypeCodeName encodes those signatures into + // the interface's identifier, and the seen map breaks // cycles formed by methods that mention the interface // itself. for i := 0; i < t.NumMethods(); i++ { @@ -946,11 +915,11 @@ func (c *compilerContext) registerLocalTypes(fn *ssa.Function, synthetic bool) { // that is stable across builds and unaffected by //line directives // (which only adjust the human-facing position from Fset.Position). sort.Slice(found, func(i, j int) bool { - return found[i].Pos() < found[j].Pos() + return found[i].Obj().Pos() < found[j].Obj().Pos() }) enclosing := fn.RelString(nil) - for i, tn := range found { - c.localTypeNames[tn] = enclosing + "." + tn.Name() + "$" + strconv.Itoa(i+1) + for i, named := range found { + c.localTypeNames.Set(named, enclosing+"."+named.Obj().Name()+"$"+strconv.Itoa(i+1)) } }