Skip to content
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

Open
wants to merge 14 commits into
base: trunk
Choose a base branch
from

Conversation

danakj
Copy link
Contributor

@danakj danakj commented Dec 9, 2024

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.

@danakj danakj requested a review from zygoloid December 9, 2024 18:28
@github-actions github-actions bot requested a review from geoffromer December 9, 2024 18:28
auto converted_value_id =
PerformBuiltinConversion(context, loc_id, foundation_value_id,
{
.kind = target.kind,
Copy link
Contributor Author

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?

Copy link
Contributor

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:

  1. (Always) Convert the source to the adapted type with an AsCompatible.
  2. If there's an init_id, create an AsCompatible in the init_block to convert the destination to the adapted type.
  3. Perform the builtin conversion.
  4. 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.

Copy link
Contributor Author

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.

@danakj danakj removed the request for review from geoffromer December 9, 2024 18:29
toolchain/check/convert.cpp Outdated Show resolved Hide resolved
auto converted_value_id =
PerformBuiltinConversion(context, loc_id, foundation_value_id,
{
.kind = target.kind,
Copy link
Contributor

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:

  1. (Always) Convert the source to the adapted type with an AsCompatible.
  2. If there's an init_id, create an AsCompatible in the init_block to convert the destination to the adapted type.
  3. Perform the builtin conversion.
  4. 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.

toolchain/check/convert.cpp Outdated Show resolved Hide resolved
@danakj danakj requested a review from zygoloid December 12, 2024 22:12
@danakj danakj force-pushed the tuple-adapt branch 2 times, most recently from 103bdb3 to c2d0cb3 Compare December 12, 2024 22:21
@danakj
Copy link
Contributor Author

danakj commented Dec 16, 2024

This is ready for review, in case it was missed

Copy link
Contributor

@zygoloid zygoloid left a 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 Show resolved Hide resolved
@@ -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)
Copy link
Contributor

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).

Copy link
Contributor Author

@danakj danakj Dec 23, 2024

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@danakj danakj changed the title Support builtin conversions of adaptor classes Support builtin conversions of adapter classes Dec 23, 2024
Copy link
Contributor Author

@danakj danakj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PTAL

@@ -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)
Copy link
Contributor Author

@danakj danakj Dec 23, 2024

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;
Copy link
Contributor Author

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.

danakj and others added 13 commits December 23, 2024 16:48
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants