Skip to content

Conversation

bricknerb
Copy link
Contributor

@bricknerb bricknerb commented Oct 17, 2025

Always take into account both the lookup access specifier and the declaration. When set, lookup access specifier takes precedence. When not set, we have two use cases:

  1. This is not a record member, so no access is specified at all. Treat this as public.
  2. This is a record member of a base class. Treat this as private. Reference.

Also, deduplicate access mapping between import and overload resolution.

Background: #6221 (comment)

Part of #5859.

Always take into account both the lookup access specifier and the lexical access specifier.
When set, lookup access specifier takes precedence.
When not set, we have two use cases:
1. This is not a record member, so no access is specified at all. Treat this as public.
2. This is a private record member of a base class. Treat this as private.
@bricknerb bricknerb changed the title C++ Interop: Fix access deduction C++ Interop: Fix access deduction to handle private member of base classes correctly Oct 17, 2025
@bricknerb bricknerb marked this pull request as ready for review October 17, 2025 13:58
@bricknerb bricknerb requested a review from a team as a code owner October 17, 2025 13:58
@bricknerb bricknerb requested review from jonmeow and removed request for a team October 17, 2025 13:58
Comment on lines 559 to 570
// CHECK:STDERR: fail_import_overload_set_public_base_class_call_non_public.carbon:[[@LINE+5]]:3: error: cannot access protected member `foo` of type `Cpp.PublicDerived` [ClassInvalidMemberAccess]
// CHECK:STDERR: Cpp.PublicDerived.foo(1);
// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~
// CHECK:STDERR: fail_import_overload_set_public_base_class_call_non_public.carbon: note: declared here [ClassMemberDeclaration]
// CHECK:STDERR:
Cpp.PublicDerived.foo(1);
// CHECK:STDERR: fail_import_overload_set_public_base_class_call_non_public.carbon:[[@LINE+5]]:3: error: cannot access private member `foo` of type `Cpp.PublicDerived` [ClassInvalidMemberAccess]
// CHECK:STDERR: Cpp.PublicDerived.foo(1, 2);
// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~
// CHECK:STDERR: fail_import_overload_set_public_base_class_call_non_public.carbon: note: declared here [ClassMemberDeclaration]
// CHECK:STDERR:
Cpp.PublicDerived.foo(1, 2);
Copy link
Contributor

Choose a reason for hiding this comment

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

Where you're doing multiple calls to foo, do you think it'd be helpful to comment about what's distinct about each call? The name "foo" and the parameters "1" don't carry meaning, and the diagnostic doesn't point out the different overrides, so I worry that outside the context of a PR it'll be hard to understand.

A different approach might be to create types that make it clearer the difference between each overload (e.g., struct PrivateCall {};) so that you would see it in the signature (e.g. Cpp.PublicDerived.foo(PrivateCall.PrivateCall());).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done (took the different approach you suggested).
Thanks!

case clang::AS_private:
return SemIR::AccessKind::Private;
}
return DeduceClangAccess(iterator.getAccess(), iterator->getAccess());
Copy link
Contributor

Choose a reason for hiding this comment

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

