1
0
mirror of https://github.com/golang/go synced 2024-07-05 09:50:19 +00:00

cmd/compile/internal/noder: shape-based stenciling for unified IR

This CL switches unified IR to use shape-based stenciling with runtime
dictionaries, like the existing non-unified frontend. Specifically,
when instantiating generic functions and types `X[T]`, we now also
instantiated shaped variants `X[shapify(T)]` that can be shared by
`T`'s with common underlying types.

For example, for generic function `F`, `F[int](args...)` will be
rewritten to `F[go.shape.int](&.dict.F[int], args...)`.

For generic type `T` with method `M` and value `t` of type `T[int]`,
`t.M(args...)` will be rewritten to `T[go.shape.int].M(t,
&.dict.T[int], args...)`.

Two notable distinctions from the non-unified frontend:

1. For simplicity, currently shaping is limited to simply converting
type arguments to their underlying type. Subsequent CLs will implement
more aggressive shaping.

2. For generic types, a single dictionary is generated to be shared by
all methods, rather than separate dictionaries for each method. I
originally went with this design because I have an idea of changing
interface calls to pass the itab pointer via the closure
register (which should have zero overhead), and then the interface
wrappers for generic methods could use the *runtime.itab to find the
runtime dictionary that corresponds to the dynamic type. This would
allow emitting fewer method wrappers.

However, this choice does have the consequence that currently even if
a method is unused and its code is pruned by the linker, it may have
produced runtime dictionary entries that need to be kept alive anyway.

I'm open to changing this to generate per-method dictionaries, though
this would require changing the unified IR export data format; so it
would be best to make this decision before Go 1.20.

The other option is making the linker smarter about pruning unneeded
dictionary entries, like how it already prunes itab entries. For
example, the runtime dictionary for `T[int]` could have a `R_DICTTYPE`
meta-relocation against symbol `.dicttype.T[go.shape.int]` that
declares it's a dictionary associated with that type; and then each
method on `T[go.shape.T]` could have `R_DICTUSE` meta-relocations
against `.dicttype.T[go.shape.T]+offset` indicating which fields
within dictionaries of that type need to be preserved.

Change-Id: I369580b1d93d19640a4b5ecada4f6231adcce3fd
Reviewed-on: https://go-review.googlesource.com/c/go/+/421821
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
Reviewed-by: Cuong Manh Le <cuong.manhle.vn@gmail.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
Matthew Dempsky 2022-08-06 16:40:56 -07:00
parent a2c2f06cad
commit 38edd9bd8d
8 changed files with 1457 additions and 288 deletions

View File

@ -41,6 +41,41 @@ func Call(call *ir.CallExpr) {
return
}
if base.Debug.Unified != 0 {
// N.B., stencil.go converts shape-typed values to interface type
// using OEFACE instead of OCONVIFACE, so devirtualization fails
// above instead. That's why this code is specific to unified IR.
// If typ is a shape type, then it was a type argument originally
// and we'd need an indirect call through the dictionary anyway.
// We're unable to devirtualize this call.
if typ.IsShape() {
return
}
// If typ *has* a shape type, then it's an shaped, instantiated
// type like T[go.shape.int], and its methods (may) have an extra
// dictionary parameter. We could devirtualize this call if we
// could derive an appropriate dictionary argument.
//
// TODO(mdempsky): If typ has has a promoted non-generic method,
// then that method won't require a dictionary argument. We could
// still devirtualize those calls.
//
// TODO(mdempsky): We have the *runtime.itab in recv.TypeWord. It
// should be possible to compute the represented type's runtime
// dictionary from this (e.g., by adding a pointer from T[int]'s
// *runtime._type to .dict.T[int]; or by recognizing static
// references to go:itab.T[int],iface and constructing a direct
// reference to .dict.T[int]).
if typ.HasShape() {
if base.Flag.LowerM != 0 {
base.WarnfAt(call.Pos(), "cannot devirtualize %v: shaped receiver %v", call, typ)
}
return
}
}
dt := ir.NewTypeAssertExpr(sel.Pos(), sel.X, nil)
dt.SetType(typ)
x := typecheck.Callee(ir.NewSelectorExpr(sel.Pos(), ir.OXDOT, dt, sel.Sel))

File diff suppressed because it is too large Load Diff

View File

