typeshelper: follow type aliases, generics, to resolve underlying types#422
Conversation
3774a14 to
63d8d5e
Compare
| // If we are ranging over a map slice or string with only a single lhs operand, then that | ||
| // operand will be int-valued. | ||
| if typeshelper.IsDeeplyMap(rhsType) || typeshelper.IsDeeplySlice(rhsType) || typeshelper.IsDeeplyArray(rhsType) || typeIsString(rhsType) { | ||
| if typeshelper.IsDeeplyMap(rhsType) || typeshelper.IsDeeplySlice(rhsType) || typeshelper.IsDeeplyArrayOrArrayPtr(rhsType) || typeIsString(rhsType) { |
There was a problem hiding this comment.
Can we use the UnwrapPtr helper function and just call typeshelper.IsDeeplyArray(typeshelper.UnwrapPtr(rhsType)) such that we don't have to introduce a new API in typeshelper?
Or did I miss anything here?
There was a problem hiding this comment.
Not quite: UnwrapPtr only takes care of the pointer, but no type aliases, named types, parameters, etc
There was a problem hiding this comment.
I have a feeling that we can make UnwrapPtr take care of type aliases, named types, parameters and it would just work.
But let's leave that to a separate journey, the blast radius is large and we may hit corner cases.
Agreed here!
| // regardless of the indices, so we must check it before the `b[_:0:_]` case below (which | ||
| // would otherwise wrongly treat `a[:0]` as a nilable empty slice). | ||
| if typeshelper.IsDeeplyArray(r.Pass().TypesInfo.Types[expr.X].Type) { | ||
| if typeshelper.IsDeeplyArrayOrArrayPtr(r.Pass().TypesInfo.TypeOf(expr.X)) { |
| // IsDeeplyArray returns true if `t` is of array type, including transitively through named | ||
| // types and aliases, as well as type parameters whose type sets contain only array types. | ||
| func IsDeeplyArray(t types.Type) bool { | ||
| switch tt := UnwrapPtr(t).(type) { | ||
| case *types.Array: | ||
| return true | ||
| case *types.Named: | ||
| return IsDeeplyArray(tt.Underlying()) | ||
| } | ||
| return false | ||
| return underlyingIs[*types.Array](t) | ||
| } |
There was a problem hiding this comment.
Now that we have underlyingIs, can we remove these APIs from the good old times and just use the generic version instead?
Of course, we should do that in a separate PR to avoid bloating this one :)
There was a problem hiding this comment.
Happy to submit a follow-up PR! func IsType[T](t types.Type) bool
| // interface intersect, this is conservative: the actual type set can only be smaller than the | ||
| // enumerated terms, and type sets we cannot enumerate (such as method-only constraints) yield | ||
| // false. | ||
| func underlyingAlwaysSatisfies(t types.Type, pred func(types.Type) bool) bool { |
There was a problem hiding this comment.
It seems that we only ever have a single pred, do we foresee that we will add more predicates in the future? if not I'd actually prefer we inline it
There was a problem hiding this comment.
hm, not trivially. this function has two callers with different predicates (IsDeeplyArrayOrArrayPtr and underlyingIs), and the predicate is used to check type parameters so it needs to loop. I could create a new function getUnderlyingTypes but it would need to return a freshly allocated slice which seems worse?
There was a problem hiding this comment.
Huh, OK then let's keep this as-is for now :)
| func constraintTerms(iface *types.Interface) []types.Type { | ||
| var terms []types.Type | ||
| for i := 0; i < iface.NumEmbeddeds(); i++ { | ||
| switch e := types.Unalias(iface.EmbeddedType(i)).(type) { | ||
| case *types.Union: | ||
| for j := 0; j < e.Len(); j++ { | ||
| if emb, isIface := e.Term(j).Type().Underlying().(*types.Interface); isIface { | ||
| terms = append(terms, constraintTerms(emb)...) | ||
| } else { | ||
| terms = append(terms, e.Term(j).Type()) | ||
| } | ||
| } | ||
| default: | ||
| if emb, isIface := e.Underlying().(*types.Interface); isIface { | ||
| terms = append(terms, constraintTerms(emb)...) | ||
| } else { | ||
| terms = append(terms, e) | ||
| } | ||
| } | ||
| } | ||
| return false | ||
| return terms | ||
| } |
There was a problem hiding this comment.
Do we know how much of a performance penalty this would bring?
We'll be calling this function over and over again (and perhaps for the same type), if this makes it slow we should probably at least add a cache.
Or, we can also move this part out to a separate PR and I'm happy to merge the rest first while we discuss on this.
There was a problem hiding this comment.
fwiw I can remove this function entirely if you're ok with me adding a new dependency on https://pkg.go.dev/golang.org/x/exp/typeparams#NormalTerms (which does a slightly better job as well, I just wasn't sure if you'd be ok with a new dependency.)
perf-wise, this code only runs on structs with type parameters. it doesn't seem to hurt the golden test- but I admittedly haven't measured the performance impact on code that heavily uses generics (like Rust-style code)
There was a problem hiding this comment.
I ran a benchmark using the uncached version vs map[types.Type]... vs sync.Map. the uncached underlyingIs outperformed its cached version (respectively 5ns/op, 9ns/op, 7ns/op) for non-parameterized types. With parameters this quickly jumped to 100ns and even upwards of 500ns for more complex parameters. Still: are those common enough to warrant it?
There was a problem hiding this comment.
Yes, having dependencies from x/exp is totally OK (I really wish NormalTerms can be included in stdlib, maybe in the future -- then we can drop that dependency)
There was a problem hiding this comment.
Still: are those common enough to warrant it?
Yeah I agree that generics in Go aren't that popular (yet?) and this should be OK. Let's swap to NormalTerms and merge.
I can run this on our internal benchmarks too, if needed we can always revert this part
|
@TvdW This is an awesome PR! I left some comments, let me know if they don't make sense :) |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #422 +/- ##
==========================================
- Coverage 87.26% 87.24% -0.03%
==========================================
Files 74 74
Lines 8451 8434 -17
==========================================
- Hits 7375 7358 -17
- Misses 877 878 +1
+ Partials 199 198 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
While doing some prework for my next commit I realized that these don't resolve correctly, hiding some types from being checked for nilness.
…e[T] Replace the six near-identical IsDeeplyArray/Slice/Map/Ptr/Chan/Interface wrappers with a single exported generic IsDeeplyType[T], built on the underlyingAlwaysSatisfies resolution (named types, aliases, and type parameters via normalized type sets).
|
@TvdW Replied to all comments, please ping me once this PR is updated and I'm happy to merge :) |
63d8d5e to
1f30814
Compare
While doing some prework for my next commit I realized that these don't resolve correctly, hiding some types from being checked for nilness.
Claude-assisted, human-reviewed.