The difference between .getAccess and ->getAccess seems subtle, perhaps document what's occurring here where you do it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No longer relevant here anymore.
Added a comment in CalculateEffectiveAccess().

}
SemIR::AccessKind access_kind = MapAccess(overload_set.begin());
for (auto it = overload_set.begin() + 1;
it != overload_set.end() && access_kind != SemIR::AccessKind::Public;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd typically expect a for loop to focus on comparing the value it's iterating over (it), with other comparisons inside the loop. What made you choose to do this comparison as part of the for loop parameters?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With the old code it made sense to me to avoid a break.
Moved the optimization to an explicit condition with a break.


// Deduces the effective access kind from the given lookup and lexical access
// specifiers.
auto DeduceClangAccess(clang::AccessSpecifier lookup_access,
Copy link
Contributor

Choose a reason for hiding this comment

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

We've generally been saying "Deduce" in the toolchain specifically for generic deduction. Also, skimming through the cpp headers, don't they typically say "Cpp" instead of "Clang"?

Perhaps there's a better term here, because it's just doing a combination of the two parameters. Had you considered something like "ConvertCppAccess"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment on lines 14 to 15
auto DeduceClangAccess(clang::AccessSpecifier lookup_access,
clang::AccessSpecifier lexical_access)
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like both callers could pass in the access specifiers as a DeclAccessPair. Had you considered taking that here? That might even let you just remove MapAccess from import.cpp, since there's it.getPair()

Copy link
Contributor Author

@bricknerb bricknerb Oct 20, 2025

Choose a reason for hiding this comment

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

Done.

Comment on lines 2126 to 2127
SemIR::AccessKind access_kind = MapAccess(overload_set.begin());
for (auto it = overload_set.begin() + 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Had you considered initializing access_kind to Private, so that you wouldn't need this begin() offset and could only call MapAccess from inside the loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

foo(1, 2);
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a test that does just a little more access control layering, such as:

Suggested change
// --- layered_inheritance_test.carbon
library "[[@TEST_NAME]]";
import Cpp inline '''
class C {
public:
static auto Public() -> void;
protected:
static auto Protected() -> void;
private:
static auto Private() -> void;
};
class D : private C {};
class E : public D {};
''';
class F {
extend base: Cpp.E;
fn G[self: Self]() {
Self.Public();
Self.Protected();
Self.Private();
}
}

Also, are non-static methods intended to work? If so, perhaps it'd be good to test those too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added more coverage that include what you propose here.
Thanks!

if (lexical_access != clang::AS_none) {
// When a base class private member is accessed through a derived class, the
// lookup access would be set to `AS_none`.
CARBON_CHECK(lexical_access == clang::AS_private);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this CHECK correct? I've suggested a test which I think may hit it.

If it's not correct, perhaps this function could be replaced with something like:

auto DeduceClangAccess(clang::AccessSpecifier lookup_access,
                       clang::AccessSpecifier lexical_access)
    -> SemIR::AccessKind {
  // Lookup access takes precedence.
  switch (lookup_access != clang::AS_none ? lookup_access : lexical_access) {
    case clang::AS_public:
      return SemIR::AccessKind::Public;
    case clang::AS_protected:
      return SemIR::AccessKind::Protected;
    case clang::AS_private:
      return SemIR::AccessKind::Private;
    case clang::AS_none:
      // This is not a record member.
      return SemIR::AccessKind::Public;
  }
}

That also is pretty short, and if you're not expecting many other access functions, perhaps could be inlined into the header to remove access.cpp.

Copy link
Contributor

@jonmeow jonmeow Oct 17, 2025

Choose a reason for hiding this comment

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

BTW, while equivalent [ed: for current tests, the suggested] tests might also suggest this still should have some TODOs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, this check was incorrect.
I've added more tests based on your suggestions and adjusted the logic to make it correct for these cases.

@jonmeow jonmeow requested a review from zygoloid October 17, 2025 23:36
@jonmeow
Copy link
Contributor

jonmeow commented Oct 17, 2025

@zygoloid Would you be okay finishing review here, once it's updated?

@bricknerb bricknerb changed the title C++ Interop: Fix access deduction to handle private member of base classes correctly C++ Interop: Fix access calculation to handle private member of base classes correctly Oct 20, 2025
@bricknerb
Copy link
Contributor Author

@zygoloid Would you be okay finishing review here, once it's updated?

Updated.

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.

A couple of minor things, but otherwise LG.

// members it means we lost access along the inheritance path. Otherwise
// it means there's no access associated with this function so we treat it
// as public.
return access_pair->isCXXClassMember() ? clang::AS_private
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd like to see some justification here for why it's correct to map "none" to "private". I think what this comes down to is:

The difference between "private" and "none" only matters when the access check is performed within a friend or member of the naming class. Because the naming class is a C++ class, and we don't yet have a mechanism for a C++ class to befriend a Carbon class, we can safely map "none" to "private" for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've included that information in the comment.
Can you clarify or provide a pointer for my understanding and for posterity how should private and none be treated in a friend or member of the naming class?


// Calculates the effective access kind from the given (declaration, lookup
// access) pair.
auto ConvertCppAccess(clang::DeclAccessPair access_pair) -> SemIR::AccessKind;
Copy link
Contributor

Choose a reason for hiding this comment

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

Convert* is typically used to name a language-level type conversion function. Maybe MapCppAccess would work better, following how we name the Map*Type functions in cpp/import.cpp?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@bricknerb bricknerb enabled auto-merge October 21, 2025 07:54
@bricknerb bricknerb added this pull request to the merge queue Oct 21, 2025
Merged via the queue into carbon-language:trunk with commit 840562f Oct 21, 2025
8 checks passed
@bricknerb bricknerb deleted the none branch October 21, 2025 08:47
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.

3 participants