diff --git a/gnovm/adr/pr5739_interface_method_set_flattening.md b/gnovm/adr/pr5739_interface_method_set_flattening.md new file mode 100644 index 00000000000..36038efc331 --- /dev/null +++ b/gnovm/adr/pr5739_interface_method_set_flattening.md @@ -0,0 +1,206 @@ +# Interface method-set flattening and cross-package unexported-method identity + +## Context + +GnoVM derives an anonymous interface's `TypeID` from its method list. Before +this change, an *embedded* interface was kept as a single named entry in +`InterfaceType.Methods` (e.g. `interface{ Stringer }` stored a method-list +entry named `Stringer` whose type was the embedded interface). That made the +embedded-interface *name* — including an alias spelling — part of the +`TypeID`, so GnoVM diverged from Go: + +- `interface{ Stringer }` was not identical to `interface{ Str() string }`. +- `interface{ SAlias }` (where `type SAlias = Stringer`) was not identical to + `interface{ Stringer }`. + +**Precise behavior on master (verified):** master named the embed entry from the +*resolved* type — `fillEmbeddedName` does `case *DeclaredType: ft.Name = ct.Name` +— and never flattened. So the two divergences are not symmetric: + +| comparison | master | Go | +|---|---|---| +| `interface{ SAlias }` vs `interface{ Stringer }` | **identical** (alias resolves to the same `Stringer` entry name) | identical | +| `interface{ Stringer }` vs `interface{ Str() string }` | **distinct** (entry named `Stringer` vs method `Str`) | identical | + +So on master the alias case was already correct; only embed-vs-explicit +diverged. No naming policy — resolved or spelled — can fix embed-vs-explicit, +because the embed is still one named entry rather than its methods. Only +flattening removes the embed name from identity. + +Go computes interface identity from the **flattened method set**; embedding +contributes methods, not a name. PR #5739 therefore flattens embedded +interfaces into their constituent methods at type construction +(`flattenInterfaceMethods`, called from `doOpInterfaceType` and +`staticTypeFromAST`), so the embed/alias spelling no longer leaks into the +`TypeID`. + +## The cross-package unexported-method problem + +Flattening exposed a latent representational gap. An unexported interface +method's identity in Go is `(pkgpath, name)` — `p.sec` and `q.sec` are +distinct methods, and a type outside `p` cannot satisfy an interface +containing `p.sec` (the "sealed interface" pattern). GnoVM encodes that +package qualification **once**, on the containing `InterfaceType.PkgPath`, +used by `FieldTypeList.TypeIDForPackage` and by interface-satisfaction gating +in `VerifyImplementedBy` / `FindEmbeddedFieldType`. + +When flattening hoists an unexported method out of an interface defined in +package `P` into an anonymous interface in package `Q`, that single-pkgpath +encoding re-qualifies the method to `Q`. A `/challenge` pass reproduced two +failures (both regressions vs the baseline before this change, confirmed against a +real-Go oracle): + +1. **Identity over-collapse.** `interface{ p.Sec }` (with unexported `p.sec`) + became identical to `interface{ sec() int }` declared in `q` — Go treats + them as distinct. +2. **Satisfaction bypass (security-relevant).** A type declared in `q` with a + `sec()` method was accepted as satisfying `p.Sec`, bypassing the sealed- + interface mechanism. Go correctly rejects it. + +Over-collapse is the dangerous direction (type confusion / access-control +bypass), and it is consensus-relevant in a VM. + +## Decision + +Make flattening fully Go-faithful by recording each method's **origin +package** alongside the method, instead of relying on the enclosing +interface's single `PkgPath`: + +- Add `FieldType.PkgPath` — the defining package of an unexported method + (empty for exported methods and for legacy/non-flattened entries, which + fall back to the enclosing interface's `PkgPath`). +- `flattenInterfaceMethods` stamps the origin package on each hoisted (and + directly-declared) unexported method, and deduplicates on `(pkgpath, name)` + so that two same-named unexported methods from different packages coexist. +- `FieldTypeList.Less` and `FieldTypeList.TypeIDForPackage` key/qualify on the + per-method `PkgPath` when set. +- `VerifyImplementedBy` gates unexported access against the method's origin + package, not the enclosing interface's package. + +This is fully faithful to Go, including the pathological case of two distinct +same-package sealed interfaces with identical method sets. + +### Cost + +`FieldType` is amino-serialized (`gnolang.proto` / `pb3_gen.go`), so adding +`PkgPath` changes the persisted shape of the type representation — a +serialization-format change requiring a coordinated chain upgrade, on top of +the `TypeID` changes flattening already implies. The new field is appended +(proto field 5) and defaults empty, so legacy-decoded entries behave exactly +as before. + +## Alternatives considered + +### Alternative A — do not hoist unsafe embeds (rejected, recorded for trace) + +Flatten only when identity is preserved: hoist exported methods and +unexported methods whose origin package equals the enclosing interface; for an +embed carrying cross-package unexported methods, keep it as a sub-interface +entry and reuse the existing recursive satisfaction path. + +- **Pro:** sound (no over-collapse, no bypass), small diff, **no serialization + change** — `FieldType` shape is untouched. +- **Con:** conservative — it *over-distinguishes* in one case: two distinct + sealed interfaces *in the same package* with identical method sets, embedded + from a *third* package, are treated as distinct where Go treats them as + identical. Over-distinction is the safe direction (a legitimate equality is + missed; satisfaction can never be forged), deterministic across nodes, and + the triggering case is exotic. + +This was rejected only because we chose full Go-fidelity over avoiding the +serialization change. If the serialization/migration cost is later judged not +worth the pathological case, Alternative A is the drop-in fallback: it closes +the same security hole with no persisted-shape change. This ADR exists so that +trade-off can be revisited. + +### Alternative B — keep the embed-entry representation (rejected) + +Name embedded interfaces from the resolved type (the minimal fix that shipped +first on this branch). Sound, but only equalizes alias-vs-target; leaves +`interface{ Stringer } != interface{ Str() string }` diverging from Go. + +## Consequences + +- Interface identity matches Go across embedding, aliasing, multi-level, + diamond, order, mixed embed+direct, and cross-package (exported and + unexported) method sets. +- Consensus-breaking: `TypeID`s of anonymous interfaces that embed other + interfaces change, and the `FieldType` serialization gains a field. Must + land with a chain upgrade (same release class as #5737: coordinated + upgrade / fresh genesis). +- Runtime **assumes flattened** interfaces. The legacy embedded-interface + branches in `FindEmbeddedFieldType` / `VerifyImplementedBy` are dropped; an + `InterfaceKind` entry in `Methods` (only possible by decoding + bytes persisted before this change) is a **hard error** (`panicUnflattened`), + enforced where the concern lives: ungated at the **decode boundary** + (`fillType`, reached from both type-entry decode and object loads — store + bytes are external input, so this check stays in production), and under + `-tags debugAssert` at the interior sites (method resolution, satisfaction, + `TypeID`), which may assume the invariant on a validated store. + +## Follow-up: same-spelled unexported members must not shadow each other + +Review (davd-gzl, omarsy) showed that once two same-named unexported methods +from different packages legally coexist (which flattening enables), every +name-keyed lookup with first-match-wins semantics becomes order-dependent and +wrong: `interface{ sec() int; ifaceext.Sec }` failed selector resolution, its +own `I`-to-`I` assignment ("main.I does not implement main.I"), and concrete +satisfaction (a type with its own `sec` plus a promoted `ifaceext.sec`). + +Two layers were fixed: + +- **Static lookups** (`StructType`/`InterfaceType`/`DeclaredType` + `FindEmbeddedFieldType`): a name match whose unexported gate fails is a + *distinct identity*, not an error — skip it and keep scanning (struct + fields fall through to embedded fields; declared-type direct methods fall + through to the base). `accessError` is reported only when the search ends + with no accessible match; the `trail != nil ⇒ accessError == false` + contract is clamped once in the `findEmbeddedFieldType` dispatcher (callers + check `accessError` before `trail`). +- **Runtime dispatch** (`getPointerToFromTV`, `VPInterface` case): the + resolver used the *dynamic type's* package as `callerPath`, which picks the + wrong member when identities collide (e.g. an interface method call + resolving to a same-named unexported *field* of the receiver). Selector ops + now pass the executing package (`m.Package.PkgPath`) — statically gated to + equal the method's origin for unexported selectors. The exported + `GetPointerToFromTV` keeps the dynamic-type fallback (debugger, + collision-free by construction there). + +Pinned by `iface_embed_sel_order.gno` (order-independent selector), +`iface_embed_same_name.gno` (self-assignment), and +`iface_embed_field_shadow.gno` (field vs promoted method, static + runtime), +each verified against real Go first. + +## Rollout: state persisted before this change is unsupported (decided) + +Every `InterfaceType` is born one of three ways: **preprocess (AST)**, +**runtime (`doOpInterfaceType`)**, or **decode**. The first two always flatten; +decode faithfully reproduces stored bytes. So an unflattened interface can +reach runtime in exactly one situation: decoding bytes persisted **before +this change**. + +Tolerating those bytes (the recursion branches this PR originally kept) was a +half-measure: the type's *identity* already moved with this change, so +resolution/satisfaction would "work" against a type whose equality, type-keyed +store entries, and hashes are silently split from post-change construction. +Silent-wrong loses to loud-fail on a chain, so the branches are **dropped** +and a decoded unflattened interface **panics** with an actionable message. + +A network carrying state persisted before this change has two supported paths, both of which +make unflattened bytes impossible (the panic is then a dead invariant check): + +- **Fresh genesis / tx replay** (how gno.land testnets regenesis): all state + is reconstructed through the new VM, flattened by construction. The only + pre-fork audit is source acceptance — historical txs must still preprocess + (this PR tightens one case: cross-package unexported-method selection). +- **Re-flatten migration** (only if in-place state must survive): walk the + type table, flatten every `InterfaceType` (stamping origin `PkgPath`), + remap old→new `TypeID`s in all object refs, merge newly-colliding entries, + and verify no `InterfaceKind` entry remains. + +Conflict handling (same-name, different-signature embedded methods) needs no +follow-up: `flattenInterfaceMethods` `panic`s, but during preprocessing that is +wrapped into a positioned `*PreprocessError` (the same idiom as other type +errors, e.g. "struct has no field"), so it surfaces as a matchable `// Error:`. +go/types also rejects it as a `// TypeCheckError:`. Both are pinned by +`iface_embed_conflict.gno`, matching Go's "duplicate method" compile error. diff --git a/gnovm/pkg/gnolang/gnolang.proto b/gnovm/pkg/gnolang/gnolang.proto index 4798eaf7fa4..3edf654a15d 100644 --- a/gnovm/pkg/gnolang/gnolang.proto +++ b/gnovm/pkg/gnolang/gnolang.proto @@ -596,6 +596,7 @@ message FieldType { google.protobuf.Any type = 2 [json_name = "Type"]; bool embedded = 3 [json_name = "Embedded"]; string tag = 4 [json_name = "Tag"]; + string pkg_path = 5 [json_name = "PkgPath"]; } message FuncType { diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 01e0109c8d6..b7118a49d32 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -2816,7 +2816,7 @@ func (m *Machine) resolvePointer(lx Expr, lhsOperands []TypedValue) (pv PointerV } case *SelectorExpr: xv := &lhsOperands[0] - pv = xv.GetPointerToFromTV(m.Alloc, m.Store, lx.Path) + pv = xv.getPointerToFromTV(m.Alloc, m.Store, lx.Path, m.Package.PkgPath) ro = m.IsReadonly(xv) case *StarExpr: xv := &lhsOperands[0] diff --git a/gnovm/pkg/gnolang/op_expressions.go b/gnovm/pkg/gnolang/op_expressions.go index a727147f411..480cc224bd0 100644 --- a/gnovm/pkg/gnolang/op_expressions.go +++ b/gnovm/pkg/gnolang/op_expressions.go @@ -80,7 +80,7 @@ func (m *Machine) doOpSelector() { default: m.incrCPU(OpCPUSelectorField) } - res := xv.GetPointerToFromTV(m.Alloc, m.Store, sx.Path).Deref() + res := xv.getPointerToFromTV(m.Alloc, m.Store, sx.Path, m.Package.PkgPath).Deref() if debug { m.Printf("-v[S] %v\n", xv) m.Printf("+v[S] %v\n", res) diff --git a/gnovm/pkg/gnolang/op_types.go b/gnovm/pkg/gnolang/op_types.go index b451fd10f83..6552c64470b 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, m.Package.PkgPath) // push interface type it := &InterfaceType{ PkgPath: m.Package.PkgPath, diff --git a/gnovm/pkg/gnolang/parity_test.go b/gnovm/pkg/gnolang/parity_test.go index 1745ea8993b..b6c81474a8d 100644 --- a/gnovm/pkg/gnolang/parity_test.go +++ b/gnovm/pkg/gnolang/parity_test.go @@ -163,5 +163,22 @@ func parityCasesGnolang() []struct { // SliceValue referring to no backing Value. {"SliceValue/empty", &SliceValue{Base: nil, Offset: 0, Length: 0, Maxcap: 0}}, + + // FieldType.PkgPath (wire field 5) — set on an unexported method that + // was flattened out of an interface in package "p". Guards the + // genproto2/reflect parity of that field: without the hand-added + // field-5 marshal in pb3_gen.go, the reflect codec emits PkgPath while + // the fast path drops it, and this case fails. + {"FieldType/unexported-pkgpath", &FieldType{Name: "sec", PkgPath: "p", Type: &FuncType{}}}, + {"FieldType/exported-empty-pkgpath", &FieldType{Name: "M", Type: &FuncType{}}}, + // InterfaceType carrying both an exported (empty PkgPath) and a + // stamped unexported method — the realistic persisted shape. + {"InterfaceType/stamped-unexported", &InterfaceType{ + PkgPath: "q", + Methods: []FieldType{ + {Name: "M", Type: &FuncType{}}, + {Name: "sec", PkgPath: "p", Type: &FuncType{}}, + }, + }}, } } diff --git a/gnovm/pkg/gnolang/pb3_gen.go b/gnovm/pkg/gnolang/pb3_gen.go index 6c18851591f..d3f7d906d6b 100644 --- a/gnovm/pkg/gnolang/pb3_gen.go +++ b/gnovm/pkg/gnolang/pb3_gen.go @@ -14252,6 +14252,18 @@ func (goo *StructType) UnmarshalBinary2(cdc *amino.Codec, bz []byte, anyDepth in func (goo FieldType) MarshalBinary2(cdc *amino.Codec, buf []byte, offset int) (int, error) { var err error + if goo.PkgPath != "" { + { + before := offset + offset = amino.PrependString(buf, offset, string(goo.PkgPath)) + valueLen := before - offset + if valueLen > 1 || (valueLen == 1 && buf[offset] != 0x00) { + offset = amino.PrependFieldNumberAndTyp3(buf, offset, 5, amino.Typ3ByteLength) + } else { + offset = before + } + } + } if goo.Tag != "" { { before := offset @@ -14323,6 +14335,9 @@ func (goo FieldType) SizeBinary2(cdc *amino.Codec) (int, error) { if goo.Tag != "" { s += 1 + amino.UvarintSize(uint64(len(goo.Tag))) + len(goo.Tag) } + if goo.PkgPath != "" { + s += 1 + amino.UvarintSize(uint64(len(goo.PkgPath))) + len(goo.PkgPath) + } return s, nil } @@ -14385,6 +14400,16 @@ func (goo *FieldType) UnmarshalBinary2(cdc *amino.Codec, bz []byte, anyDepth int } bz = bz[n:] goo.Tag = Tag(v) + case 5: + if typ3 != amino.Typ3ByteLength { + return fmt.Errorf("field 5: expected typ3 %v, got %v", amino.Typ3ByteLength, typ3) + } + v, n, err := amino.DecodeString(bz) + if err != nil { + return err + } + bz = bz[n:] + goo.PkgPath = string(v) default: return fmt.Errorf("unknown field number %d for FieldType", fnum) } diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 9b1a7c28516..f921e969b70 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -4153,9 +4153,13 @@ func staticTypeFromAST(store Store, last BlockNode, x Expr) (Type, bool) { validateStructFields(st, "") return st, true case *InterfaceTypeExpr: + pkgPath := packageOf(last).PkgPath it := &InterfaceType{ - PkgPath: packageOf(last).PkgPath, - Methods: buildFieldTypesAST(store, last, x.Methods, true), + PkgPath: pkgPath, + // Build without embed naming (embed=false): flattenInterfaceMethods + // expands embedded interfaces into their method set, making identity + // the method set rather than the embedded-interface (alias) spelling. + Methods: flattenInterfaceMethods(buildFieldTypesAST(store, last, x.Methods, false), pkgPath), Generic: x.Generic, } validateEmbedDepth(it, "") @@ -4167,8 +4171,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 names embedded fields per field; +// embed=false mirrors doOpFuncType and the interface path (which leaves embeds +// unnamed and flattens them via flattenInterfaceMethods). func buildFieldTypesAST(store Store, last BlockNode, fxs FieldTypeExprs, embed bool) []FieldType { fts := make([]FieldType, len(fxs)) for i := range fxs { @@ -4181,7 +4186,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/realm.go b/gnovm/pkg/gnolang/realm.go index ab6d83ec3ac..fb0634f63ef 100644 --- a/gnovm/pkg/gnolang/realm.go +++ b/gnovm/pkg/gnolang/realm.go @@ -1509,6 +1509,7 @@ func copyFieldsWithRefs(fields []FieldType) []FieldType { Type: refOrCopyType(field.Type), Embedded: field.Embedded, Tag: field.Tag, + PkgPath: field.PkgPath, } } return fieldsCpy @@ -1819,6 +1820,12 @@ func fillType(store Store, typ Type) Type { case *InterfaceType: for i, mthd := range ct.Methods { ct.Methods[i].Type = fillType(store, mthd.Type) + // An embed entry means the bytes predate interface flattening + // (unsupported state); reject at this decode boundary, which + // sees every stored type. See panicUnflattened. + if ct.Methods[i].Type.Kind() == InterfaceKind { + ct.panicUnflattened(ct.Methods[i]) + } } return ct case *TypeType: diff --git a/gnovm/pkg/gnolang/types.go b/gnovm/pkg/gnolang/types.go index 6b7e89491da..850b09e2281 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -2,6 +2,7 @@ package gnolang import ( "fmt" + "slices" "sort" "strconv" "strings" @@ -352,6 +353,16 @@ type FieldType struct { Type Type Embedded bool Tag Tag + // PkgPath is the defining package of an unexported interface method, + // recorded when flattenInterfaceMethods hoists a method out of an + // embedded interface (whose package may differ from the enclosing + // interface). Empty for exported methods, struct fields, and same-package + // methods (direct or same-package embeds); an empty value falls back to + // the enclosing interface's PkgPath. Used to keep unexported-method + // identity package-qualified (see ms.TypeIDForPackage) and to gate + // unexported selection and interface satisfaction (see + // FindEmbeddedFieldType, VerifyImplementedBy). + PkgPath string } func (ft FieldType) Kind() Kind { @@ -397,25 +408,43 @@ func (ft FieldType) IsNamed() bool { type FieldTypeList []FieldType -// FieldTypeList implements sort.Interface. -func (l FieldTypeList) Len() int { - return len(l) -} - -// FieldTypeList implements sort.Interface. -func (l FieldTypeList) Less(i, j int) bool { - iname, jname := l[i].Name, l[j].Name - if iname == jname { - panic(fmt.Sprintf("duplicate name found in field list: %s", iname)) +// originPkg returns the method's defining package: its stamped PkgPath, or +// fallback (the enclosing interface's package) when unstamped. Same-package +// methods are left unstamped, so the fallback is the common case. +func (ft FieldType) originPkg(fallback string) string { + if ft.PkgPath != "" { + return ft.PkgPath + } + return fallback +} + +// idName returns the field's identity name: an exported name is bare, an +// unexported name is qualified by the method's origin package (see originPkg). +// This keeps unexported-method identity package-scoped, matching Go, even +// after a method has been flattened out of an embedded interface from another +// package. +func (ft FieldType) idName(fallbackPkg string) string { + if isUpper(string(ft.Name)) { + return string(ft.Name) + } + return ft.originPkg(fallbackPkg) + "." + string(ft.Name) +} + +// sortForPackage sorts the methods by their package-qualified identity name +// (idName with pkgPath as the fallback) — the SAME key TypeIDForPackage emits +// with. Keying the sort and the emission identically means the order does not +// depend on whether a method's PkgPath is stamped (cross-package hoist) or +// empty (same-package fallback), so the same logical interface always yields +// one TypeID. Panics on a duplicate qualified name. +func (l FieldTypeList) sortForPackage(pkgPath string) { + sort.SliceStable(l, func(i, j int) bool { + return l[i].idName(pkgPath) < l[j].idName(pkgPath) + }) + for i := 1; i < len(l); i++ { + if l[i].idName(pkgPath) == l[i-1].idName(pkgPath) { + panic(fmt.Sprintf("duplicate name found in field list: %s", l[i].idName(pkgPath))) + } } - return iname < jname -} - -// FieldTypeList implements sort.Interface. -func (l FieldTypeList) Swap(i, j int) { - t := l[i] - l[i] = l[j] - l[j] = t } // User should call sort for interface methods. @@ -443,12 +472,9 @@ func (l FieldTypeList) TypeIDForPackage(pkgPath string) TypeID { ll := len(l) s := "" for i, ft := range l { - fn := ft.Name - if isUpper(string(fn)) { - s += string(fn) + " " + ft.Type.TypeID().String() - } else { - s += pkgPath + "." + string(fn) + " " + ft.Type.TypeID().String() - } + // idName qualifies an unexported method by its origin package + // (ft.PkgPath when flattened out of another package, else pkgPath). + s += ft.idName(pkgPath) + " " + ft.Type.TypeID().String() if i != ll-1 { s += ";" } @@ -869,20 +895,22 @@ func (st *StructType) FindEmbeddedFieldType(callerPath string, n Name, m map[Typ sf := &st.Fields[i] // Maybe is a field of the struct. if sf.Name == n { - // Ensure exposed or package match. - if !isUpper(string(n)) && st.PkgPath != callerPath { - return nil, false, nil, nil, true + // Ensure exposed or package match. A same-spelled unexported + // name from another package is a distinct identity: skip it and + // keep searching embedded fields for an accessible one. + if isUpper(string(n)) || st.PkgPath == callerPath { + vp := NewValuePathField(0, uint16(i), n) + return []ValuePath{vp}, false, nil, sf.Type, false } - vp := NewValuePathField(0, uint16(i), n) - return []ValuePath{vp}, false, nil, sf.Type, false + accessError = true } // Maybe is embedded within a field. if sf.Embedded { st := sf.Type trail2, hasPtr2, rcvr2, field2, accessError2 := findEmbeddedFieldType(callerPath, st, n, m) if accessError2 { - // XXX make test case and check against go - return nil, false, nil, nil, true + // A gated match somewhere below: remember, keep scanning. + accessError = true } else if trail2 != nil { if trail != nil { // conflict detected. return none. @@ -895,6 +923,9 @@ func (st *StructType) FindEmbeddedFieldType(callerPath string, n Name, m map[Typ } } } + // A sibling gated match may have set accessError even though an + // accessible one was found; the findEmbeddedFieldType dispatcher + // clamps it (trail != nil implies accessError == false). return // may be found or nil. } @@ -970,13 +1001,26 @@ func (it *InterfaceType) TypeID() TypeID { } } if it.typeid.IsZero() { + // Identity is the flattened method set; an embed-carrying Methods + // list would emit a nonsense TypeID. Interior invariant only: the + // decode boundary (fillType) rejects such state ungated. + if debugAssert { + for i := range it.Methods { + if it.Methods[i].Type.Kind() == InterfaceKind { + it.panicUnflattened(it.Methods[i]) + } + } + } // NOTE Interface types expressed or declared in different // packages may have the same TypeID if and only if // neither have unexported fields. pt.Path is only // included in field names that are not uppercase. ms := FieldTypeList(it.Methods) // XXX pre-sort. - sort.Sort(ms) + // Sort with the same package fallback emission uses, so stamped + // (cross-package hoist) and unstamped (same-package fallback) entries + // of the same interface always produce one TypeID. + ms.sortForPackage(it.PkgPath) it.typeid = typeid("interface{" + ms.TypeIDForPackage(it.PkgPath).String() + "}") } return it.typeid @@ -1018,66 +1062,55 @@ func (it *InterfaceType) IsNamed() bool { func (it *InterfaceType) FindEmbeddedFieldType(callerPath string, n Name, m map[Type]struct{}) ( trail []ValuePath, hasPtr bool, rcvr Type, ft Type, accessError bool, ) { - // Recursion guard - if m == nil { - m = map[Type]struct{}{it: (struct{}{})} - } else if _, exists := m[it]; exists { - return nil, false, nil, nil, false - } else { - m[it] = struct{}{} - } - // ... + // Methods is flattened at construction (flattenInterfaceMethods): every + // entry is a concrete method (FuncType), so lookup is a flat scan. for _, im := range it.Methods { + if debugAssert && im.Type.Kind() == InterfaceKind { + it.panicUnflattened(im) + } if im.Name == n { - // Ensure exposed or package match. - if !isUpper(string(n)) && it.PkgPath != callerPath { - return nil, false, nil, nil, true - } - // a matched name cannot be an embedded interface. - if im.Type.Kind() == InterfaceKind { - return nil, false, nil, nil, false + // Ensure exposed or package match. Gate against the method's + // origin package (its stamp when flattened out of another + // package), not the enclosing interface's — same rule as + // VerifyImplementedBy below. A same-spelled unexported method + // from another package is a distinct method: skip it and keep + // scanning for one with a matching identity. + if !isUpper(string(n)) && im.originPkg(it.PkgPath) != callerPath { + accessError = true + continue } // match found. tr := []ValuePath{NewValuePathInterface(n)} - hasPtr := false - rcvr := Type(nil) - ft := im.Type - return tr, hasPtr, rcvr, ft, false - } - if et, ok := baseOf(im.Type).(*InterfaceType); ok { - // embedded interfaces must be recursively searched. - trail, hasPtr, rcvr, ft, accessError = et.FindEmbeddedFieldType(callerPath, n, m) - if accessError { - // XXX make test case and check against go - return nil, false, nil, nil, true - } else if trail != nil { - if debug { - if len(trail) != 1 || trail[0].Type != VPInterface { - panic("should not happen") - } - } - return trail, hasPtr, rcvr, ft, false - } // else continue search. - } // else continue search. + return tr, false, nil, im.Type, false + } } - return nil, false, nil, nil, false + return nil, false, nil, nil, accessError +} + +// panicUnflattened reports an InterfaceKind entry in Methods — impossible +// from any construction path (all flatten via flattenInterfaceMethods), so it +// can only be state persisted by code that predates interface flattening, +// which is unsupported; see adr/pr5739_interface_method_set_flattening.md. +// Enforced ungated at the decode boundary (fillType); interior sites assert +// under -tags debugAssert. +func (it *InterfaceType) panicUnflattened(im FieldType) { + panic(fmt.Sprintf( + "unflattened embedded interface %q in %s", + im.Name, it.String())) } // For run-time type assertion. // TODO: optimize somehow. func (it *InterfaceType) VerifyImplementedBy(ot Type) error { for _, im := range it.Methods { - if im.Type.Kind() == InterfaceKind { - // field is embedded interface... - im2 := baseOf(im.Type).(*InterfaceType) - if err := im2.VerifyImplementedBy(ot); err != nil { - return err - } else { - continue - } - } - // find method in field. - tr, hp, rt, ft, _ := findEmbeddedFieldType(it.PkgPath, ot, im.Name, nil) + if debugAssert && im.Type.Kind() == InterfaceKind { + it.panicUnflattened(im) + } + // find method in field. Gate unexported-method access against the + // method's origin package (its stamp when flattened out of another + // package), not the enclosing interface's — otherwise a type could + // satisfy another package's sealed interface. + tr, hp, rt, ft, _ := findEmbeddedFieldType(im.originPkg(it.PkgPath), ot, im.Name, nil) if tr == nil { // not found. return fmt.Errorf("missing method %s", im.Name) } @@ -2175,30 +2208,32 @@ func (dt *DeclaredType) FindEmbeddedFieldType(callerPath string, n Name, m map[T m[dt] = struct{}{} } // Search direct methods via O(1) name lookup. + gated := false if i, ok := dt.lookupMethod(n); ok { - fv := dt.Methods[i].V.(*FuncValue) // Ensure exposed or package match. - if !isUpper(string(n)) && dt.PkgPath != callerPath { - return nil, false, nil, nil, true - } - // NOTE: makes code simple but requires preprocessor's - // Store to pre-load method types. - rt := fv.GetType(nil).Params[0].Type - var vp ValuePath - if _, isPtr := rt.(*PointerType); isPtr { - vp = NewValuePathPtrMethod(i, n) - } else { - vp = NewValuePathValMethod(i, n) + if isUpper(string(n)) || dt.PkgPath == callerPath { + fv := dt.Methods[i].V.(*FuncValue) + // NOTE: makes code simple but requires preprocessor's + // Store to pre-load method types. + mt := fv.GetType(nil) + rt := mt.Params[0].Type + var vp ValuePath + if _, isPtr := rt.(*PointerType); isPtr { + vp = NewValuePathPtrMethod(i, n) + } else { + vp = NewValuePathValMethod(i, n) + } + return []ValuePath{vp}, false, rt, mt.BoundType(), false } - // NOTE: makes code simple but requires preprocessor's - // Store to pre-load method types. - bt := fv.GetType(nil).BoundType() - return []ValuePath{vp}, false, rt, bt, false + // A same-spelled unexported method from another package is a + // distinct method: fall through to the base search for one with + // a matching identity. + gated = true } // Otherwise, search base. trail, hasPtr, rcvr, ft, accessError = findEmbeddedFieldType(callerPath, dt.Base, n, m) if trail == nil { - return nil, false, nil, nil, accessError + return nil, false, nil, nil, accessError || gated } switch trail[0].Type { case VPInterface: @@ -2637,61 +2672,101 @@ 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 interfaces into their methods so +// Methods holds only concrete methods. Interface identity then equals the +// flattened method set (matching Go): the embed/alias spelling no longer +// leaks into the TypeID. Diamond embeds dedup; a same-name/different-type +// conflict can't reach here (go/types rejects it first), so the panic is a +// should-not-happen guard. Bounded: sources are already flattened+capped. +// pkgPath is the enclosing interface's package, used to qualify the identity +// of directly-declared unexported methods. Methods hoisted from an embedded +// interface keep their own origin package (the embed's PkgPath, or, if the +// embed is itself from another package, the method's already-stamped +// FieldType.PkgPath), so that an unexported method's (pkgpath, name) identity +// — and the sealed-interface guarantee it backs — survives flattening. +func flattenInterfaceMethods(fts []FieldType, pkgPath string) []FieldType { + // Fast path: the common interface (interface{}, or one with only direct + // methods) has nothing to flatten — return it as-is, skipping the dedup + // map and per-method key building. Direct unexported methods need no + // PkgPath stamp: idName falls back to the enclosing pkgPath at TypeID time. + hasEmbed := slices.ContainsFunc(fts, func(ft FieldType) bool { + return embeddedInterface(ft.Type) != nil + }) + if !hasEmbed { + return fts + } + out := make([]FieldType, 0, len(fts)) + // keyed on the package-qualified identity name: two same-named unexported + // methods from different packages are distinct and must coexist. + seen := make(map[string]Type, len(fts)) + add := func(name Name, typ Type, origin string) { + ft := FieldType{Name: name, Type: typ} + // Stamp only cross-package unexported methods; same-package ones stay + // unstamped and rely on idName's fallback to the enclosing pkgPath, + // matching the fast path (so both construction routes agree). + if !isUpper(string(name)) && origin != pkgPath { + ft.PkgPath = origin + } + key := ft.idName(pkgPath) + if prev, ok := seen[key]; ok { + if prev.TypeID() != typ.TypeID() { + panic(fmt.Sprintf("duplicate method %s with conflicting types in interface", name)) + } + return + } + seen[key] = typ + out = append(out, ft) + } + for i := range fts { + if et := embeddedInterface(fts[i].Type); et != nil { + for j := range et.Methods { + m := et.Methods[j] + // origin pkg: the method's own stamp (if already hoisted from + // another package) else the embedded interface's package. + origin := m.PkgPath + if origin == "" { + origin = et.PkgPath + } + add(m.Name, m.Type, origin) + } + continue + } + add(fts[i].Name, fts[i].Type, pkgPath) + } + return out +} + func IsImplementedBy(it Type, ot Type) bool { switch cbt := baseOf(it).(type) { case *InterfaceType: @@ -2956,21 +3031,27 @@ func isGeneric(t Type) bool { // TODO: could this be more optimized for the runtime? // are Go-style itables the solution or? // callerPath: the path of package where selector node was declared. +// Contract: trail != nil implies accessError == false — a same-spelled +// unexported name from another package is a distinct identity, so a gated +// candidate is skipped, not an error, when an accessible match exists. +// Callers rely on this (they check accessError before trail). func findEmbeddedFieldType(callerPath string, t Type, n Name, m map[Type]struct{}) ( trail []ValuePath, hasPtr bool, rcvr Type, ft Type, accessError bool, ) { switch ct := t.(type) { case *DeclaredType: - return ct.FindEmbeddedFieldType(callerPath, n, m) + trail, hasPtr, rcvr, ft, accessError = ct.FindEmbeddedFieldType(callerPath, n, m) case *PointerType: - return ct.FindEmbeddedFieldType(callerPath, n, m) + trail, hasPtr, rcvr, ft, accessError = ct.FindEmbeddedFieldType(callerPath, n, m) case *StructType: - return ct.FindEmbeddedFieldType(callerPath, n, m) + trail, hasPtr, rcvr, ft, accessError = ct.FindEmbeddedFieldType(callerPath, n, m) case *InterfaceType: - return ct.FindEmbeddedFieldType(callerPath, n, m) + trail, hasPtr, rcvr, ft, accessError = ct.FindEmbeddedFieldType(callerPath, n, m) default: return nil, false, nil, nil, false } + accessError = accessError && trail == nil // enforce the contract above. + return } // GetPkgID returns the cached PkgID for this DeclaredType, computing diff --git a/gnovm/pkg/gnolang/types_test.go b/gnovm/pkg/gnolang/types_test.go new file mode 100644 index 00000000000..31b6b97b860 --- /dev/null +++ b/gnovm/pkg/gnolang/types_test.go @@ -0,0 +1,281 @@ +package gnolang + +import ( + "strings" + "testing" +) + +// ft builds a concrete (non-embedded) method entry. +func mkMethod(name string, typ Type) FieldType { + return FieldType{Name: Name(name), Type: typ} +} + +// mkEmbed builds an embed entry: a field whose type is an (already flattened) +// interface from package pkg with the given methods. +func mkEmbed(pkg string, methods ...FieldType) FieldType { + return FieldType{Type: &InterfaceType{PkgPath: pkg, Methods: methods}} +} + +func TestFlattenInterfaceMethods(t *testing.T) { + t.Parallel() + + fn := &FuncType{} // func() + + type want struct { + name string + pkg string + } + cases := []struct { + desc string + in []FieldType + pkgPath string + want []want + }{ + { + // No embed → fast path returns the slice unchanged: a direct + // unexported method is NOT stamped (idName falls back to the + // enclosing pkg at TypeID time), so PkgPath stays empty. + desc: "no-embed interface returned unchanged (fast path)", + in: []FieldType{mkMethod("M", fn), mkMethod("m", fn)}, + pkgPath: "q", + want: []want{{"M", ""}, {"m", ""}}, + }, + { + // On the slow path a same-package unexported method is still left + // unstamped (only cross-package methods are stamped), so it relies + // on idName's fallback — matching the fast path. + desc: "same-package unexported left unstamped even on the slow path", + in: []FieldType{ + mkEmbed("p", mkMethod("E", fn)), + mkMethod("m", fn), + }, + pkgPath: "q", + want: []want{{"E", ""}, {"m", ""}}, + }, + { + desc: "embedded exported method flattens, pkg stays empty (package-independent identity)", + in: []FieldType{mkEmbed("p", mkMethod("E", fn))}, + pkgPath: "q", + want: []want{{"E", ""}}, + }, + { + desc: "embedded unexported method keeps its origin package, not the enclosing one", + in: []FieldType{mkEmbed("p", FieldType{Name: "sec", Type: fn, PkgPath: "p"})}, + pkgPath: "q", + want: []want{{"sec", "p"}}, + }, + { + desc: "diamond: same unexported method via two embeds dedups to one", + in: []FieldType{ + mkEmbed("p", FieldType{Name: "sec", Type: fn, PkgPath: "p"}), + mkEmbed("p", FieldType{Name: "sec", Type: fn, PkgPath: "p"}), + }, + pkgPath: "q", + want: []want{{"sec", "p"}}, + }, + { + // cross-package sec (stamped "p") and a same-package sec (left + // unstamped, qualified to "q" via fallback) are distinct and coexist. + desc: "same name, different package: distinct methods coexist", + in: []FieldType{ + mkEmbed("p", FieldType{Name: "sec", Type: fn, PkgPath: "p"}), + mkMethod("sec", fn), // declared directly in enclosing q + }, + pkgPath: "q", + want: []want{{"sec", "p"}, {"sec", ""}}, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + got := flattenInterfaceMethods(tc.in, tc.pkgPath) + if len(got) != len(tc.want) { + t.Fatalf("len = %d, want %d (%+v)", len(got), len(tc.want), got) + } + for i, w := range tc.want { + if string(got[i].Name) != w.name || got[i].PkgPath != w.pkg { + t.Errorf("entry %d = (%s, %q), want (%s, %q)", + i, got[i].Name, got[i].PkgPath, w.name, w.pkg) + } + if got[i].Embedded { + t.Errorf("entry %d (%s) should not be marked Embedded", i, got[i].Name) + } + } + }) + } +} + +// Two embeds contributing the same method name with conflicting signatures is +// a should-not-happen (go/types rejects it upstream); flatten's dedup guards +// with a panic. (A pure direct-method duplicate takes the fast path and is +// instead caught later by sortForPackage at TypeID time.) +func TestFlattenInterfaceMethods_ConflictPanics(t *testing.T) { + t.Parallel() + fn := &FuncType{} + fn2 := &FuncType{Results: []FieldType{{Type: BoolType}}} + in := []FieldType{ + mkEmbed("p", mkMethod("M", fn)), + mkEmbed("p", mkMethod("M", fn2)), + } + + defer func() { + if recover() == nil { + t.Fatal("expected panic on conflicting embedded method, got none") + } + }() + flattenInterfaceMethods(in, "q") +} + +// Regression for the sort/emit provenance mismatch (thehowl review). The same +// interface in package p can hold an unexported method with PkgPath either +// stamped to "p" (the method was hoisted from an embed — slow path) or empty +// (the method was declared directly, so the no-embed fast path leaves it +// unstamped). Both must yield one TypeID: the sort key and the emitted id +// both qualify via the enclosing package, so the order can't flip between +// the two representations. +func TestInterfaceTypeID_PkgPathProvenance(t *testing.T) { + t.Parallel() + fn := &FuncType{} + stamped := &InterfaceType{PkgPath: "p", Methods: []FieldType{ + {Name: "M", Type: fn}, + {Name: "z", Type: fn, PkgPath: "p"}, // hoisted from an embed + }} + unstamped := &InterfaceType{PkgPath: "p", Methods: []FieldType{ + {Name: "M", Type: fn}, + {Name: "z", Type: fn}, // declared directly (fast path, unstamped) + }} + if stamped.TypeID() != unstamped.TypeID() { + t.Fatalf("stamped vs empty PkgPath gave different TypeIDs:\n stamped: %s\n unstamped: %s", + stamped.TypeID(), unstamped.TypeID()) + } +} + +// An InterfaceKind entry in Methods can only be state persisted before +// interface flattening (every construction path flattens), which is +// unsupported — identity already moved. Store bytes are external input, so +// the decode boundary (fillType, reached from both GetTypeSafe and +// fillTypesOfValue) rejects it unconditionally; the interior sites +// (FindEmbeddedFieldType/VerifyImplementedBy/TypeID) assume the invariant +// and only assert under -tags debugAssert. Pins the drop of the legacy +// embedded-interface branches; see adr/pr5739. +func TestInterfaceType_UnflattenedIsHardError(t *testing.T) { + t.Parallel() + embedded := &InterfaceType{PkgPath: "p", Methods: []FieldType{{Name: "M", Type: &FuncType{}}}} + legacy := &InterfaceType{PkgPath: "p", Methods: []FieldType{ + {Name: "E", Type: embedded, Embedded: true}, // as decoded from bytes persisted before flattening + }} + + uses := map[string]func(){ + // no RefType inside, so a nil store never dereferences + "fillType": func() { fillType(nil, legacy) }, + } + if debugAssert { + uses["FindEmbeddedFieldType"] = func() { legacy.FindEmbeddedFieldType("p", "M", nil) } + uses["VerifyImplementedBy"] = func() { legacy.VerifyImplementedBy(embedded) } + uses["TypeID"] = func() { legacy.TypeID() } + } + for name, use := range uses { + t.Run(name, func(t *testing.T) { + t.Parallel() // uses is read-only on the shared types; TypeID panics before its cache write + defer func() { + r := recover() + if r == nil { + t.Fatal("expected panic on unflattened interface, got none") + } + if s, ok := r.(string); !ok || !strings.Contains(s, "unflattened embedded interface") { + t.Fatalf("expected unflattened-interface panic, got: %v", r) + } + }() + use() + }) + } +} + +// The runtime interface-construction path (doOpInterfaceType) must flatten its +// embeds just like the preprocess path. This path executes during filetests +// (e.g. v.(interface{ Embed })) but its result is never observed for identity +// there, so no filetest pins it; drive the op directly and assert the built +// InterfaceType holds the embed's methods, with no embedded-interface entry. +func TestDoOpInterfaceType_Flattens(t *testing.T) { + m := NewMachine("p", nil) + defer m.Release() + + // Two embeds that overlap on B (a diamond): {A,B} and {B,C}. Flattening + // expands both and dedups B, so the result is exactly {A,B,C} — 3 methods. + // Without flattening the interface would instead hold 2 embedded-interface + // entries, so the count distinguishes real flatten+dedup from pass-through. + fnB := &FuncType{} // shared signature so the overlapping B dedups (not conflicts) + e1 := &InterfaceType{PkgPath: "p", Methods: []FieldType{{Name: "A", Type: &FuncType{}}, {Name: "B", Type: fnB}}} + e2 := &InterfaceType{PkgPath: "p", Methods: []FieldType{{Name: "B", Type: fnB}, {Name: "C", Type: &FuncType{}}}} + + // interface{ e1; e2 }: doOpInterfaceType reads len(x.Methods) from the expr + // and pops one resolved type per method off the value stack. + m.PushValue(TypedValue{T: gTypeType, V: toTypeValue(FieldType{Type: e1})}) + m.PushValue(TypedValue{T: gTypeType, V: toTypeValue(FieldType{Type: e2})}) + m.PushExpr(&InterfaceTypeExpr{Methods: FieldTypeExprs{{}, {}}}) + + m.doOpInterfaceType() + + it := m.PopValue().V.(TypeValue).Type.(*InterfaceType) + if len(it.Methods) != 3 { + t.Fatalf("expected 3 flattened+deduped methods (A,B,C), got %d: %+v", len(it.Methods), it.Methods) + } + got := map[Name]int{} + for _, ft := range it.Methods { + if ft.Type.Kind() == InterfaceKind { + t.Fatalf("embedded-interface entry survived flattening: %+v", ft) + } + got[ft.Name]++ + } + for _, n := range []Name{"A", "B", "C"} { + if got[n] != 1 { + t.Fatalf("method %s appears %d times, want exactly 1: %+v", n, got[n], it.Methods) + } + } +} + +// The runtime struct-construction path (doOpStructType) must name embedded +// fields from the source expr like the preprocess path (buildFieldTypesAST) +// does. Nothing in the Files suite reaches this op with an embedded field +// (verified by instrumentation), so drive the op directly: the popped +// FieldType carries only the resolved type (the alias is gone), and the name +// must come from the expr as written. +func TestDoOpStructType_EmbedNames(t *testing.T) { + m := NewMachine("p", nil) + defer m.Release() + + // struct{ Int; *MyInt; pkg.T; N int } — three embed spellings + one named + // field (must pass through untouched). + exprs := FieldTypeExprs{ + {Type: Nx("Int")}, // alias embed: resolved type int, spelled Int + {Type: &StarExpr{X: Nx("MyInt")}}, // pointer embed: name from elem + {Type: Sel(Nx("pkg"), "T")}, // qualified embed: name from selector + {NameExpr: *Nx("N"), Type: Nx("int")}, // named field: not an embed + } + fts := []FieldType{ + {Type: IntType}, + {Type: &PointerType{Elt: IntType}}, + {Type: IntType}, + {Name: "N", Type: IntType}, + } + for _, ft := range fts { + m.PushValue(TypedValue{T: gTypeType, V: toTypeValue(ft)}) + } + m.PushExpr(&StructTypeExpr{Fields: exprs}) + + m.doOpStructType() + + st := m.PopValue().V.(TypeValue).Type.(*StructType) + want := []struct { + name Name + embed bool + }{{"Int", true}, {"MyInt", true}, {"T", true}, {"N", false}} + for i, w := range want { + f := st.Fields[i] + if f.Name != w.name || f.Embedded != w.embed { + t.Fatalf("field %d: got (name=%s, embedded=%v), want (name=%s, embedded=%v)", + i, f.Name, f.Embedded, w.name, w.embed) + } + } +} diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index 75252be54b5..7749110fde5 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -1751,9 +1751,17 @@ func (tv *TypedValue) AssignToBlock(other TypedValue) { // allocated, *Allocator.AllocatePointer() is called separately, // as in OpRef. func (tv *TypedValue) GetPointerToFromTV(alloc *Allocator, store Store, path ValuePath) PointerValue { + return tv.getPointerToFromTV(alloc, store, path, "") +} + +// callerPath is the package of the code executing the selector; VPInterface +// resolution needs it to pick the right same-spelled unexported method +// (identity is package-qualified). Empty falls back to the dynamic type's +// package, which is only correct when no such collision exists (debugger). +func (tv *TypedValue) getPointerToFromTV(alloc *Allocator, store Store, path ValuePath, callerPath string) PointerValue { if debug { if tv.IsUndefined() { - panic("GetPointerToFromTV() on undefined value") + panic("getPointerToFromTV() on undefined value") } } @@ -1977,7 +1985,9 @@ func (tv *TypedValue) GetPointerToFromTV(alloc *Allocator, store Store, path Val if dtv.T.Kind() == InterfaceKind { panic("cannot resolve an interface path at static time") } - callerPath := dtv.T.GetPkgPath() + if callerPath == "" { + callerPath = dtv.T.GetPkgPath() + } tr, _, _, _, _ := findEmbeddedFieldType(callerPath, dtv.T, path.Name, nil) if len(tr) == 0 { panic(fmt.Sprintf("method %s not found in type %s", @@ -1985,7 +1995,7 @@ func (tv *TypedValue) GetPointerToFromTV(alloc *Allocator, store Store, path Val } btv := *dtv for i, path := range tr { - ptr := btv.GetPointerToFromTV(alloc, store, path) + ptr := btv.getPointerToFromTV(alloc, store, path, callerPath) if i == len(tr)-1 { return ptr // done } diff --git a/gnovm/pkg/gnolang/values_export.go b/gnovm/pkg/gnolang/values_export.go index e77a8451560..7f7fa6db534 100644 --- a/gnovm/pkg/gnolang/values_export.go +++ b/gnovm/pkg/gnolang/values_export.go @@ -410,6 +410,7 @@ func exportCopyFieldsWithRefs(fields []FieldType, seen map[Object]int) []FieldTy Type: exportRefOrCopyType(field.Type, seen), Embedded: field.Embedded, Tag: field.Tag, + PkgPath: field.PkgPath, } } return fieldsCpy diff --git a/gnovm/tests/files/alias2.gno b/gnovm/tests/files/alias2.gno new file mode 100644 index 00000000000..ab90b74fbd0 --- /dev/null +++ b/gnovm/tests/files/alias2.gno @@ -0,0 +1,32 @@ +// 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..595a8fe5ec8 --- /dev/null +++ b/gnovm/tests/files/alias5.gno @@ -0,0 +1,19 @@ +// Uverse builtin aliases follow the same embedded-field naming rule as user +// aliases (see alias2): the field is named by the alias as written, so +// struct{rune} stays distinct from struct{int32}, and struct{byte} from +// struct{uint8}. Matches Go. +package main + +func main() { + var x, y interface{} = struct{ rune }{}, struct{ int32 }{} + if x == y { + mustNot() + } + x, y = struct{ byte }{}, struct{ uint8 }{} + if x == y { + mustNot() + } +} +func mustNot() { panic(badMsg) } + +const badMsg = "FAIL" diff --git a/gnovm/tests/files/alias6.gno b/gnovm/tests/files/alias6.gno new file mode 100644 index 00000000000..ebee7853628 --- /dev/null +++ b/gnovm/tests/files/alias6.gno @@ -0,0 +1,31 @@ +// A struct embedding an interface alias takes the field name from the written +// spelling (the struct rule), NOT the resolved interface — so struct{ SAlias } +// (field "SAlias") is distinct from struct{ Stringer } (field "Stringer"), +// even though SAlias = Stringer. This is the opposite of the interface-embed +// rule (iface_embed_*), confirming the spelling-vs-flatten split is +// dispatched by container kind, not by the embedded type's kind. +package main + +type Stringer interface{ Str() string } +type SAlias = Stringer + +type T struct{} + +func (T) Str() string { return "hi" } + +func main() { + var x, y interface{} = struct{ SAlias }{}, struct{ Stringer }{} + if x == y { + panic("FAIL: struct{SAlias} == struct{Stringer}") + } + var z interface{} = struct{ SAlias }{} + if x != z { + panic("FAIL: struct{SAlias} != struct{SAlias}") + } + // the embedded field is named SAlias and promotes the method set + s := struct{ SAlias }{SAlias: T{}} + println(s.Str()) +} + +// Output: +// hi diff --git a/gnovm/tests/files/alias7.gno b/gnovm/tests/files/alias7.gno new file mode 100644 index 00000000000..1ef13421506 --- /dev/null +++ b/gnovm/tests/files/alias7.gno @@ -0,0 +1,31 @@ +// Alias-embed naming through the RUNTIME struct-construction path +// (doOpStructType): this combination — local type decl + struct assertion +// target + anonymous struct literal in a closure — is pinned (by +// instrumentation) to build struct{ Int } via the machine op rather than +// preprocess-only, and x == w observes that the runtime-built type keeps the +// alias spelling: it equals the preprocess-built struct{ Int } and stays +// distinct from struct{ int }. Matches Go. (TestDoOpStructType_EmbedNames +// drives the op directly for the same rule.) +package main + +type Int = int + +func main() { + type S struct{ Int } + var v interface{} = S{} + _, ok := v.(struct{ Int }) + println(ok) + f := func() interface{} { return struct{ Int }{} } + x := f() + println(x == v) + var w interface{} = struct{ Int }{} + println(x == w) + var u interface{} = struct{ int }{} + println(x == u) +} + +// Output: +// false +// false +// true +// false 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/extern/ifaceext/ifaceext.gno b/gnovm/tests/files/extern/ifaceext/ifaceext.gno new file mode 100644 index 00000000000..6617a3c31c9 --- /dev/null +++ b/gnovm/tests/files/extern/ifaceext/ifaceext.gno @@ -0,0 +1,13 @@ +package ifaceext + +type Str interface{ S() string } +type Sec interface{ sec() int } // unexported method: identity is pkgpath-qualified + +type SecImpl struct{} + +func (SecImpl) sec() int { return 7 } + +// UseSec selects sec from within its origin package: legitimate in Go no +// matter how the caller spelled the interface (the anonymous embed here +// flattens sec with the same ifaceext-qualified identity). +func UseSec(x interface{ Sec }) int { return x.sec() } diff --git a/gnovm/tests/files/extern/ifaceext2/ifaceext2.gno b/gnovm/tests/files/extern/ifaceext2/ifaceext2.gno new file mode 100644 index 00000000000..46667270b04 --- /dev/null +++ b/gnovm/tests/files/extern/ifaceext2/ifaceext2.gno @@ -0,0 +1,3 @@ +package ifaceext2 + +type Sec interface{ sec() int } diff --git a/gnovm/tests/files/extern/ifacemid/ifacemid.gno b/gnovm/tests/files/extern/ifacemid/ifacemid.gno new file mode 100644 index 00000000000..6b161bece1c --- /dev/null +++ b/gnovm/tests/files/extern/ifacemid/ifacemid.gno @@ -0,0 +1,7 @@ +package ifacemid + +import "filetests/extern/ifaceext" + +// Mid embeds ifaceext.Sec, so its (flattened) method set is {ifaceext.sec}; +// the unexported method's origin package stays ifaceext, not ifacemid. +type Mid interface{ ifaceext.Sec } diff --git a/gnovm/tests/files/iface_embed_conflict.gno b/gnovm/tests/files/iface_embed_conflict.gno new file mode 100644 index 00000000000..ccce6e824f3 --- /dev/null +++ b/gnovm/tests/files/iface_embed_conflict.gno @@ -0,0 +1,23 @@ +// Embedding two interfaces with the same method name but conflicting +// signatures is rejected, matching Go's "duplicate method" compile error. +// go/types reports it (TypeCheckError); the VM's flattenInterfaceMethods +// reports it as a positioned preprocess Error when go/types is not run. +package main + +type A interface{ M() int } +type B interface{ M() string } + +func main() { + var x interface { + A + B + } + _ = x + println("unreached") +} + +// Error: +// main/iface_embed_conflict.gno:11:8-14:3: duplicate method M with conflicting types in interface + +// TypeCheckError: +// main/iface_embed_conflict.gno:13:3: duplicate method M; main/iface_embed_conflict.gno:12:3: other declaration of method M diff --git a/gnovm/tests/files/iface_embed_field_shadow.gno b/gnovm/tests/files/iface_embed_field_shadow.gno new file mode 100644 index 00000000000..19e1531bff1 --- /dev/null +++ b/gnovm/tests/files/iface_embed_field_shadow.gno @@ -0,0 +1,26 @@ +package main + +import "filetests/extern/ifaceext" + +// F has an unexported main-package FIELD named sec at depth 0 and the +// promoted ifaceext.sec METHOD at depth 1 via the embed. The identities are +// distinct: the field must not block F from implementing ifaceext.Sec, and +// f.sec from main must select the field (StructType lookup pins for the +// skip-gated-match rule on PR #5739). +type F struct { + sec int + ifaceext.SecImpl +} + +func fieldsel(f F) int { return f.sec } + +func main() { + var f F + f.sec = 42 + println(ifaceext.UseSec(f)) // promoted SecImpl.sec + println(fieldsel(f)) // main's field +} + +// Output: +// 7 +// 42 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 diff --git a/gnovm/tests/files/iface_embed_more.gno b/gnovm/tests/files/iface_embed_more.gno new file mode 100644 index 00000000000..1f507ae40c4 --- /dev/null +++ b/gnovm/tests/files/iface_embed_more.gno @@ -0,0 +1,64 @@ +// Interface identity is the fully-flattened method set (matching Go) across +// multi-level embedding, order, mixed embed+direct, distinctness, the empty +// interface, and dynamic dispatch through an embed. +package main + +type Str interface{ S() string } +type Rdr interface{ R() int } +type Wtr interface{ W() int } +type L1 interface{ Str } +type L2 interface{ L1 } // multi-level + +type T struct{} + +func (T) S() string { return "s" } + +func eq(a, b interface{}) bool { return a == b } + +func main() { + println("multi-level:", eq(struct{ X interface{ L2 } }{}, struct{ X interface{ S() string } }{})) + println("order:", eq(struct { + X interface { + Rdr + Wtr + } + }{}, struct { + X interface { + Wtr + Rdr + } + }{})) + println("mixed:", eq(struct { + X interface { + Str + R() int + } + }{}, struct { + X interface { + S() string + Rdr + } + }{})) + println("superset!=subset:", eq(struct { + X interface { + Rdr + Wtr + } + }{}, struct{ X interface{ Rdr } }{})) + println("empty-embed:", eq(struct{ X interface{ interface{} } }{}, struct{ X interface{} }{})) + var v interface{} = T{} + s, ok := v.(interface{ Str }) + if ok { + println("dispatch:", s.S()) + } else { + println("dispatch: !ok") + } +} + +// Output: +// multi-level: true +// order: true +// mixed: true +// superset!=subset: false +// empty-embed: true +// dispatch: s diff --git a/gnovm/tests/files/iface_embed_provenance.gno b/gnovm/tests/files/iface_embed_provenance.gno new file mode 100644 index 00000000000..1da3aafcb00 --- /dev/null +++ b/gnovm/tests/files/iface_embed_provenance.gno @@ -0,0 +1,26 @@ +// The same interface method set must have one TypeID regardless of how it was +// built: an unexported method listed directly (PkgPath left unstamped by the +// no-embed fast path) vs promoted through an embed (PkgPath stamped with the +// origin package). The TypeID sort and emission must use the same package +// fallback, else the unstamped and stamped forms diverge. Guards the +// construction-level half of the sort/emit provenance fix (PR #5739 review); +// the persisted-decode half is covered by TestInterfaceTypeID_PkgPathProvenance. +package main + +type I = interface { + M() int + z() int +} + +func main() { + var a, b interface{} = struct { + X interface { + M() int + z() int + } + }{}, struct{ X interface{ I } }{} + println("direct==embedded:", a == b) +} + +// Output: +// direct==embedded: true diff --git a/gnovm/tests/files/iface_embed_same_name.gno b/gnovm/tests/files/iface_embed_same_name.gno new file mode 100644 index 00000000000..717319b6b55 --- /dev/null +++ b/gnovm/tests/files/iface_embed_same_name.gno @@ -0,0 +1,31 @@ +package main + +import "filetests/extern/ifaceext" + +// omarsy's review case on PR #5739: I holds two distinct unexported methods +// both named sec (main.sec and the flattened ifaceext.sec). First-match-wins +// name lookup makes I fail its own implements check — even assigning a value +// of type I to type I panics "main.I does not implement main.I". + +type I interface { + sec() int + ifaceext.Sec +} + +func f(x I) int { + var y I = x // assign to the SAME type + return y.sec() +} + +// T satisfies both: own main.sec plus promoted ifaceext.sec. +type T struct{ ifaceext.SecImpl } + +func (T) sec() int { return 1 } + +func main() { + var t T + println(f(t)) +} + +// Output: +// 1 diff --git a/gnovm/tests/files/iface_embed_sel_order.gno b/gnovm/tests/files/iface_embed_sel_order.gno new file mode 100644 index 00000000000..f454396df7c --- /dev/null +++ b/gnovm/tests/files/iface_embed_sel_order.gno @@ -0,0 +1,38 @@ +package main + +import "filetests/extern/ifaceext" + +// Two distinct unexported methods named sec (main.sec and ifaceext.sec) in +// one interface. Selector resolution from main must find main's own sec +// regardless of declaration order (davd-gzl's review case on PR #5739). + +// embed-first: foreign sealed ifaceext.sec listed before main's own sec. +func selEmbedFirst(x interface { + ifaceext.Sec + sec() int +}) int { + return x.sec() +} + +// own-first: main's own sec listed before the foreign embed. +func selOwnFirst(x interface { + sec() int + ifaceext.Sec +}) int { + return x.sec() +} + +// T satisfies both: own main.sec plus promoted ifaceext.sec. +type T struct{ ifaceext.SecImpl } + +func (T) sec() int { return 1 } + +func main() { + var t T + println(selEmbedFirst(t)) + println(selOwnFirst(t)) +} + +// Output: +// 1 +// 1 diff --git a/gnovm/tests/files/iface_embed_xpkg.gno b/gnovm/tests/files/iface_embed_xpkg.gno new file mode 100644 index 00000000000..65bbdb6d2b0 --- /dev/null +++ b/gnovm/tests/files/iface_embed_xpkg.gno @@ -0,0 +1,58 @@ +// Cross-package interface embedding. An embedded interface's EXPORTED methods +// flatten by signature, so a cross-package embed equals the explicit method +// set. An UNEXPORTED method keeps its origin-package identity through +// flattening: it stays distinct from a same-named local unexported method, a +// local type cannot satisfy another package's sealed interface, and two +// same-named unexported methods from different packages coexist as distinct +// methods. Matches Go (guards the cross-package soundness fix; see adr/pr5739). +package main + +import ( + "filetests/extern/ifaceext" + "filetests/extern/ifaceext2" +) + +type Tloc struct{} + +func (Tloc) sec() int { return 1 } + +func eq(a, b interface{}) bool { return a == b } + +func main() { + println("exported-embed:", eq(struct{ X interface{ ifaceext.Str } }{}, struct{ X interface{ S() string } }{})) + println("unexported-distinct:", eq(struct{ X interface{ ifaceext.Sec } }{}, struct{ X interface{ sec() int } }{})) + var v interface{} = Tloc{} + _, sealed := v.(interface{ ifaceext.Sec }) + println("sealed-bypass:", sealed) + _, local := v.(interface{ sec() int }) + println("local-ok:", local) + // two unexported sec() from different packages are distinct and coexist: + // interface{ ifaceext.Sec; ifaceext2.Sec } has both, distinct from one. + dup := struct { + X interface { + ifaceext.Sec + ifaceext2.Sec + } + }{} + println("dup-coexist==self:", eq(dup, struct { + X interface { + ifaceext.Sec + ifaceext2.Sec + } + }{})) + println("dup!=one:", !eq(dup, struct{ X interface{ ifaceext.Sec } }{})) + // sec stays selectable from within its origin package, even through + // main's re-spelled interface: the access gate keys on the method's + // origin (ifaceext), and main->ifaceext interface conversion passes it. + var s interface{ ifaceext.Sec } = ifaceext.SecImpl{} + println("origin-call:", ifaceext.UseSec(s)) +} + +// Output: +// exported-embed: true +// unexported-distinct: false +// sealed-bypass: false +// local-ok: true +// dup-coexist==self: true +// dup!=one: true +// origin-call: 7 diff --git a/gnovm/tests/files/iface_embed_xpkg2.gno b/gnovm/tests/files/iface_embed_xpkg2.gno new file mode 100644 index 00000000000..d91cfcdbc9a --- /dev/null +++ b/gnovm/tests/files/iface_embed_xpkg2.gno @@ -0,0 +1,20 @@ +// Multi-level cross-package embedding: ifacemid.Mid embeds ifaceext.Sec (an +// unexported sec() owned by ifaceext). Embedding Mid here must keep sec's +// origin package as ifaceext through the second hoist (ifaceext -> ifacemid -> +// main), so interface{ ifacemid.Mid } and interface{ ifaceext.Sec } have the +// same method set {ifaceext.sec} and one TypeID. Guards the origin-preserving +// branch in flattenInterfaceMethods (m.PkgPath kept over the embed's PkgPath). +package main + +import ( + "filetests/extern/ifaceext" + "filetests/extern/ifacemid" +) + +func main() { + var a, b interface{} = struct{ X interface{ ifacemid.Mid } }{}, struct{ X interface{ ifaceext.Sec } }{} + println("midSec==sec:", a == b) +} + +// Output: +// midSec==sec: true diff --git a/gnovm/tests/files/iface_embed_xpkg_access.gno b/gnovm/tests/files/iface_embed_xpkg_access.gno new file mode 100644 index 00000000000..410aec899c5 --- /dev/null +++ b/gnovm/tests/files/iface_embed_xpkg_access.gno @@ -0,0 +1,18 @@ +// Selecting a cross-package unexported method is rejected at compile time +// even after flattening hoists it into a locally-spelled interface: the +// access gate keys on the method's origin package (ifaceext), not the +// enclosing interface's (main). Matches Go and pre-flattening master. +package main + +import "filetests/extern/ifaceext" + +func main() { + var x interface{ ifaceext.Sec } = ifaceext.SecImpl{} + println(x.sec()) +} + +// Error: +// main/iface_embed_xpkg_access.gno:11:10-15: cannot access interface {sec func() int}.sec from main + +// TypeCheckError: +// main/iface_embed_xpkg_access.gno:11:12: x.sec undefined (cannot refer to unexported method sec) diff --git a/gnovm/tests/files/zrealm_crossrealm21.gno b/gnovm/tests/files/zrealm_crossrealm21.gno index 7342dc356fe..5cda65833d7 100644 --- a/gnovm/tests/files/zrealm_crossrealm21.gno +++ b/gnovm/tests/files/zrealm_crossrealm21.gno @@ -86,7 +86,7 @@ func main(cur realm) { // }, // "Index": "0", // u[07e973c6aeab1f1cb747d0903cb1e4d369226376:2](0)= -// @@ -105,7 +105,7 @@ +// @@ -106,7 +106,7 @@ // }, // "V": { // "@type": "/gno.RefValue", diff --git a/gnovm/tests/files/zrealm_crossrealm22.gno b/gnovm/tests/files/zrealm_crossrealm22.gno index 63bf1ab2194..d2a4f234aed 100644 --- a/gnovm/tests/files/zrealm_crossrealm22.gno +++ b/gnovm/tests/files/zrealm_crossrealm22.gno @@ -118,6 +118,7 @@ func main(cur realm) { // { // "Embedded": false, // "Name": ".res.0", +// "PkgPath": "", // "Tag": "", // "Type": { // "@type": "/gno.RefType", @@ -183,7 +184,7 @@ func main(cur realm) { // }, // "Index": "0", // u[07e973c6aeab1f1cb747d0903cb1e4d369226376:2](0)= -// @@ -268,7 +268,7 @@ +// @@ -276,7 +276,7 @@ // }, // "V": { // "@type": "/gno.RefValue", @@ -325,7 +326,7 @@ func main(cur realm) { // "OwnerID": "06279d1e03ecb38b84822f9caf584bd16913470d:2", // "RefCount": "1" // }, -// @@ -24,7 +24,6 @@ +// @@ -25,7 +25,6 @@ // }, // "V": { // "@type": "/gno.RefValue", @@ -334,7 +335,7 @@ func main(cur realm) { // } // } // u[07e973c6aeab1f1cb747d0903cb1e4d369226376:2](0)= -// @@ -268,7 +268,7 @@ +// @@ -276,7 +276,7 @@ // }, // "V": { // "@type": "/gno.RefValue", @@ -433,6 +434,7 @@ func main(cur realm) { // { // "Embedded": false, // "Name": ".res.0", +// "PkgPath": "", // "Tag": "", // "Type": { // "@type": "/gno.RefType", @@ -464,7 +466,7 @@ func main(cur realm) { // "Parent": null, // "PkgPath": "gno.land/r/tests/vm/crossrealm_b", // u[07e973c6aeab1f1cb747d0903cb1e4d369226376:2](0)= -// @@ -268,7 +268,7 @@ +// @@ -276,7 +276,7 @@ // }, // "V": { // "@type": "/gno.RefValue", diff --git a/gnovm/tests/files/zrealm_crossrealm28.gno b/gnovm/tests/files/zrealm_crossrealm28.gno index 40b306bcee6..331c85edd79 100644 --- a/gnovm/tests/files/zrealm_crossrealm28.gno +++ b/gnovm/tests/files/zrealm_crossrealm28.gno @@ -52,7 +52,7 @@ func main(cur realm) { // } // } // u[06279d1e03ecb38b84822f9caf584bd16913470d:2](0)= -// @@ -118,7 +118,7 @@ +// @@ -120,7 +120,7 @@ // }, // "V": { // "@type": "/gno.RefValue", diff --git a/gnovm/tests/files/zrealm_natbind0.gno b/gnovm/tests/files/zrealm_natbind0.gno index d2a51054dbc..f7142dd84f2 100644 --- a/gnovm/tests/files/zrealm_natbind0.gno +++ b/gnovm/tests/files/zrealm_natbind0.gno @@ -27,7 +27,7 @@ func main(cur realm) { // Realm: // finalizerealm["gno.land/r/test"] // u[08ada09dee16d791fd406d629fe29bb0ed084a30:3](-2)= -// @@ -17,15 +17,15 @@ +// @@ -18,15 +18,15 @@ // "Tag": "", // "Type": { // "@type": "/gno.PrimitiveType", diff --git a/gnovm/tests/files/zrealm_stdlib_ref.gno b/gnovm/tests/files/zrealm_stdlib_ref.gno index 26569269b3f..d2e0562b79c 100644 --- a/gnovm/tests/files/zrealm_stdlib_ref.gno +++ b/gnovm/tests/files/zrealm_stdlib_ref.gno @@ -20,7 +20,7 @@ func main(cur realm) { // Realm: // finalizerealm["gno.land/r/test"] // u[08ada09dee16d791fd406d629fe29bb0ed084a30:3](199)= -// @@ -2,9 +2,40 @@ +// @@ -2,9 +2,42 @@ // "ObjectInfo": { // "ID": "08ada09dee16d791fd406d629fe29bb0ed084a30:3", // "LastObjectSize": "190", @@ -37,6 +37,7 @@ func main(cur realm) { // + { // + "Embedded": false, // + "Name": "i", +// + "PkgPath": "", // + "Tag": "", // + "Type": { // + "@type": "/gno.PrimitiveType", @@ -48,6 +49,7 @@ func main(cur realm) { // + { // + "Embedded": false, // + "Name": ".res.0", +// + "PkgPath": "", // + "Tag": "", // + "Type": { // + "@type": "/gno.PrimitiveType",