From 6fb5ea717e7d0e90f09ae0e06edc0ab5a031780e Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 28 May 2026 00:02:13 +0800 Subject: [PATCH 1/9] test(gnovm): add failing filetest alias2 --- gnovm/tests/files/alias2.gno | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 gnovm/tests/files/alias2.gno diff --git a/gnovm/tests/files/alias2.gno b/gnovm/tests/files/alias2.gno new file mode 100644 index 00000000000..2e112b7aa75 --- /dev/null +++ b/gnovm/tests/files/alias2.gno @@ -0,0 +1,27 @@ +package main + +type Int = int + +type A = struct{ int } +type B = struct{ Int } + +func main() { + var x, y interface{} = A{}, B{} + if x == y { + mustNot() + } + + { + type C = int32 + x = struct{ C }{} + } + { + type C = uint32 + y = struct{ C }{} + } + if x == y { + mustNot() + } +} +func mustNot() { panic(badMsg) } +const badMsg = "FAIL" From ce32d098caaa1e919aca8ffa267c1dd9c13c1f6d Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 28 May 2026 00:18:52 +0800 Subject: [PATCH 2/9] fix(gnovm): preserve embedded alias name as struct field name --- gnovm/pkg/gnolang/op_types.go | 6 ++--- gnovm/pkg/gnolang/preprocess.go | 2 +- gnovm/pkg/gnolang/types.go | 41 ++++++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/gnovm/pkg/gnolang/op_types.go b/gnovm/pkg/gnolang/op_types.go index b451fd10f83..ecd9489b313 100644 --- a/gnovm/pkg/gnolang/op_types.go +++ b/gnovm/pkg/gnolang/op_types.go @@ -116,9 +116,9 @@ func (m *Machine) doOpStructType() { // allocate (minimum) space for fields fields := make([]FieldType, 0, len(x.Fields)) // populate fields - for _, ftv := range ftvs { + for i, ftv := range ftvs { ft := ftv.V.(TypeValue).Type.(FieldType) - fillEmbeddedName(&ft) + fillEmbeddedName(&ft, x.Fields[i].Type) fields = append(fields, ft) } // push struct type @@ -142,7 +142,7 @@ func (m *Machine) doOpInterfaceType() { // pop methods for i := len(x.Methods) - 1; 0 <= i; i-- { ft := m.PopValue().V.(TypeValue).Type.(FieldType) - fillEmbeddedName(&ft) + fillEmbeddedName(&ft, x.Methods[i].Type) methods[i] = ft } // push interface type diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 3963642df6e..623e7bd5432 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -4164,7 +4164,7 @@ func buildFieldTypesAST(store Store, last BlockNode, fxs FieldTypeExprs, embed b ft.Tag = Tag(evalConst(store, last, fx.Tag).GetString()) } if embed { - fillEmbeddedName(&ft) + fillEmbeddedName(&ft, fx.Type) } fts[i] = ft } diff --git a/gnovm/pkg/gnolang/types.go b/gnovm/pkg/gnolang/types.go index 6b7e89491da..7b20ddba1f3 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -2637,10 +2637,15 @@ func defaultTypeOf(t Type) Type { } } -func fillEmbeddedName(ft *FieldType) { +func fillEmbeddedName(ft *FieldType, nameSrc ...Expr) { if ft.Name != "" { return } + if n := pickEmbedName(nameSrc); n != "" { + ft.Name = n + ft.Embedded = true + return + } switch ct := ft.Type.(type) { case *PointerType: // dereference one level @@ -2692,6 +2697,40 @@ func fillEmbeddedName(ft *FieldType) { ft.Embedded = true } +func pickEmbedName(xs []Expr) Name { + var zero Name + if len(xs) == 0 { + return zero + } + x := xs[0] + for { + switch e := x.(type) { + case *constTypeExpr: + if e.Source == nil { + return zero + } + x = e.Source + case *ConstExpr: + if e.Source == nil { + return zero + } + x = e.Source + case *StarExpr: + x = e.X + default: + goto done + } + } +done: + switch e := x.(type) { + case *NameExpr: + return e.Name + case *SelectorExpr: + return e.Sel + } + return zero +} + func IsImplementedBy(it Type, ot Type) bool { switch cbt := baseOf(it).(type) { case *InterfaceType: From 813ad3b39578a6f3ca752c65b8f798babc251002 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Sat, 13 Jun 2026 00:20:33 +0800 Subject: [PATCH 3/9] refactor(gnovm): reuse unconst in pickEmbedName --- gnovm/pkg/gnolang/types.go | 126 ++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 72 deletions(-) diff --git a/gnovm/pkg/gnolang/types.go b/gnovm/pkg/gnolang/types.go index 7b20ddba1f3..521104b2925 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -2637,98 +2637,80 @@ func defaultTypeOf(t Type) Type { } } -func fillEmbeddedName(ft *FieldType, nameSrc ...Expr) { +func fillEmbeddedName(ft *FieldType, nameSrc Expr) { if ft.Name != "" { return } if n := pickEmbedName(nameSrc); n != "" { ft.Name = n - ft.Embedded = true - return - } - switch ct := ft.Type.(type) { - case *PointerType: - // dereference one level - switch ct := ct.Elt.(type) { + } else { + switch ct := ft.Type.(type) { + case *PointerType: + // dereference one level + switch ct := ct.Elt.(type) { + case *DeclaredType: + ft.Name = ct.Name + default: + // should not happen, + panic("should not happen") + } case *DeclaredType: ft.Name = ct.Name + case PrimitiveType: + switch ct { + case BoolType: + ft.Name = Name("bool") + case StringType: + ft.Name = Name("string") + case IntType: + ft.Name = Name("int") + case Int8Type: + ft.Name = Name("int8") + case Int16Type: + ft.Name = Name("int16") + case Int32Type: + ft.Name = Name("int32") + case Int64Type: + ft.Name = Name("int64") + case UintType: + ft.Name = Name("uint") + case Uint8Type: + ft.Name = Name("uint8") + case Uint16Type: + ft.Name = Name("uint16") + case Uint32Type: + ft.Name = Name("uint32") + case Uint64Type: + ft.Name = Name("uint64") + case Float32Type: + ft.Name = Name("float32") + case Float64Type: + ft.Name = Name("float64") + } default: - // should not happen, - panic("should not happen") - } - case *DeclaredType: - ft.Name = ct.Name - case PrimitiveType: - switch ct { - case BoolType: - ft.Name = Name("bool") - case StringType: - ft.Name = Name("string") - case IntType: - ft.Name = Name("int") - case Int8Type: - ft.Name = Name("int8") - case Int16Type: - ft.Name = Name("int16") - case Int32Type: - ft.Name = Name("int32") - case Int64Type: - ft.Name = Name("int64") - case UintType: - ft.Name = Name("uint") - case Uint8Type: - ft.Name = Name("uint8") - case Uint16Type: - ft.Name = Name("uint16") - case Uint32Type: - ft.Name = Name("uint32") - case Uint64Type: - ft.Name = Name("uint64") - case Float32Type: - ft.Name = Name("float32") - case Float64Type: - ft.Name = Name("float64") + panic(fmt.Sprintf( + "unexpected field type %s", + ft.Type.String())) } - default: - panic(fmt.Sprintf( - "unexpected field type %s", - ft.Type.String())) } ft.Embedded = true } -func pickEmbedName(xs []Expr) Name { - var zero Name - if len(xs) == 0 { - return zero - } - x := xs[0] +func pickEmbedName(x Expr) Name { for { switch e := x.(type) { - case *constTypeExpr: - if e.Source == nil { - return zero - } - x = e.Source - case *ConstExpr: - if e.Source == nil { - return zero - } - x = e.Source + case *constTypeExpr, *ConstExpr: + x = unconst(x) case *StarExpr: x = e.X + case *NameExpr: + return e.Name + case *SelectorExpr: + return e.Sel default: - goto done + return "" } } -done: - switch e := x.(type) { - case *NameExpr: - return e.Name - case *SelectorExpr: - return e.Sel - } - return zero } func IsImplementedBy(it Type, ot Type) bool { From d0a7b5e61f4eefdf1eab7b0579fb7a8de0bb4576 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Sat, 13 Jun 2026 00:20:33 +0800 Subject: [PATCH 4/9] test(gnovm): cover selector, pointer, and interface alias embeds --- gnovm/tests/files/alias3.gno | 47 +++++++++++++++++++ .../tests/files/extern/aliasdef/aliasdef.gno | 3 ++ 2 files changed, 50 insertions(+) create mode 100644 gnovm/tests/files/alias3.gno create mode 100644 gnovm/tests/files/extern/aliasdef/aliasdef.gno diff --git a/gnovm/tests/files/alias3.gno b/gnovm/tests/files/alias3.gno new file mode 100644 index 00000000000..107377fff65 --- /dev/null +++ b/gnovm/tests/files/alias3.gno @@ -0,0 +1,47 @@ +package main + +import "filetests/extern/aliasdef" + +type Int = int + +type B = struct{ Int } + +type Stringer interface{ Str() string } +type SAlias = Stringer +type I interface{ SAlias } + +type T struct{} + +func (T) Str() string { return "ok" } + +func main() { + // a qualified alias embeds as its own name, so struct{aliasdef.Int} + // is identical to struct{Int} + var x, y interface{} = B{}, struct{ aliasdef.Int }{} + if x != y { + panic("FAIL: struct{Int} != struct{aliasdef.Int}") + } + + // pointer embeds also use the alias name + x, y = struct{ *Int }{}, struct{ *int }{} + if x == y { + panic("FAIL: struct{*Int} == struct{*int}") + } + + // the embedded field is accessible by the alias name + b := B{7} + b.Int++ + println(b.Int) + + // embedding an alias in an interface still flattens the method set + var v interface{} = T{} + s, ok := v.(I) + if !ok { + panic("FAIL: T does not implement interface{SAlias}") + } + println(s.Str()) +} + +// Output: +// 8 +// ok diff --git a/gnovm/tests/files/extern/aliasdef/aliasdef.gno b/gnovm/tests/files/extern/aliasdef/aliasdef.gno new file mode 100644 index 00000000000..6be46f71936 --- /dev/null +++ b/gnovm/tests/files/extern/aliasdef/aliasdef.gno @@ -0,0 +1,3 @@ +package aliasdef + +type Int = int From fbfbab4a8cb7c0ca2a1c3c819a9f351fceda8310 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Wed, 17 Jun 2026 21:02:24 +0800 Subject: [PATCH 5/9] refactor(gnovm): inline pickEmbedName, drop dead embed-name fallback --- gnovm/pkg/gnolang/types.go | 75 ++++++++------------------------------ 1 file changed, 15 insertions(+), 60 deletions(-) diff --git a/gnovm/pkg/gnolang/types.go b/gnovm/pkg/gnolang/types.go index 521104b2925..210e9848700 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -2641,76 +2641,31 @@ func fillEmbeddedName(ft *FieldType, nameSrc Expr) { if ft.Name != "" { return } - if n := pickEmbedName(nameSrc); n != "" { - ft.Name = n - } else { - switch ct := ft.Type.(type) { - case *PointerType: - // dereference one level - switch ct := ct.Elt.(type) { - case *DeclaredType: - ft.Name = ct.Name - default: - // should not happen, - panic("should not happen") - } - case *DeclaredType: - ft.Name = ct.Name - case PrimitiveType: - switch ct { - case BoolType: - ft.Name = Name("bool") - case StringType: - ft.Name = Name("string") - case IntType: - ft.Name = Name("int") - case Int8Type: - ft.Name = Name("int8") - case Int16Type: - ft.Name = Name("int16") - case Int32Type: - ft.Name = Name("int32") - case Int64Type: - ft.Name = Name("int64") - case UintType: - ft.Name = Name("uint") - case Uint8Type: - ft.Name = Name("uint8") - case Uint16Type: - ft.Name = Name("uint16") - case Uint32Type: - ft.Name = Name("uint32") - case Uint64Type: - ft.Name = Name("uint64") - case Float32Type: - ft.Name = Name("float32") - case Float64Type: - ft.Name = Name("float64") - } - default: - panic(fmt.Sprintf( - "unexpected field type %s", - ft.Type.String())) - } - } - ft.Embedded = true -} - -func pickEmbedName(x Expr) Name { + // Derive the field name from the source expr, not ft.Type: an alias + // (type Int = int) resolves away, so ft.Type would yield "int" instead + // of the written "Int". An embedded field is always a (qualified/pointer) + // type name, so unwrapping const/pointer layers lands on the NameExpr or + // SelectorExpr that carries the name as written. + x := nameSrc for { switch e := x.(type) { case *constTypeExpr, *ConstExpr: x = unconst(x) + continue case *StarExpr: x = e.X + continue case *NameExpr: - return e.Name + ft.Name = e.Name case *SelectorExpr: - return e.Sel - default: - return "" + ft.Name = e.Sel } + break } + if ft.Name == "" { + panic(fmt.Sprintf("cannot derive embedded name for field type %s", ft.Type.String())) + } + ft.Embedded = true } func IsImplementedBy(it Type, ot Type) bool { From bd22a3ce4f4a95fd04a02595ec8ffb97a2be6897 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Wed, 17 Jun 2026 21:02:24 +0800 Subject: [PATCH 6/9] test(gnovm): cover alias-to-defined-type embedded field name --- gnovm/tests/files/alias4.gno | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 gnovm/tests/files/alias4.gno diff --git a/gnovm/tests/files/alias4.gno b/gnovm/tests/files/alias4.gno new file mode 100644 index 00000000000..7f1959ca927 --- /dev/null +++ b/gnovm/tests/files/alias4.gno @@ -0,0 +1,30 @@ +package main + +type T struct{ V int } + +type C = T // alias to a defined type + +type Named struct{ V int } // distinct defined type, same shape as T + +func main() { + // Embedding the alias C names the field "C", embedding T names it "T", + // so the two structs are distinct even though C and T are the same type. + var x, y interface{} = struct{ C }{}, struct{ T }{} + if x == y { + panic("FAIL: struct{C} == struct{T}") + } + + // And distinct from embedding an unrelated defined type of the same shape. + x, y = struct{ C }{}, struct{ Named }{} + if x == y { + panic("FAIL: struct{C} == struct{Named}") + } + + // The embedded field is accessible by the alias name C, not T. + s := struct{ C }{} + s.C.V = 9 + println(s.C.V) +} + +// Output: +// 9 From 155f1a7efd29e38936ca60f00aaab53e0e47e854 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Wed, 17 Jun 2026 21:12:05 +0800 Subject: [PATCH 7/9] docs(gnovm): explain what each alias-embed filetest guards --- gnovm/tests/files/alias2.gno | 4 ++++ gnovm/tests/files/alias3.gno | 5 +++++ gnovm/tests/files/alias4.gno | 2 ++ 3 files changed, 11 insertions(+) diff --git a/gnovm/tests/files/alias2.gno b/gnovm/tests/files/alias2.gno index 2e112b7aa75..63f8be15bef 100644 --- a/gnovm/tests/files/alias2.gno +++ b/gnovm/tests/files/alias2.gno @@ -1,3 +1,7 @@ +// A plain (unqualified) embedded type alias keeps the alias name as the field +// name: struct{Int} stays distinct from struct{int}, and two same-named aliases +// of different underlying types stay distinct too. (alias3 covers the qualified +// selector form pkg.T, plus pointer and interface embeds.) package main type Int = int diff --git a/gnovm/tests/files/alias3.gno b/gnovm/tests/files/alias3.gno index 107377fff65..c4dc47aaca8 100644 --- a/gnovm/tests/files/alias3.gno +++ b/gnovm/tests/files/alias3.gno @@ -1,3 +1,8 @@ +// An embedded type alias takes its field name from the name as written, not +// from the resolved underlying type, across all embed forms: the qualified +// selector pkg.T (named after T, the selector's final segment), pointer *T, +// and interface embeds. Otherwise distinct alias embeds collapse to equal +// TypeIDs. package main import "filetests/extern/aliasdef" diff --git a/gnovm/tests/files/alias4.gno b/gnovm/tests/files/alias4.gno index 7f1959ca927..64e2b9af14e 100644 --- a/gnovm/tests/files/alias4.gno +++ b/gnovm/tests/files/alias4.gno @@ -1,3 +1,5 @@ +// An alias to a defined type (type C = T) embeds under the alias name "C", +// not the target type's name "T", so struct{C} and struct{T} are distinct. package main type T struct{ V int } From 589ab871835fb91596dbb662ca08f8b1e12b9768 Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 18 Jun 2026 13:43:06 +0800 Subject: [PATCH 8/9] fix(gnovm): name embedded interfaces from resolved type, not alias spelling Interface identity is the method set, so interface{ SAlias } must equal interface{ Stringer }; only struct embeds take the written name. Adds alias5 regression test. interface{ Stringer } != interface{ Str()string } stays (pre-existing; needs method-set flattening, see TODO). --- gnovm/pkg/gnolang/op_types.go | 4 +++- gnovm/pkg/gnolang/preprocess.go | 14 +++++++++++--- gnovm/pkg/gnolang/types.go | 30 ++++++++++++++++++++++++++++++ gnovm/tests/files/alias5.gno | 23 +++++++++++++++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 gnovm/tests/files/alias5.gno diff --git a/gnovm/pkg/gnolang/op_types.go b/gnovm/pkg/gnolang/op_types.go index ecd9489b313..784f5d95475 100644 --- a/gnovm/pkg/gnolang/op_types.go +++ b/gnovm/pkg/gnolang/op_types.go @@ -142,7 +142,9 @@ func (m *Machine) doOpInterfaceType() { // pop methods for i := len(x.Methods) - 1; 0 <= i; i-- { ft := m.PopValue().V.(TypeValue).Type.(FieldType) - fillEmbeddedName(&ft, x.Methods[i].Type) + // embedded interfaces are named from the resolved type, not the + // written spelling, so an alias and its target stay identical. + fillEmbeddedInterfaceName(&ft) methods[i] = ft } // push interface type diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 623e7bd5432..a747bd65269 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -4136,9 +4136,16 @@ func staticTypeFromAST(store Store, last BlockNode, x Expr) (Type, bool) { validateStructFields(st, "") return st, true case *InterfaceTypeExpr: + // Build without struct-style embed naming (embed=false), then name + // embedded interfaces from the resolved type rather than the written + // spelling, so an alias and its target collapse to one identity. + methods := buildFieldTypesAST(store, last, x.Methods, false) + for i := range methods { + fillEmbeddedInterfaceName(&methods[i]) + } it := &InterfaceType{ PkgPath: packageOf(last).PkgPath, - Methods: buildFieldTypesAST(store, last, x.Methods, true), + Methods: methods, Generic: x.Generic, } validateEmbedDepth(it, "") @@ -4150,8 +4157,9 @@ func staticTypeFromAST(store Store, last BlockNode, x Expr) (Type, bool) { // buildFieldTypesAST constructs a []FieldType directly from FieldTypeExprs, // mirroring doOpFieldType + doOp{Struct,Interface,Func}Type aggregation. -// embed=true mirrors doOpStructType/doOpInterfaceType which call -// fillEmbeddedName per field; embed=false mirrors doOpFuncType which does not. +// embed=true mirrors doOpStructType, which calls fillEmbeddedName per field; +// embed=false mirrors doOpFuncType (and the interface path, which builds with +// embed=false and then names embeds via fillEmbeddedInterfaceName). func buildFieldTypesAST(store Store, last BlockNode, fxs FieldTypeExprs, embed bool) []FieldType { fts := make([]FieldType, len(fxs)) for i := range fxs { diff --git a/gnovm/pkg/gnolang/types.go b/gnovm/pkg/gnolang/types.go index 210e9848700..6811397e770 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -2668,6 +2668,36 @@ func fillEmbeddedName(ft *FieldType, nameSrc Expr) { ft.Embedded = true } +// fillEmbeddedInterfaceName names an embedded-interface field from its +// resolved type, NOT from the source spelling. This is deliberately the +// opposite of fillEmbeddedName (used for struct embeds): Go computes +// interface identity from the flattened method set, so an embedded alias and +// its target must collapse to one identity — interface{ SAlias } (where +// type SAlias = Stringer) must equal interface{ Stringer }. Deriving the +// name from the resolved *DeclaredType keeps both named "Stringer", matching +// pre-#5739 behavior and Go. +// +// TODO(gnovm): this only equalizes alias-vs-target. GnoVM still stores an +// embedded interface as a single named method entry instead of flattening +// its method set, so structurally identical interfaces with different +// spellings still diverge from Go (e.g. interface{ Stringer } != +// interface{ Str() string }, which is also broken on master). The complete +// fix flattens embedded method sets at construction, dropping the embed name +// from the TypeID entirely; see the follow-up branch scratch/iface-flatten. +// Embedding an alias to an *anonymous* interface still panics here, as it +// did pre-#5739 — the flattening follow-up also resolves that. +func fillEmbeddedInterfaceName(ft *FieldType) { + if ft.Name != "" { + return + } + dt, ok := ft.Type.(*DeclaredType) + if !ok { + panic(fmt.Sprintf("cannot derive embedded interface name for type %s", ft.Type.String())) + } + ft.Name = dt.Name + ft.Embedded = true +} + func IsImplementedBy(it Type, ot Type) bool { switch cbt := baseOf(it).(type) { case *InterfaceType: diff --git a/gnovm/tests/files/alias5.gno b/gnovm/tests/files/alias5.gno new file mode 100644 index 00000000000..1ed18fcfade --- /dev/null +++ b/gnovm/tests/files/alias5.gno @@ -0,0 +1,23 @@ +// An embedded interface is named from its resolved type, not the written +// spelling, so embedding an alias is identical to embedding its target: +// interface{ SAlias } == interface{ Stringer }. This is the inverse of the +// struct rule (alias2-4), because Go computes interface identity from the +// method set, not the embedded name. Guards against the regression where +// alias-named embeds got distinct TypeIDs. +// +// NOTE: this only equalizes alias-vs-target. Structurally identical +// interfaces with different spellings still diverge (interface{ Stringer } +// != interface{ Str() string }); fixing that needs method-set flattening +// (follow-up, branch scratch/iface-flatten). +package main + +type Stringer interface{ Str() string } +type SAlias = Stringer + +func main() { + var x, y interface{} = struct{ A interface{ SAlias } }{}, struct{ A interface{ Stringer } }{} + println("alias==canonical:", x == y) +} + +// Output: +// alias==canonical: true From 6426f3dea70254965311d82aaa9ca406533b92dd Mon Sep 17 00:00:00 2001 From: ltzMaxwell Date: Thu, 18 Jun 2026 13:49:17 +0800 Subject: [PATCH 9/9] fix(gnovm): flatten embedded interface method sets for type identity Replaces #5739's minimal interface-embed fix: instead of naming embeds from the resolved type, flatten embedded interfaces into their method set at construction, so the embed name leaves the TypeID entirely. interface{Stringer} == interface{Str()string} now holds too, not just alias-vs-target. Consensus-breaking (anonymous-interface TypeIDs change); land with a chain upgrade. --- gnovm/pkg/gnolang/op_types.go | 6 +-- gnovm/pkg/gnolang/preprocess.go | 18 +++---- gnovm/pkg/gnolang/types.go | 62 ++++++++++++++---------- gnovm/tests/files/alias5.gno | 17 +++---- gnovm/tests/files/iface_embed_id.gno | 70 ++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 50 deletions(-) create mode 100644 gnovm/tests/files/iface_embed_id.gno diff --git a/gnovm/pkg/gnolang/op_types.go b/gnovm/pkg/gnolang/op_types.go index 784f5d95475..850959305ea 100644 --- a/gnovm/pkg/gnolang/op_types.go +++ b/gnovm/pkg/gnolang/op_types.go @@ -142,11 +142,11 @@ func (m *Machine) doOpInterfaceType() { // pop methods for i := len(x.Methods) - 1; 0 <= i; i-- { ft := m.PopValue().V.(TypeValue).Type.(FieldType) - // embedded interfaces are named from the resolved type, not the - // written spelling, so an alias and its target stay identical. - fillEmbeddedInterfaceName(&ft) methods[i] = ft } + // flatten embedded interfaces so identity is the method set, not the + // embedded-interface (alias) spelling; see flattenInterfaceMethods. + methods = flattenInterfaceMethods(methods) // push interface type it := &InterfaceType{ PkgPath: m.Package.PkgPath, diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index a747bd65269..6c42a067994 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -4136,16 +4136,12 @@ func staticTypeFromAST(store Store, last BlockNode, x Expr) (Type, bool) { validateStructFields(st, "") return st, true case *InterfaceTypeExpr: - // Build without struct-style embed naming (embed=false), then name - // embedded interfaces from the resolved type rather than the written - // spelling, so an alias and its target collapse to one identity. - methods := buildFieldTypesAST(store, last, x.Methods, false) - for i := range methods { - fillEmbeddedInterfaceName(&methods[i]) - } it := &InterfaceType{ PkgPath: packageOf(last).PkgPath, - Methods: methods, + // flatten embedded interfaces so identity is the method set, not + // the embedded-interface (alias) spelling; see + // flattenInterfaceMethods. + Methods: flattenInterfaceMethods(buildFieldTypesAST(store, last, x.Methods, true)), Generic: x.Generic, } validateEmbedDepth(it, "") @@ -4157,9 +4153,9 @@ func staticTypeFromAST(store Store, last BlockNode, x Expr) (Type, bool) { // buildFieldTypesAST constructs a []FieldType directly from FieldTypeExprs, // mirroring doOpFieldType + doOp{Struct,Interface,Func}Type aggregation. -// embed=true mirrors doOpStructType, which calls fillEmbeddedName per field; -// embed=false mirrors doOpFuncType (and the interface path, which builds with -// embed=false and then names embeds via fillEmbeddedInterfaceName). +// embed=true mirrors doOpStructType / doOpInterfaceType, which name embedded +// fields (the interface path then flattens embeds via flattenInterfaceMethods, +// which discards the embed name); embed=false mirrors doOpFuncType. func buildFieldTypesAST(store Store, last BlockNode, fxs FieldTypeExprs, embed bool) []FieldType { fts := make([]FieldType, len(fxs)) for i := range fxs { diff --git a/gnovm/pkg/gnolang/types.go b/gnovm/pkg/gnolang/types.go index 6811397e770..820cb54e2a4 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -2668,34 +2668,46 @@ func fillEmbeddedName(ft *FieldType, nameSrc Expr) { ft.Embedded = true } -// fillEmbeddedInterfaceName names an embedded-interface field from its -// resolved type, NOT from the source spelling. This is deliberately the -// opposite of fillEmbeddedName (used for struct embeds): Go computes -// interface identity from the flattened method set, so an embedded alias and -// its target must collapse to one identity — interface{ SAlias } (where -// type SAlias = Stringer) must equal interface{ Stringer }. Deriving the -// name from the resolved *DeclaredType keeps both named "Stringer", matching -// pre-#5739 behavior and Go. +// flattenInterfaceMethods expands embedded-interface entries into their +// constituent methods, so a freshly-constructed InterfaceType's Methods list +// holds only concrete methods, never an embedded interface. This makes +// interface identity the flattened method set, matching Go: the +// embedded-interface name (e.g. an alias spelling) no longer leaks into the +// TypeID, so interface{ Stringer } and interface{ Str() string } — and an +// embedded alias vs its target — share one identity. // -// TODO(gnovm): this only equalizes alias-vs-target. GnoVM still stores an -// embedded interface as a single named method entry instead of flattening -// its method set, so structurally identical interfaces with different -// spellings still diverge from Go (e.g. interface{ Stringer } != -// interface{ Str() string }, which is also broken on master). The complete -// fix flattens embedded method sets at construction, dropping the embed name -// from the TypeID entirely; see the follow-up branch scratch/iface-flatten. -// Embedding an alias to an *anonymous* interface still panics here, as it -// did pre-#5739 — the flattening follow-up also resolves that. -func fillEmbeddedInterfaceName(ft *FieldType) { - if ft.Name != "" { - return +// Each source interface was itself flattened and method-count-capped at its +// own construction, so the expansion is bounded; validateInterfaceMethods +// re-checks the cap on the flattened result at the call sites. +// +// Identical methods reached via two paths (diamond embedding) are deduped. +// A genuine same-name/different-signature conflict is rejected by go/types +// (TypeCheckMemPackage, run before the VM constructs any type on the addpkg +// path), so reaching the panic here is should-not-happen — consistent with +// the other construction-time guards in this file (e.g. FieldTypeList.Less). +func flattenInterfaceMethods(fts []FieldType) []FieldType { + out := make([]FieldType, 0, len(fts)) + seen := make(map[Name]Type, len(fts)) + add := func(name Name, typ Type) { + if prev, ok := seen[name]; ok { + if prev.TypeID() != typ.TypeID() { + panic(fmt.Sprintf("duplicate method %s with conflicting types in interface", name)) + } + return + } + seen[name] = typ + out = append(out, FieldType{Name: name, Type: typ}) } - dt, ok := ft.Type.(*DeclaredType) - if !ok { - panic(fmt.Sprintf("cannot derive embedded interface name for type %s", ft.Type.String())) + for i := range fts { + if et := embeddedInterface(fts[i].Type); et != nil { + for j := range et.Methods { + add(et.Methods[j].Name, et.Methods[j].Type) + } + continue + } + add(fts[i].Name, fts[i].Type) } - ft.Name = dt.Name - ft.Embedded = true + return out } func IsImplementedBy(it Type, ot Type) bool { diff --git a/gnovm/tests/files/alias5.gno b/gnovm/tests/files/alias5.gno index 1ed18fcfade..5277a170f18 100644 --- a/gnovm/tests/files/alias5.gno +++ b/gnovm/tests/files/alias5.gno @@ -1,14 +1,9 @@ -// An embedded interface is named from its resolved type, not the written -// spelling, so embedding an alias is identical to embedding its target: -// interface{ SAlias } == interface{ Stringer }. This is the inverse of the -// struct rule (alias2-4), because Go computes interface identity from the -// method set, not the embedded name. Guards against the regression where -// alias-named embeds got distinct TypeIDs. -// -// NOTE: this only equalizes alias-vs-target. Structurally identical -// interfaces with different spellings still diverge (interface{ Stringer } -// != interface{ Str() string }); fixing that needs method-set flattening -// (follow-up, branch scratch/iface-flatten). +// An embedded interface contributes only its method set to identity, never +// the written spelling, so embedding an alias is identical to embedding its +// target: interface{ SAlias } == interface{ Stringer }. This is the inverse +// of the struct rule (alias2-4), because Go computes interface identity from +// the method set, not the embedded name. See iface_embed_id.gno for the wider +// flattening cases (embed-vs-explicit, diamond). package main type Stringer interface{ Str() string } diff --git a/gnovm/tests/files/iface_embed_id.gno b/gnovm/tests/files/iface_embed_id.gno new file mode 100644 index 00000000000..8f9092fb785 --- /dev/null +++ b/gnovm/tests/files/iface_embed_id.gno @@ -0,0 +1,70 @@ +// An embedded interface contributes only its method set to identity, never +// its own (alias) name: anonymous interfaces with the same flattened method +// set share one TypeID, matching Go. Covers embed-vs-explicit, alias-vs- +// target, and diamond embedding (the same method reached twice dedups). +package main + +type Stringer interface{ Str() string } +type SAlias = Stringer + +type R interface{ Read() int } +type W interface{ Write() int } +type A interface{ R } +type B interface{ R } + +func main() { + // embedding a named interface flattens to its methods, so an embed is + // identical to spelling the method out + var a, b interface{} = struct { + X interface{ Stringer } + }{}, struct { + X interface{ Str() string } + }{} + if a != b { + panic("FAIL: interface{Stringer} != interface{Str() string}") + } + + // an embedded alias is identical to embedding its target + var c, d interface{} = struct { + X interface{ SAlias } + }{}, struct { + X interface{ Stringer } + }{} + if c != d { + panic("FAIL: interface{SAlias} != interface{Stringer}") + } + + // embedding two interfaces vs the union of their methods + type RW = interface { + Read() int + Write() int + } + var e, f interface{} = struct { + X interface { + R + W + } + }{}, struct{ X RW }{} + if e != f { + panic("FAIL: interface{R;W} != interface{Read;Write}") + } + + // diamond: interface{A;B} reaches R's method twice; dedups to {Read}, + // identical to interface{R} + var g, h interface{} = struct { + X interface { + A + B + } + }{}, struct { + X interface{ R } + }{} + if g != h { + panic("FAIL: diamond interface{A;B} != interface{R}") + } + + println("ok") +} + +// Output: +// ok