Skip to content
Closed
8 changes: 5 additions & 3 deletions gnovm/pkg/gnolang/op_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
12 changes: 8 additions & 4 deletions gnovm/pkg/gnolang/preprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<anonymous interface>")
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down
112 changes: 65 additions & 47 deletions gnovm/pkg/gnolang/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
31 changes: 31 additions & 0 deletions gnovm/tests/files/alias2.gno
Original file line number Diff line number Diff line change
@@ -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"
52 changes: 52 additions & 0 deletions gnovm/tests/files/alias3.gno
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions gnovm/tests/files/alias4.gno
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions gnovm/tests/files/alias5.gno
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions gnovm/tests/files/extern/aliasdef/aliasdef.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package aliasdef

type Int = int
70 changes: 70 additions & 0 deletions gnovm/tests/files/iface_embed_id.gno
Original file line number Diff line number Diff line change
@@ -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
Loading