-
Notifications
You must be signed in to change notification settings - Fork 18k
proposal: go/types: an API for normalized interface terms #61013
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Might be helpful to go over what |
This issue is currently labeled as early-in-cycle for Go 1.22. |
Unfortunately, this isn't going to happen for 1.22, which is perhaps not unreasonable given #63940. Let's wait another cycle to see if the correct API emerges. |
This issue is currently labeled as early-in-cycle for Go 1.23. |
I still think we should do this, but it's been an unfortunately low priority as most users have access to terms via x/tools/internal or x/exp/typeparams. Moving to the backlog, as it seems unlikely that we'll take this on before there is more demand. As mentioned above, #63940 is a higher priority for the team, and may affect the design of this API. |
@timothy-king as you are working on go/types and have a lot of experience with core types / normalized interface terms from your work on go/ssa, you seem like the perfect person to design a nice API here :) Tentatively reassigning, though I will leave it to you to prioritize. |
We really need to expose an API here, independent of discussion around core types. It is too important for tools, and we have for too long relied on the x/exp/typeparams package. I think the reason we don't have a solid proposal is that we didn't really know what the API should be. To kick start this discussion, here's a proposed API: // A TermSet is an ordered set of [Terms] representing the [type element] restrictions on the
// type set of a type, if any.
//
// The set is minimal, in the sense that there is no term set with fewer terms that represents
// the same set of types. As described at [link], every [Interface] type has a unique minimal
// term set.
//
// See [NewTermSet] for details on what the TermSet means for non-interface types.
type TermSet struct { /* ... */ }
// NewTermSet returns the normalized [Term] set for the given type.
//
// If the underlying of the type t is an [Interface] type, the result corresponds to the set
// of type terms in t, after normalizing as described at [link]. If the interface does not
// contain any terms after normalizing, the resulting set is a "singleton set" containing
// a single Term with Tilde() == false and Type() == t.
//
// If the underlying of the type t is a [Union] type, the result is equivalent to the result
// for interface{t}.
//
// For all other types, the result is a singleton set for t, as described above.
//
// An error is returned if the type is invalid or exceeds complexity bounds.
// If the type has an empty type set (meaning no type satisfies its terms), the resulting
// set has Len == 0.
//
// NewTermSet makes no guarantees about the order of terms, except that it is
// deterministic.
func NewTermSet(t types.Type) (*TermSet, error)
// Len returns the number of terms in the normalized term set.
func (*TermSet) Len() int
// At returns the ith term in the term set.
func (*TermSet) At(i int) *Term
// Terms returns a go1.23 iterator over the terms in the term set.
//
// Example: for t := range NewTermSet(t).Terms() { ... }
func (*TermSet) Terms() iter.Seq[*Term] This may not be ideal, but I did consider a few problems with the existing
@griesemer @adonovan @dominikh WDYT? |
This seems fine to me. We should add |
Another question that comes to mind is whether this comes with the same performance issues as NewMethodSet, i.e. is it advisable to cache results? Will it benefit from the internal caching of type sets that is done by go/types for interface types? If yes, are there going to be any caveats w.r.t. concurrent access? |
Some questions:
Further out:
|
Thanks all. Responses: @adonovan says:
Indeed, thanks. Added the iterator method to the proposal. @fbbdev says:
Hmm, good point. @griesemer should we add an implementation to the spec, similar to what we do for the precision of numeric constants?
No, I don't think so: the normalized term set is already cached on all interface types. Maybe it could be a problem to request term sets of Unions (which is not cached), but since Unions don't appear as the type of an Object, this is unlikely to be a problem. @griesemer asks:
Sure. This is the crux of the API problem. Consider the term sets of the following two union types:
Internally, we use a nil slice to represent 1 (empty), and a non-nil empty slice to represent 2 (no restrictions = all terms). In the proposed API, we return nil for 1, and {{any, false}} for 2. That seemed more generally useful, for the following reason: callers will typically be asking a question about what the underlying of the type could be (consider uses of go/types.underIs). If the underlying could be anything, we can represent that with var _ interface {
M()
int | any
} The most important case to consider is a type parameter
I don't follow this. I'd think that since it is representing the logical set (where
I agree, but we probably want to have
We could, but shouldn't the type parameter restricted by Let me highlight that this API is not natural or obvious. Just as we discovered that the underlying of
I think that would be OK, or am I missing something? I guess a concrete example would be |
I realize the above may be a wall of text. It may be helpful to frame the critical API decisions in terms of examples. Here I'll use some short hand notation for types, but hopefully my meaning is clear.
I think if you agree with those examples, the rest of the API behavior is implied. |
Thanks @findleyr for the additional details.
I still have some doubts about thread safety. In // An Interface represents an interface type.
type Interface struct {
// [...]
tset *_TypeSet // type set described by this interface, computed lazily
} And in // computeInterfaceTypeSet may be called with check == nil.
func computeInterfaceTypeSet(check *Checker, pos token.Pos, ityp *Interface) *_TypeSet {
if ityp.tset != nil {
return ityp.tset
}
// [...]
ityp.tset = &_TypeSet{terms: allTermlist} // TODO(gri) is this sufficient?
// [...] That looks like a potential data race, not to speak of later field accesses in the same function. The docs are silent on whether methods that involve access to type sets are safe for concurrent use. Should consumers of those APIs (and by extension the proposed term set API) worry about that? Is the cache guaranteed to be safely initialized before interface types are passed on to external consumers? |
Nevermind, I had missed this
Which of course is implicitly called while type checking packages, and has to be explicitly called by external consumers when constructing interface types. |
This is a placeholder for planning purposes, to be exchanged for a proper proposal at a future date.EDIT(2024-02-06): Please see the actual proposal in this comment below: #61013 (comment)
As discussed in #60994, there are some missing
go/types
APIs that are currently papered over with thex/exp/typeparams
package.In particular, we should propose a
go/types
API that serves the purpose of theNormalTerms
function -- some way to traverse a normalized representation of the terms of an interface types.We could expose an equivalent API to
NormalTerms
, or do something simpler. Let's decide early in the go1.22 cycle.CC @griesemer @adonovan @timothy-king @mdempsky
The text was updated successfully, but these errors were encountered: