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
toolchain/check/convert.cpp Outdated Show resolved Hide resolved
@danakj danakj removed the request for review from geoffromer December 9, 2024 18:29
toolchain/check/convert.cpp Outdated Show resolved Hide resolved
toolchain/check/convert.cpp Outdated Show resolved Hide resolved
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.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think GetFoundationType is also unclear -- we're using "foundation declaration" to mean "base or adapt declaration", so I'd expect GetFoundationType to return the immediate base class or adapted type, whereas this transitively looks through adapters and does not look through bases. Conversely, we use "compatible" to mean "transitively related through adapters".

If you want to avoid the negation, maybe GetCanonicalCompatibleType or GetTransitiveAdaptedType or GetIndirectlyAdaptedType?

}

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Trying to clarify: you can have fail_ and non-fail_ splits in the same on-disk file. The fail_ split will fail (even after autoupdate) if it doesn't produce errors, and conversely the non-fail_ split will fail (even after autoupdate) if it does produce errors. It doesn't matter what is in the other splits in the same on-disk file or whether they're fail_ / non-fail_.

What we try to avoid is putting expected-to-pass tests in a fail_ split, because if they start producing errors we might not notice. (It's also desirable for the same reason to have only one error per fail_ split, so that the test won't still pass if one of the errors goes away.)

@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 <richard@metafoo.co.uk>
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.

LG with any of the suggested names for GetFoundationType.

// the type (or some part of it) is not copyable.
DiagnosticAnnotationScope annotate_diagnostics(
&context.emitter(), [&](auto& builder) {
CARBON_DIAGNOSTIC(InCopy, Note, "in copy of `{0}`", TypeOfInstId);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
CARBON_DIAGNOSTIC(InCopy, Note, "in copy of `{0}`", TypeOfInstId);
CARBON_DIAGNOSTIC(InCopy, Note, "in copy of {0}", TypeOfInstId);

The diagnostic machinery adds the `s for you.

}

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.

Trying to clarify: you can have fail_ and non-fail_ splits in the same on-disk file. The fail_ split will fail (even after autoupdate) if it doesn't produce errors, and conversely the non-fail_ split will fail (even after autoupdate) if it does produce errors. It doesn't matter what is in the other splits in the same on-disk file or whether they're fail_ / non-fail_.

What we try to avoid is putting expected-to-pass tests in a fail_ split, because if they start producing errors we might not notice. (It's also desirable for the same reason to have only one error per fail_ split, so that the test won't still pass if one of the errors goes away.)

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