@ -120,25 +120,47 @@ func unified(noders []*noder) {
base.ExitIfErrors() // just in case
}
// readBodies reads in bodies for any
// readBodies iteratively expands all pending dictionaries and
// function bodies.
func readBodies(target *ir.Package) {
// Don't use range--bodyIdx can add closures to todoBodies.
for len(todoBodies) > 0 {
// The order we expand bodies doesn't matter, so pop from the end
// to reduce todoBodies reallocations if it grows further.
fn := todoBodies[len(todoBodies)-1]
todoBodies = todoBodies[:len(todoBodies)-1]
for {
// The order we expand dictionaries and bodies doesn't matter, so
// pop from the end to reduce todoBodies reallocations if it grows
// further.
//
// However, we do at least need to flush any pending dictionaries
// before reading bodies, because bodies might reference the
// dictionaries.
pri, ok := bodyReader[fn]
assert(ok)
pri.funcBody(fn)
// Instantiated generic function: add to Decls for typechecking
// and compilation.
if fn.OClosure == nil && len(pri.dict.targs) != 0 {
target.Decls = append(target.Decls, fn)
if len(todoDicts) > 0 {
fn := todoDicts[len(todoDicts)-1]
todoDicts = todoDicts[:len(todoDicts)-1]
fn()
continue
}
if len(todoBodies) > 0 {
fn := todoBodies[len(todoBodies)-1]
todoBodies = todoBodies[:len(todoBodies)-1]
pri, ok := bodyReader[fn]
assert(ok)
pri.funcBody(fn)
// Instantiated generic function: add to Decls for typechecking
// and compilation.
if fn.OClosure == nil && len(pri.dict.targs) != 0 {
target.Decls = append(target.Decls, fn)
}
continue
}
break
}
todoDicts = nil
todoBodies = nil
}
@ -247,7 +269,7 @@ func readPackage(pr *pkgReader, importpkg *types.Pkg, localStub bool) {
path, name, code := r.p.PeekObj(idx)
if code != pkgbits.ObjStub {
objReader[types.NewPkg(path, "").Lookup(name)] = pkgReaderIndex{pr, idx, nil, nil}
objReader[types.NewPkg(path, "").Lookup(name)] = pkgReaderIndex{pr, idx, nil, nil, nil}
}
}
@ -271,7 +293,7 @@ func readPackage(pr *pkgReader, importpkg *types.Pkg, localStub bool) {
sym := types.NewPkg(path, "").Lookup(name)
if _, ok := importBodyReader[sym]; !ok {
importBodyReader[sym] = pkgReaderIndex{pr, idx, nil, nil}
importBodyReader[sym] = pkgReaderIndex{pr, idx, nil, nil, nil}
}
}

View File

@ -172,12 +172,37 @@ type writerDict struct {
// derivedIdx maps a Type to its corresponding index within the
// derived slice, if present.
derivedIdx map[types2.Type]pkgbits.Index
// These slices correspond to entries in the runtime dictionary.
typeParamMethodExprs []writerMethodExprInfo
subdicts []objInfo
rtypes []typeInfo
itabs []itabInfo
}
type itabInfo struct {
typ typeInfo
iface typeInfo
}
// typeParamIndex returns the index of the given type parameter within
// the dictionary. This may differ from typ.Index() when there are
// implicit type parameters due to defined types declared within a
// generic function or method.
func (dict *writerDict) typeParamIndex(typ *types2.TypeParam) int {
for idx, implicit := range dict.implicits {
if implicit.Type().(*types2.TypeParam) == typ {
return idx
}
}
return len(dict.implicits) + typ.Index()
}
// A derivedInfo represents a reference to an encoded generic Go type.
type derivedInfo struct {
idx pkgbits.Index
needed bool
needed bool // TODO(mdempsky): Remove.
}
// A typeInfo represents a reference to an encoded Go type.
@ -234,6 +259,75 @@ func (info objInfo) equals(other objInfo) bool {
return true
}
type writerMethodExprInfo struct {
typeParamIdx int
methodInfo selectorInfo
}
// typeParamMethodExprIdx returns the index where the given encoded
// method expression function pointer appears within this dictionary's
// type parameters method expressions section, adding it if necessary.
func (dict *writerDict) typeParamMethodExprIdx(typeParamIdx int, methodInfo selectorInfo) int {
newInfo := writerMethodExprInfo{typeParamIdx, methodInfo}
for idx, oldInfo := range dict.typeParamMethodExprs {
if oldInfo == newInfo {
return idx
}
}
idx := len(dict.typeParamMethodExprs)
dict.typeParamMethodExprs = append(dict.typeParamMethodExprs, newInfo)
return idx
}
// subdictIdx returns the index where the given encoded object's
// runtime dictionary appears within this dictionary's subdictionary
// section, adding it if necessary.
func (dict *writerDict) subdictIdx(newInfo objInfo) int {
for idx, oldInfo := range dict.subdicts {
if oldInfo.equals(newInfo) {
return idx
}
}
idx := len(dict.subdicts)
dict.subdicts = append(dict.subdicts, newInfo)
return idx
}
// rtypeIdx returns the index where the given encoded type's
// *runtime._type value appears within this dictionary's rtypes
// section, adding it if necessary.
func (dict *writerDict) rtypeIdx(newInfo typeInfo) int {
for idx, oldInfo := range dict.rtypes {
if oldInfo == newInfo {
return idx
}
}
idx := len(dict.rtypes)
dict.rtypes = append(dict.rtypes, newInfo)
return idx
}
// itabIdx returns the index where the given encoded type pair's
// *runtime.itab value appears within this dictionary's itabs section,
// adding it if necessary.
func (dict *writerDict) itabIdx(typInfo, ifaceInfo typeInfo) int {
newInfo := itabInfo{typInfo, ifaceInfo}
for idx, oldInfo := range dict.itabs {
if oldInfo == newInfo {
return idx
}
}
idx := len(dict.itabs)
dict.itabs = append(dict.itabs, newInfo)
return idx
}
func (pw *pkgWriter) newWriter(k pkgbits.RelocKind, marker pkgbits.SyncMarker) *writer {
return &writer{
Encoder: pw.NewEncoder(k, marker),
@ -412,19 +506,9 @@ func (pw *pkgWriter) typIdx(typ types2.Type, dict *writerDict) typeInfo {
w.obj(obj, targs)
case *types2.TypeParam:
index := func() int {
for idx, name := range w.dict.implicits {
if name.Type().(*types2.TypeParam) == typ {
return idx
}
}
return len(w.dict.implicits) + typ.Index()
}()
w.derived = true
w.Code(pkgbits.TypeTypeParam)
w.Len(index)
w.Len(w.dict.typeParamIndex(typ))
case *types2.Array:
w.Code(pkgbits.TypeArray)
@ -757,6 +841,33 @@ func (w *writer) objDict(obj types2.Object, dict *writerDict) {
w.Bool(typ.needed)
}
// Write runtime dictionary information.
//
// N.B., the go/types importer reads up to the section, but doesn't
// read any further, so it's safe to change. (See TODO above.)
w.Len(len(dict.typeParamMethodExprs))
for _, info := range dict.typeParamMethodExprs {
w.Len(info.typeParamIdx)
w.selectorInfo(info.methodInfo)
}
w.Len(len(dict.subdicts))
for _, info := range dict.subdicts {
w.objInfo(info)
}
w.Len(len(dict.rtypes))
for _, info := range dict.rtypes {
w.typInfo(info)
}
w.Len(len(dict.itabs))
for _, info := range dict.itabs {
w.typInfo(info.typ)
w.typInfo(info.iface)
}
assert(len(dict.derived) == nderived)
}
@ -960,17 +1071,21 @@ func (w *writer) funcargs(sig *types2.Signature) {
func (w *writer) funcarg(param *types2.Var, result bool) {
if param.Name() != "" || result {
w.addLocal(param)
w.addLocal(param, true)
}
}
// addLocal records the declaration of a new local variable.
func (w *writer) addLocal(obj *types2.Var) {
w.Sync(pkgbits.SyncAddLocal)
func (w *writer) addLocal(obj *types2.Var, isParam bool) {
idx := len(w.localsIdx)
if w.p.SyncMarkers() {
w.Int(idx)
if !isParam {
w.Sync(pkgbits.SyncAddLocal)
if w.p.SyncMarkers() {
w.Int(idx)
}
}
if w.localsIdx == nil {
w.localsIdx = make(map[*types2.Var]int)
}
@ -1161,7 +1276,7 @@ func (w *writer) assign(expr syntax.Expr) {
// TODO(mdempsky): Minimize locals index size by deferring
// this until the variables actually come into scope.
w.addLocal(obj)
w.addLocal(obj, false)
return
}
}
@ -1424,7 +1539,7 @@ func (w *writer) switchStmt(stmt *syntax.SwitchStmt) {
obj := obj.(*types2.Var)
w.typ(obj.Type())
w.addLocal(obj)
w.addLocal(obj, false)
}
w.stmts(clause.Body)
@ -1494,7 +1609,8 @@ func (w *writer) expr(expr syntax.Expr) {
obj := obj.(*types2.Func)
w.Code(exprFuncInst)
w.obj(obj, targs)
w.pos(expr)
w.funcInst(obj, targs)
return
}
@ -1540,9 +1656,9 @@ func (w *writer) expr(expr syntax.Expr) {
case types2.MethodVal:
w.Code(exprMethodVal)
w.recvExpr(expr, sel)
typ := w.recvExpr(expr, sel)
w.pos(expr)
w.selector(sel.Obj())
w.methodExpr(expr, typ, sel)
case types2.MethodExpr:
w.Code(exprMethodExpr)
@ -1551,9 +1667,27 @@ func (w *writer) expr(expr syntax.Expr) {
assert(ok)
assert(tv.IsType())
w.typ(tv.Type)
index := sel.Index()
implicits := index[:len(index)-1]
typ := tv.Type
w.typ(typ)
w.Len(len(implicits))
for _, ix := range implicits {
w.Len(ix)
typ = deref2(typ).Underlying().(*types2.Struct).Field(ix).Type()
}
recv := sel.Obj().(*types2.Func).Type().(*types2.Signature).Recv().Type()
if w.Bool(isPtrTo(typ, recv)) { // need deref
typ = recv
} else if w.Bool(isPtrTo(recv, typ)) { // need addr
typ = recv
}
w.pos(expr)
w.selector(sel.Obj())
w.methodExpr(expr, typ, sel)
}
case *syntax.IndexExpr:
@ -1695,18 +1829,28 @@ func (w *writer) expr(expr syntax.Expr) {
}
writeFunExpr := func() {
if selector, ok := unparen(expr.Fun).(*syntax.SelectorExpr); ok {
fun := unparen(expr.Fun)
if selector, ok := fun.(*syntax.SelectorExpr); ok {
if sel, ok := w.p.info.Selections[selector]; ok && sel.Kind() == types2.MethodVal {
w.recvExpr(selector, sel)
w.Bool(true) // method call
w.pos(selector)
w.selector(sel.Obj())
typ := w.recvExpr(selector, sel)
w.methodExpr(selector, typ, sel)
return
}
}
w.expr(expr.Fun)
w.Bool(false) // not a method call (i.e., normal function call)
if obj, inst := lookupObj(w.p.info, fun); w.Bool(obj != nil && inst.TypeArgs.Len() != 0) {
obj := obj.(*types2.Func)
w.pos(fun)
w.funcInst(obj, inst.TypeArgs)
return
}
w.expr(fun)
}
sigType := types2.CoreType(tv.Type).(*types2.Signature)
@ -1742,7 +1886,8 @@ func (w *writer) optExpr(expr syntax.Expr) {
}
// recvExpr writes out expr.X, but handles any implicit addressing,
// dereferencing, and field selections.
// dereferencing, and field selections appropriate for the method
// selection.
func (w *writer) recvExpr(expr *syntax.SelectorExpr, sel *types2.Selection) types2.Type {
index := sel.Index()
implicits := index[:len(index)-1]
@ -1758,13 +1903,6 @@ func (w *writer) recvExpr(expr *syntax.SelectorExpr, sel *types2.Selection) type
w.Len(ix)
}
isPtrTo := func(from, to types2.Type) bool {
if from, ok := from.(*types2.Pointer); ok {
return types2.Identical(from.Elem(), to)
}
return false
}
recv := sel.Obj().(*types2.Func).Type().(*types2.Signature).Recv().Type()
if w.Bool(isPtrTo(typ, recv)) { // needs deref
typ = recv
@ -1775,6 +1913,84 @@ func (w *writer) recvExpr(expr *syntax.SelectorExpr, sel *types2.Selection) type
return typ
}
// funcInst writes a reference to an instantiated function.
func (w *writer) funcInst(obj *types2.Func, targs *types2.TypeList) {
info := w.p.objInstIdx(obj, targs, w.dict)
// Type arguments list contains derived types; we can emit a static
// call to the shaped function, but need to dynamically compute the
// runtime dictionary pointer.
if w.Bool(info.anyDerived()) {
w.Len(w.dict.subdictIdx(info))
return
}
// Type arguments list is statically known; we can emit a static
// call with a statically reference to the respective runtime
// dictionary.
w.objInfo(info)
}
// methodExpr writes out a reference to the method selected by
// expr. sel should be the corresponding types2.Selection, and recv
// the type produced after any implicit addressing, dereferencing, and
// field selection. (Note: recv might differ from sel.Obj()'s receiver
// parameter in the case of interface types, and is needed for
// handling type parameter methods.)
func (w *writer) methodExpr(expr *syntax.SelectorExpr, recv types2.Type, sel *types2.Selection) {
fun := sel.Obj().(*types2.Func)
sig := fun.Type().(*types2.Signature)
w.typ(recv)
w.signature(sig)
w.pos(expr)
w.selector(fun)
// Method on a type parameter. These require an indirect call
// through the current function's runtime dictionary.
if typeParam, ok := recv.(*types2.TypeParam); w.Bool(ok) {
typeParamIdx := w.dict.typeParamIndex(typeParam)
methodInfo := w.p.selectorIdx(fun)
w.Len(w.dict.typeParamMethodExprIdx(typeParamIdx, methodInfo))
return
}
if isInterface(recv) != isInterface(sig.Recv().Type()) {
w.p.fatalf(expr, "isInterface inconsistency: %v and %v", recv, sig.Recv().Type())
}
if !isInterface(recv) {
if named, ok := deref2(recv).(*types2.Named); ok {
obj, targs := splitNamed(named)
info := w.p.objInstIdx(obj, targs, w.dict)
// Method on a derived receiver type. These can be handled by a
// static call to the shaped method, but require dynamically
// looking up the appropriate dictionary argument in the current
// function's runtime dictionary.
if w.p.hasImplicitTypeParams(obj) || info.anyDerived() {
w.Bool(true) // dynamic subdictionary
w.Len(w.dict.subdictIdx(info))
return
}
// Method on a fully known receiver type. These can be handled
// by a static call to the shaped method, and with a static
// reference to the receiver type's dictionary.
if targs.Len() != 0 {
w.Bool(false) // no dynamic subdictionary
w.Bool(true) // static dictionary
w.objInfo(info)
return
}
}
}
w.Bool(false) // no dynamic subdictionary
w.Bool(false) // no static dictionary
}
// multiExpr writes a sequence of expressions, where the i'th value is
// implicitly converted to dstType(i). It also handles when exprs is a
// single, multi-valued expression (e.g., the multi-valued argument in
@ -1930,18 +2146,34 @@ func (w *writer) exprs(exprs []syntax.Expr) {
// rtype writes information so that the reader can construct an
// expression of type *runtime._type representing typ.
func (w *writer) rtype(typ types2.Type) {
typ = types2.Default(typ)
w.Sync(pkgbits.SyncRType)
w.typNeeded(typ)
info := w.p.typIdx(typ, w.dict)
if w.Bool(info.derived) {
w.Len(w.dict.rtypeIdx(info))
} else {
w.typInfo(info)
}
}
// typNeeded writes a reference to typ, and records that its
// *runtime._type is needed.
func (w *writer) typNeeded(typ types2.Type) {
info := w.p.typIdx(typ, w.dict)
w.typInfo(info)
func isUntyped(typ types2.Type) bool {
basic, ok := typ.(*types2.Basic)
return ok && basic.Info()&types2.IsUntyped != 0
}
if info.derived {
w.dict.derived[info.idx].needed = true
func (w *writer) itab(typ, iface types2.Type) {
typ = types2.Default(typ)
iface = types2.Default(iface)
typInfo := w.p.typIdx(typ, w.dict)
ifaceInfo := w.p.typIdx(iface, w.dict)
if w.Bool(typInfo.derived || ifaceInfo.derived) {
w.Len(w.dict.itabIdx(typInfo, ifaceInfo))
} else {
w.typInfo(typInfo)
w.typInfo(ifaceInfo)
}
}
@ -1949,8 +2181,7 @@ func (w *writer) typNeeded(typ types2.Type) {
// expressions for converting from src to dst.
func (w *writer) convRTTI(src, dst types2.Type) {
w.Sync(pkgbits.SyncConvRTTI)
w.typNeeded(src)
w.typNeeded(dst)
w.itab(src, dst)
}
func (w *writer) exprType(iface types2.Type, typ syntax.Expr) {
@ -1963,9 +2194,13 @@ func (w *writer) exprType(iface types2.Type, typ syntax.Expr) {
w.Sync(pkgbits.SyncExprType)
w.pos(typ)
w.typNeeded(tv.Type)
if w.Bool(iface != nil && !iface.Underlying().(*types2.Interface).Empty()) {
w.typ(iface)
w.itab(tv.Type, iface)
} else {
w.rtype(tv.Type)
info := w.p.typIdx(tv.Type, w.dict)
w.Bool(info.derived)
}
}
@ -2435,3 +2670,9 @@ func asPragmaFlag(p syntax.Pragma) ir.PragmaFlag {
}
return p.(*pragmas).Flag
}
// isPtrTo reports whether from is the type *to.
func isPtrTo(from, to types2.Type) bool {
ptr, ok := from.(*types2.Pointer)
return ok && types2.Identical(ptr.Elem(), to)
}

View File

@ -88,7 +88,7 @@ func TestDebugLinesPushback(t *testing.T) {
fn := "(*List[go.shape.int_0]).PushBack"
if buildcfg.Experiment.Unified {
// Unified mangles differently
fn = "(*List[int]).PushBack-shaped"
fn = "(*List[go.shape.int]).PushBack"
}
testDebugLines(t, "-N -l", "pushback.go", fn, []int{17, 18, 19, 20, 21, 22, 24}, true)
}
@ -105,7 +105,7 @@ func TestDebugLinesConvert(t *testing.T) {
fn := "G[go.shape.int_0]"
if buildcfg.Experiment.Unified {
// Unified mangles differently
fn = "G[int]-shaped"
fn = "G[go.shape.int]"
}
testDebugLines(t, "-N -l", "convertline.go", fn, []int{9, 10, 11}, true)
}

View File

@ -382,6 +382,11 @@ func Assignop1(src, dst *types.Type) (ir.Op, string) {
// don't have the methods for them.
return ir.OCONVIFACE, ""
}
if base.Debug.Unified != 0 && src.HasShape() {
// Unified IR uses OCONVIFACE for converting all derived types
// to interface type, not just type arguments themselves.
return ir.OCONVIFACE, ""
}
if implements(src, dst, &missing, &have, &ptr) {
return ir.OCONVIFACE, ""
}

View File

@ -45,7 +45,14 @@ func walkConvInterface(n *ir.ConvExpr, init *ir.Nodes) ir.Node {
toType := n.Type()
if !fromType.IsInterface() && !ir.IsBlank(ir.CurFunc.Nname) {
// skip unnamed functions (func _())
reflectdata.MarkTypeUsedInInterface(fromType, ir.CurFunc.LSym)
if base.Debug.Unified != 0 && fromType.HasShape() {
// Unified IR uses OCONVIFACE for converting all derived types
// to interface type. Avoid assertion failure in
// MarkTypeUsedInInterface, because we've marked used types
// separately anyway.
} else {
reflectdata.MarkTypeUsedInInterface(fromType, ir.CurFunc.LSym)
}
}
if !fromType.IsInterface() {

View File

@ -104,11 +104,27 @@ func main() {
F[V]()
F[W]()
type X[A any] U[X[A]]
F[X[int]]()
F[X[Int]]()
F[X[GlobalInt]]()
// TODO(go.dev/issue/54512): Restore these tests. They currently
// cause problems for shaping with unified IR.
//
// For example, instantiating X[int] requires instantiating shape
// type X[shapify(int)] == X[go.shape.int]. In turn, this requires
// instantiating U[shapify(X[go.shape.int])]. But we're still in the
// process of constructing X[go.shape.int], so we don't yet know its
// underlying type.
//
// Notably, this is a consequence of unified IR writing out type
// declarations with a reference to the full RHS expression (i.e.,
// U[X[A]]) rather than its underlying type (i.e., int), which is
// necessary to support //go:notinheap. Once go.dev/issue/46731 is
// implemented and unified IR is updated, I expect this will just
// work.
//
// type X[A any] U[X[A]]
//
// F[X[int]]()
// F[X[Int]]()
// F[X[GlobalInt]]()
for j, tj := range tests {
for i, ti := range tests[:j+1] {