From 1d4e210256ec89fd5625e1b06abb9517f1316b93 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:10:52 -0500 Subject: [PATCH 1/3] compiler: promoted unexported methods are named by their package A type can hold two unexported methods of one name, its own and one promoted from an embedded type in another package, since Go names an unexported method by its declaring package. RelString spells both the same, so they shared one symbol: a call through the other package's interface ran the type's own method, and gvisor's tcp.Endpoint, which has beforeSave this way, was reported as redeclared. The promoted one now carries its package. Fixes #5845 --- build_test.go | 1 + compiler/symbol.go | 35 ++++++++++++++++++++++++++------ testdata/promoted.go | 16 +++++++++++++++ testdata/promoted.txt | 2 ++ testdata/promoted/inner/inner.go | 10 +++++++++ 5 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 testdata/promoted.go create mode 100644 testdata/promoted.txt create mode 100644 testdata/promoted/inner/inner.go diff --git a/build_test.go b/build_test.go index 4ba65c2e4d..628144751a 100644 --- a/build_test.go +++ b/build_test.go @@ -51,6 +51,7 @@ func TestBuild(t *testing.T) { "math.go", "oldgo/", "print.go", + "promoted.go", "reflect.go", "signal.go", "signalnotify.go", diff --git a/compiler/symbol.go b/compiler/symbol.go index d3a80b47de..ad9f9ea194 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -365,14 +365,37 @@ func (c *compilerContext) getFunctionInfo(f *ssa.Function) functionInfo { func (c *compilerContext) canonicalFunctionName(f *ssa.Function) string { typeArgs := f.TypeArgs() - if len(typeArgs) == 0 { - return f.RelString(nil) + name := f.RelString(nil) + if len(typeArgs) != 0 { + parts := make([]string, len(typeArgs)) + for i, ta := range typeArgs { + parts[i], _ = c.getTypeCodeName(ta) + } + name = f.Origin().RelString(nil) + "[" + strings.Join(parts, ",") + "]" + } + return name + methodPackageQualifier(f) +} + +// methodPackageQualifier names an unexported method promoted from another +// package by that package, as RelString spells it like the type's own method. +func methodPackageQualifier(f *ssa.Function) string { + recv := f.Signature.Recv() + if recv == nil { + return "" + } + obj, ok := f.Object().(*types.Func) + if !ok || obj == nil || obj.Exported() || obj.Pkg() == nil { + return "" + } + t := recv.Type() + if ptr, ok := t.(*types.Pointer); ok { + t = ptr.Elem() } - parts := make([]string, len(typeArgs)) - for i, ta := range typeArgs { - parts[i], _ = c.getTypeCodeName(ta) + named, ok := types.Unalias(t).(*types.Named) + if !ok || named.Obj().Pkg() == nil || named.Obj().Pkg() == obj.Pkg() { + return "" } - return f.Origin().RelString(nil) + "[" + strings.Join(parts, ",") + "]" + return "·" + obj.Pkg().Path() } // parsePragmas is used by getFunctionInfo to parse function pragmas such as diff --git a/testdata/promoted.go b/testdata/promoted.go new file mode 100644 index 0000000000..85c6196040 --- /dev/null +++ b/testdata/promoted.go @@ -0,0 +1,16 @@ +package main + +import "github.com/tinygo-org/tinygo/testdata/promoted/inner" + +// Outer has its own unexported value method and also the one promoted from +// inner.Inner, which Go treats as a different method. +type Outer struct { + inner.Inner +} + +func (Outer) value() int { return 2 } + +func main() { + println("own:", Outer{}.value()) + println("promoted:", inner.Value(Outer{})) +} diff --git a/testdata/promoted.txt b/testdata/promoted.txt new file mode 100644 index 0000000000..ec19fbaca2 --- /dev/null +++ b/testdata/promoted.txt @@ -0,0 +1,2 @@ +own: 2 +promoted: 1 diff --git a/testdata/promoted/inner/inner.go b/testdata/promoted/inner/inner.go new file mode 100644 index 0000000000..f8c7539ee9 --- /dev/null +++ b/testdata/promoted/inner/inner.go @@ -0,0 +1,10 @@ +package inner + +type Inner struct{} + +func (Inner) value() int { return 1 } + +type valuer interface{ value() int } + +// Value calls the value method of package inner through an interface. +func Value(v any) int { return v.(valuer).value() } From 7051b2fdb049366dadcb9594acb4c0b1bc8472f3 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:03:38 -0500 Subject: [PATCH 2/3] compiler: qualify promoted methods with $ and test a pointer receiver Use $ before the package path, like the other synthetic names, so that symbol names stay ASCII. Reword the comment and add a pointer receiver case to the test. --- compiler/symbol.go | 6 +++--- testdata/promoted.go | 8 ++++++-- testdata/promoted.txt | 2 ++ testdata/promoted/inner/inner.go | 7 +++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/compiler/symbol.go b/compiler/symbol.go index ad9f9ea194..e1b2bb7389 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -376,8 +376,8 @@ func (c *compilerContext) canonicalFunctionName(f *ssa.Function) string { return name + methodPackageQualifier(f) } -// methodPackageQualifier names an unexported method promoted from another -// package by that package, as RelString spells it like the type's own method. +// methodPackageQualifier adds the declaring package to an unexported method +// promoted from another package, since RelString names it like the type's own. func methodPackageQualifier(f *ssa.Function) string { recv := f.Signature.Recv() if recv == nil { @@ -395,7 +395,7 @@ func methodPackageQualifier(f *ssa.Function) string { if !ok || named.Obj().Pkg() == nil || named.Obj().Pkg() == obj.Pkg() { return "" } - return "·" + obj.Pkg().Path() + return "$" + obj.Pkg().Path() } // parsePragmas is used by getFunctionInfo to parse function pragmas such as diff --git a/testdata/promoted.go b/testdata/promoted.go index 85c6196040..5023dc18c6 100644 --- a/testdata/promoted.go +++ b/testdata/promoted.go @@ -2,15 +2,19 @@ package main import "github.com/tinygo-org/tinygo/testdata/promoted/inner" -// Outer has its own unexported value method and also the one promoted from -// inner.Inner, which Go treats as a different method. +// Outer has its own unexported methods and also the ones promoted from +// inner.Inner, which Go treats as different methods. type Outer struct { inner.Inner } func (Outer) value() int { return 2 } +func (*Outer) ptr() int { return 2 } + func main() { println("own:", Outer{}.value()) println("promoted:", inner.Value(Outer{})) + println("own ptr:", (&Outer{}).ptr()) + println("promoted ptr:", inner.Ptr(&Outer{})) } diff --git a/testdata/promoted.txt b/testdata/promoted.txt index ec19fbaca2..78e2c74d67 100644 --- a/testdata/promoted.txt +++ b/testdata/promoted.txt @@ -1,2 +1,4 @@ own: 2 promoted: 1 +own ptr: 2 +promoted ptr: 1 diff --git a/testdata/promoted/inner/inner.go b/testdata/promoted/inner/inner.go index f8c7539ee9..5ff5be1f3e 100644 --- a/testdata/promoted/inner/inner.go +++ b/testdata/promoted/inner/inner.go @@ -4,7 +4,14 @@ type Inner struct{} func (Inner) value() int { return 1 } +func (*Inner) ptr() int { return 1 } + type valuer interface{ value() int } +type ptrer interface{ ptr() int } + // Value calls the value method of package inner through an interface. func Value(v any) int { return v.(valuer).value() } + +// Ptr calls the pointer method of package inner through an interface. +func Ptr(v any) int { return v.(ptrer).ptr() } From bc7d24f137be1debd7f1a377cc44fd46345d6e7a Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:47:19 -0500 Subject: [PATCH 3/3] compiler: compare package paths for promoted methods, and test more embeddings Comparing *types.Package pointers relied on every package being type checked with one importer. The tests from dgryski cover embedding through a pointer, two levels, two packages with the same unexported method name, and a generic outer type. --- compiler/symbol.go | 2 +- testdata/promoted.go | 47 +++++++++++++++++++++++++++++- testdata/promoted.txt | 11 +++++++ testdata/promoted/inner2/inner2.go | 9 ++++++ 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 testdata/promoted/inner2/inner2.go diff --git a/compiler/symbol.go b/compiler/symbol.go index e1b2bb7389..f3f6799a33 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -392,7 +392,7 @@ func methodPackageQualifier(f *ssa.Function) string { t = ptr.Elem() } named, ok := types.Unalias(t).(*types.Named) - if !ok || named.Obj().Pkg() == nil || named.Obj().Pkg() == obj.Pkg() { + if !ok || named.Obj().Pkg() == nil || named.Obj().Pkg().Path() == obj.Pkg().Path() { return "" } return "$" + obj.Pkg().Path() diff --git a/testdata/promoted.go b/testdata/promoted.go index 5023dc18c6..63680b6053 100644 --- a/testdata/promoted.go +++ b/testdata/promoted.go @@ -1,6 +1,9 @@ package main -import "github.com/tinygo-org/tinygo/testdata/promoted/inner" +import ( + "github.com/tinygo-org/tinygo/testdata/promoted/inner" + "github.com/tinygo-org/tinygo/testdata/promoted/inner2" +) // Outer has its own unexported methods and also the ones promoted from // inner.Inner, which Go treats as different methods. @@ -12,9 +15,51 @@ func (Outer) value() int { return 2 } func (*Outer) ptr() int { return 2 } +type PtrOuter struct { + *inner.Inner +} + +func (PtrOuter) value() int { return 2 } + +func (PtrOuter) ptr() int { return 2 } + +type Outer2 struct { + Outer +} + +type Both struct { + inner.Inner + inner2.Inner2 +} + +type G[T any] struct { + inner.Inner + v T +} + +func (G[T]) value() int { return 2 } + +type mainValuer interface{ value() int } + func main() { println("own:", Outer{}.value()) println("promoted:", inner.Value(Outer{})) println("own ptr:", (&Outer{}).ptr()) println("promoted ptr:", inner.Ptr(&Outer{})) + + po := PtrOuter{&inner.Inner{}} + println("embedded ptr own:", po.value()) + println("embedded ptr promoted:", inner.Value(po)) + println("embedded ptr promoted ptr:", inner.Ptr(po)) + + println("two levels main:", any(Outer2{}).(mainValuer).value()) + println("two levels inner:", inner.Value(Outer2{})) + println("two levels inner ptr:", inner.Ptr(&Outer2{})) + + println("both inner:", inner.Value(Both{})) + println("both inner2:", inner2.Value(Both{})) + + println("generic own:", G[int]{}.value()) + println("generic promoted:", inner.Value(G[int]{})) + println("generic string promoted:", inner.Value(G[string]{})) } diff --git a/testdata/promoted.txt b/testdata/promoted.txt index 78e2c74d67..5c368cc3eb 100644 --- a/testdata/promoted.txt +++ b/testdata/promoted.txt @@ -2,3 +2,14 @@ own: 2 promoted: 1 own ptr: 2 promoted ptr: 1 +embedded ptr own: 2 +embedded ptr promoted: 1 +embedded ptr promoted ptr: 1 +two levels main: 2 +two levels inner: 1 +two levels inner ptr: 1 +both inner: 1 +both inner2: 3 +generic own: 2 +generic promoted: 1 +generic string promoted: 1 diff --git a/testdata/promoted/inner2/inner2.go b/testdata/promoted/inner2/inner2.go new file mode 100644 index 0000000000..b80af03638 --- /dev/null +++ b/testdata/promoted/inner2/inner2.go @@ -0,0 +1,9 @@ +package inner2 + +type Inner2 struct{} + +func (Inner2) value() int { return 3 } + +type valuer interface{ value() int } + +func Value(v any) int { return v.(valuer).value() }