-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Support builtin conversions of adapter classes #4655
base: trunk
Are you sure you want to change the base?
Conversation
toolchain/check/convert.cpp
Outdated
auto converted_value_id = | ||
PerformBuiltinConversion(context, loc_id, foundation_value_id, | ||
{ | ||
.kind = target.kind, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The construction of this ConversionTarget
is the most questionable to me. Should it use the same kind, init_id and init_block? If the init_block is used, should the AsCompatible be put into the block?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have an init_id
and init_block
here, the init_id
should refer to the destination object that we are initializing. So I think we need to apply a conversion to init_id
, and the logic ends up being:
- (Always) Convert the source to the adapted type with an
AsCompatible
. - If there's an
init_id
, create anAsCompatible
in theinit_block
to convert the destination to the adapted type. - Perform the builtin conversion.
- Convert the resulting expression back to the adapter type with an
AsCompatible
.
It seems a bit weird that we need three AsCompatible
conversions in some cases, but in SemIR we model an initializing expression as (sometimes) taking the location to be initialized and (always) producing an "initializing value" of the destination type, so weirdly we do end up with three things to convert.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It took me a while to really understand everything that was said here and why, but I think I got it, and the semir looks better now with this change.
toolchain/check/convert.cpp
Outdated
auto converted_value_id = | ||
PerformBuiltinConversion(context, loc_id, foundation_value_id, | ||
{ | ||
.kind = target.kind, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have an init_id
and init_block
here, the init_id
should refer to the destination object that we are initializing. So I think we need to apply a conversion to init_id
, and the logic ends up being:
- (Always) Convert the source to the adapted type with an
AsCompatible
. - If there's an
init_id
, create anAsCompatible
in theinit_block
to convert the destination to the adapted type. - Perform the builtin conversion.
- Convert the resulting expression back to the adapter type with an
AsCompatible
.
It seems a bit weird that we need three AsCompatible
conversions in some cases, but in SemIR we model an initializing expression as (sometimes) taking the location to be initialized and (always) producing an "initializing value" of the destination type, so weirdly we do end up with three things to convert.
103bdb3
to
c2d0cb3
Compare
This is ready for review, in case it was missed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, some trivial things but this looks good. I'm happy for the note I suggested to be added as a separate PR (approving on that basis), or as part of this PR; your choice.
toolchain/check/convert.cpp
Outdated
@@ -707,7 +707,7 @@ static auto CanUseValueOfInitializer(const SemIR::File& sem_ir, | |||
} | |||
|
|||
// Returns the non-adapter type that is compatible with the specified type. | |||
static auto GetCompatibleBaseType(Context& context, SemIR::TypeId type_id) | |||
static auto GetCompatibleFoundationType(Context& context, SemIR::TypeId type_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if GetCompatibleNonAdapterType
would actually work better here -- "compatible" is already supposed to imply that we're looking at adapters (an adapter and its adapted type are compatible with each other).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you feel like its currently redundant, I would prefer GetFoundationType
over "CompatibleNonAdapter". The negation there feels more ambiguous - many things may be non-adapters, though the compatible narrows it down. Maybe you're trying to highlight that it chases through the chain of adapters, though I would say foundation does at least as good a job of highlighting that. Maybe it's also that "adapter" and "adapted" are very close linguistically and make me pause and think about the relationship (kind of like how definition and declaration still do today...) in a way foundation does not.
} | ||
|
||
fn F(a: A) { | ||
let a_value: A = (a as (i32, Noncopyable)) as A; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually put tests that are expected to pass in different file splits from ones that are expected to fail. That way we get extra assurance that we don't have regressions after autoupdate -- if an expected-to-pass test starts to fail, the test will fail even after an autoupdate run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, thanks. I will split off the fail_init_class.carbon
above as well.
FWIW I tried making a non-fail_
test in this file fail, ran autoupdate, and then bazel test
and it still fails that expected-to-pass test regardless of the presence of a fail_
test in the same file or not. Not sure if that's unexpected then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PTAL
toolchain/check/convert.cpp
Outdated
@@ -707,7 +707,7 @@ static auto CanUseValueOfInitializer(const SemIR::File& sem_ir, | |||
} | |||
|
|||
// Returns the non-adapter type that is compatible with the specified type. | |||
static auto GetCompatibleBaseType(Context& context, SemIR::TypeId type_id) | |||
static auto GetCompatibleFoundationType(Context& context, SemIR::TypeId type_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you feel like its currently redundant, I would prefer GetFoundationType
over "CompatibleNonAdapter". The negation there feels more ambiguous - many things may be non-adapters, though the compatible narrows it down. Maybe you're trying to highlight that it chases through the chain of adapters, though I would say foundation does at least as good a job of highlighting that. Maybe it's also that "adapter" and "adapted" are very close linguistically and make me pause and think about the relationship (kind of like how definition and declaration still do today...) in a way foundation does not.
} | ||
|
||
fn F(a: A) { | ||
let a_value: A = (a as (i32, Noncopyable)) as A; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, thanks. I will split off the fail_init_class.carbon
above as well.
FWIW I tried making a non-fail_
test in this file fail, ran autoupdate, and then bazel test
and it still fails that expected-to-pass test regardless of the presence of a fail_
test in the same file or not. Not sure if that's unexpected then.
When creating a tuple or struct type object/value, we will walk each of the tuple's or struct's parts, respectively, and Convert() each of them. This allows (T, T) to convert to (U, U) and so forth. It also performs the conversion from value to initialization even for the same types, such as converting from a value of (T, T) to an object of (T, T). Classes need to define their own conversions but when the target and source types are the same, there is no conversion of types taking place. If the class adapts a tuple or struct then walk each of the tuple's or struct's parts, respectively, and Convert() each of them in order to initialize the parts of the target. For copyable types, the conversion implies a copy in the initialization, and for non-copyable types, an error is emitted. This supports copying a class value to a class object when it is an adapter of a tuple or struct.
Co-authored-by: Richard Smith <[email protected]>
When creating a tuple or struct type object/value, we will walk each of
the tuple's or struct's parts, respectively, and Convert() each of them.
This allows (T, T) to convert to (U, U) and so forth. It also performs
the conversion from value to initialization even for the same types,
such as converting from a value of (T, T) to an object of (T, T).
Classes need to define their own conversions but when the target and
source types are the same, there is no conversion of types taking place.
If the class adapts a tuple or struct then walk each of the tuple's or
struct's parts, respectively, and Convert() each of them in order to
initialize the parts of the target.
For copyable types, the conversion implies a copy in the initialization,
and for non-copyable types, an error is emitted.
This supports copying a class value to a class object when it is an
adapter of a tuple or struct.