diff --git a/gnovm/pkg/gnolang/op_types.go b/gnovm/pkg/gnolang/op_types.go index b451fd10f83..850959305ea 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,9 +142,11 @@ func (m *Machine) doOpInterfaceType() { // pop methods for i := len(x.Methods) - 1; 0 <= i; i-- { ft := m.PopValue().V.(TypeValue).Type.(FieldType) - fillEmbeddedName(&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 3963642df6e..6c42a067994 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -4138,7 +4138,10 @@ func staticTypeFromAST(store Store, last BlockNode, x Expr) (Type, bool) { case *InterfaceTypeExpr: it := &InterfaceType{ PkgPath: packageOf(last).PkgPath, - Methods: buildFieldTypesAST(store, last, x.Methods, true), + // 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, "") @@ -4150,8 +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/doOpInterfaceType which call -// fillEmbeddedName per field; embed=false mirrors doOpFuncType which does not. +// 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 { @@ -4164,7 +4168,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..820cb54e2a4 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -2637,61 +2637,79 @@ func defaultTypeOf(t Type) Type { } } -func fillEmbeddedName(ft *FieldType) { +func fillEmbeddedName(ft *FieldType, nameSrc Expr) { if ft.Name != "" { return } - 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") + // 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: + ft.Name = e.Name + case *SelectorExpr: + ft.Name = e.Sel } - default: - panic(fmt.Sprintf( - "unexpected field type %s", - ft.Type.String())) + break + } + if ft.Name == "" { + panic(fmt.Sprintf("cannot derive embedded name for field type %s", ft.Type.String())) } ft.Embedded = true } +// 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. +// +// 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}) + } + 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) + } + return out +} + func IsImplementedBy(it Type, ot Type) bool { switch cbt := baseOf(it).(type) { case *InterfaceType: diff --git a/gnovm/tests/files/alias2.gno b/gnovm/tests/files/alias2.gno new file mode 100644 index 00000000000..63f8be15bef --- /dev/null +++ b/gnovm/tests/files/alias2.gno @@ -0,0 +1,31 @@ +// 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 + +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" diff --git a/gnovm/tests/files/alias3.gno b/gnovm/tests/files/alias3.gno new file mode 100644 index 00000000000..c4dc47aaca8 --- /dev/null +++ b/gnovm/tests/files/alias3.gno @@ -0,0 +1,52 @@ +// 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" + +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/alias4.gno b/gnovm/tests/files/alias4.gno new file mode 100644 index 00000000000..64e2b9af14e --- /dev/null +++ b/gnovm/tests/files/alias4.gno @@ -0,0 +1,32 @@ +// 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 } + +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 diff --git a/gnovm/tests/files/alias5.gno b/gnovm/tests/files/alias5.gno new file mode 100644 index 00000000000..5277a170f18 --- /dev/null +++ b/gnovm/tests/files/alias5.gno @@ -0,0 +1,18 @@ +// 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 } +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 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 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