Allow impls with sufficiently primitive generic parameters to be declared final.#6511
Allow impls with sufficiently primitive generic parameters to be declared final.#6511zygoloid wants to merge 6 commits intocarbon-language:trunkfrom
Conversation
…vely final. See carbon-language#6500 for details on why this is important.
Add testing.
41317a3 to
bf71a11
Compare
toolchain/check/impl.cpp
Outdated
| // TODO: How should we handle the case where the parameter type is | ||
| // incomplete? Can we defer determining whether an impl is effectively | ||
| // final until we see its definition? It's not clear that a parameter of | ||
| // incomplete type can be used in an impl declaration, so this may not | ||
| // matter. For now we treat such impls as not effectively final. |
There was a problem hiding this comment.
FWIW We don't need to decide this until we build/have the full witness table, which is either the definition or the first time we wrote something into it.
toolchain/check/impl_lookup.cpp
Outdated
| result.has_final_value() && | ||
| !IsImplEffectivelyFinal(context, | ||
| context.impls().Get(candidate.impl_id))) { | ||
| context.impls().Get(candidate.impl_id).final_kind == |
There was a problem hiding this comment.
Can we pass the Impl& to GetWitnessIdForImpl and avoid a second lookup here?
toolchain/check/impl.cpp
Outdated
| while (!worklist.empty()) { | ||
| type_id = worklist.pop_back_val(); | ||
|
|
||
| type_id = context.types().GetObjectRepr(type_id); |
There was a problem hiding this comment.
Can you leave a comment explaining this? Is there a test that fails without it?
There was a problem hiding this comment.
Added brief comment and some more explicit test coverage. (This was covered by the tests for eg i32, since it's an adapter for IntType, but now I'm more directly testing an adapter.)
| } | ||
|
|
||
| if (query_is_concrete || impl.is_final) { | ||
| // TODO: These final results should be cached somehow. Positive (non-None) |
There was a problem hiding this comment.
Did you mean to remove the TODO?
There was a problem hiding this comment.
Yes, this caching was added in #6452. I could keep a TODO specifically around caching negative results if you like (we currently only cache successful lookups)?
toolchain/check/impl_lookup.cpp
Outdated
| // they can only be cached in a limited way, or the cache needs to | ||
| // be invalidated by writing a final impl that would match. | ||
| if (query_is_concrete || | ||
| impl.final_kind != SemIR::FinalImplKind::Specializable) { |
There was a problem hiding this comment.
This does the wrong thing if:
- The impl is "effectively final"
- The impl is in Core prelude
- The lookup is in Core prelude
It's only okay-ish because/if:
- All the generic types named in TypeCanIntroduceAssociatedLibraries() are in Core prelude
- We don't write any specializations in core for those generic types
The decision to be effectively final seems inherently tied to the view of the impl (different library), and can't just be a fixed property of the impl itself. I am not sure how you'd like to proceed with this PR. I tried to suggest some simple checks around being in Core but they do the wrong thing: Here, we want non-generic things to be effectively final still in Core prelude. In impl construction we don't know if the user is from the same library yet. Maybe we need to split EffectivelyFinal up to EffectivelyFinal and EffectivelyFinalOutsideCorePrelude which is at least obviously being a bit of a hack. But it feels like the way this in being stored and used needs to be changed and we might as well do that now?
There was a problem hiding this comment.
Hm. I don't love the rules being different in the cross-library versus in the same-library case. This would violate the concatenation principle, among other issues.
I had a discussion with Josh about this and we came up with a different approach: we don't treat the new kind of impl as being effectively final, but instead we allow it to be explicitly declared final. We think we have a proof that this is sound.
I'm going to rework this PR in that direction.
Co-authored-by: Dana Jansens <danakj@orodu.net>
toolchain/check/impl.cpp
Outdated
| // Determines whether the given generic parameter type can have a value that | ||
| // introduces additional associated libraries during impl lookup. | ||
| static auto TypeCanIntroduceAssociatedLibraries(Context& context, | ||
| SemIR::TypeId initial_type_id) -> bool { |
There was a problem hiding this comment.
[diff] reported by reviewdog 🐶
| SemIR::TypeId initial_type_id) -> bool { | |
| SemIR::TypeId initial_type_id) | |
| -> bool { |
| #include "toolchain/check/type_completion.h" | ||
| #include "toolchain/check/type_structure.h" | ||
| #include "toolchain/diagnostics/diagnostic_emitter.h" | ||
| #include "toolchain/sem_ir/builtin_function_kind.h" |
| // If the types of all the generic bindings for the impl do not allow values | ||
| // with associated libraries, then no further specialization is possible in | ||
| // any other library, so the impl is effectively final. |
There was a problem hiding this comment.
I think this comment might need to be updated. It's not wrong but it's talking about the old context of this PR, but here we're checking for an impl being externally specializable, a somewhat inverse of effectively final
|
|
||
| // - adapter for such a type | ||
|
|
||
| class Adapt { adapt i32; } |
There was a problem hiding this comment.
Can we test an Adapt of an array of i32?
| class A[T:! type](V:! T) {} | ||
|
|
||
| // We can't tell whether a parameter of an incomplete type would make the impl | ||
| // effectively final, but it doesn't matter because the impl is invalid anyway. |
There was a problem hiding this comment.
"would make the impl unable to be specialized externally"? Does this test still make sense?
|
|
||
| library "[[@TEST_NAME]]"; | ||
|
|
||
| fn ConvertZero(N:! Core.IntLiteral()) -> Core.Int(N) { |
There was a problem hiding this comment.
Could these tests get a comment explaining what they are testing (that the Core impl is final)?
|
We triage inactive PRs and issues in order to make it easier to find active work. If this PR should remain active, please comment or remove the This PR is labeled |
If all the generic parameters of an impl have types that mean they can't possibly be further specialized in other libraries, then allow the impl to be declared final. For example, if an impl's only generic parameter is of type
i32, then it's not possible for an impl in another library to specialize it, because such an impl must necessarily be an orphan, so we can allow it to befinaland check locally that it's not specialized.We treat a generic parameter as being unable to support further specialization if its type is
bool, an integer or floating-point type, or an integer, floating-point or character literal type, or if it's built exclusively from such types. Critically, if there's a facet type (includingtype) somewhere within the object representation of the parameter type, we assume it can be further specialized.See #6500 for details on why this is important. This doesn't resolve that issue, but gets us incrementally closer.