This is an archive of the discontinued LLVM Phabricator instance.

[clang] Fix `static_cast` to array of unknown bound
Needs ReviewPublic

Authored by Fznamznon on Jun 2 2023, 8:20 AM.

Details

Reviewers
usaxena95
shafik
ayzhao
aaron.ballman
erichkeane
cor3ntin
Group Reviewers
Restricted Project
Summary

Per P1975R0 an expression like static_cast<U[]>(...) defines the type
of the expression as U[1].

Fixes https://github.com/llvm/llvm-project/issues/62863

Diff Detail

Event Timeline

Fznamznon created this revision.Jun 2 2023, 8:20 AM
Herald added a project: Restricted Project. · View Herald TranscriptJun 2 2023, 8:20 AM
Fznamznon requested review of this revision.Jun 2 2023, 8:20 AM
Herald added a project: Restricted Project. · View Herald TranscriptJun 2 2023, 8:20 AM
Herald added a subscriber: cfe-commits. · View Herald Transcript
erichkeane added inline comments.Jun 2 2023, 8:32 AM
clang/lib/Sema/SemaType.cpp
8859

I am not really sure this is the right way about this. You're supposed to be testing T, but this looks like it is checking the type of E, isn't it? I think you just need to check Cast->getType().

8861

Not supposed to have curleys here.

Fznamznon added inline comments.Jun 2 2023, 8:40 AM
clang/lib/Sema/SemaType.cpp
8859

For the case I'm trying to fix, T is array of unknown bound, it is already checked before calling completeExprArrayBound. Right now when you write something like

int (&&arr1)[1] = static_cast<int[]>(42);

Clang actually is able to realize that parenthesized initialization is made, so it actually generates CXXParenListInitExpr that has type int[1]. Here I'm just paranoidally double-checking that is the case before switching the type. Maybe I don't need this double-check at all then?

erichkeane added inline comments.Jun 2 2023, 8:45 AM
clang/lib/Sema/SemaType.cpp
8859

That makes me wonder if this is the correct place for this? Should when we generate this type when we do the CXXParenListInitExpr fixup?

Either way, I think making this depend on that behavior (which would possibly be fragile), we should just do it based on the type passed ot the static_cast.

Another question: is this something that needs to be done for other cast types? Does similar behavior exist for the other casts? Should it also happen with 'clobber' casts (C-style) that are effectively static?

Fznamznon added inline comments.Jun 5 2023, 3:53 AM
clang/lib/Sema/SemaType.cpp
8859

That makes me wonder if this is the correct place for this? Should when we generate this type when we do the CXXParenListInitExpr fixup?

The function called completeExprArrayBound seemed like an appropriate place for me.
Could you please elaborate what you mean under CXXParenListInitExpr fixup?

is this something that needs to be done for other cast types?

I'm not sure. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1975r0.html seems to be extending only static_casts. And this was the original purpose of this bugfix.
gcc and msvc don't agree on that matter https://godbolt.org/z/1M3ahhsYr.

erichkeane added inline comments.Jun 5 2023, 5:57 AM
clang/lib/Sema/SemaType.cpp
8859

Could you please elaborate what you mean under CXXParenListInitExpr fixup?

You mentioned that at a different place we set the type of the CXXParenListInitExpr to be the 1-arity array. I was saying that if there is logic THERE based on whether its an incomplete array type it would be for the same purpose, and perhaps be the correct place to do this.

I'm not sure. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1975r0.html seems to be extending only static_casts.

Yep, thats what I was hoping to see, thanks.

And this was the original purpose of this bugfix.

I was concerned that we increasing the inconsistency by only fixing 1 of the types of casts. It might seem like 'feature creep', but it is very important that we make sure a patch is reasonably complete, so that it doesn't make the compiler less consistent.

Fznamznon added inline comments.Jun 29 2023, 10:55 AM
clang/lib/Sema/SemaType.cpp
8859

I was saying that if there is logic THERE based on whether its an incomplete array type it would be for the same purpose, and perhaps be the correct place to do this.

Well, it seems to be happening inside of a TryStaticImplicitCast before the CXXStaticCastExpr created. There is initialization sequence performed, and the source expression comes out of as an array of 1 element. It is not very convenient to switch the type there since there is no cast expression itself yet and the destination type is passed as a copy through several functions. Will it be ok to check and switch the type of resulting static_cast expression just before/after it is created?

Fznamznon updated this revision to Diff 536172.Jun 30 2023, 3:38 AM

Move changes to another place, check destination type

Ping for review.

aaron.ballman added reviewers: Restricted Project, cor3ntin.Jul 5 2023, 6:28 AM

Thank you for working on this!

clang/lib/Sema/SemaCast.cpp
1269–1272 ↗(On Diff #536172)

This seems incorrect to me -- I think the CastOperation should be created with the correct result type to begin with. What you are citing is a note, but the actual wording that specifies that note is: https://eel.is/c++draft/dcl.init#general-16.5 so I think the fix approach is somewhat correct but the code lives in the wrong place, this should be closer to the initialization code I think.

clang/test/SemaCXX/paren-list-agg-init.cpp
276–285

I'd like to see test coverage for:

int (&&arr)[] = (int[])(42);
int (&&arr1)[1] = (int[])(42);
int (&&arrr2)[2] = (int[])(42);
int (&&arr3)[3] = (int[3])(42);

where we're using a C-style cast, because: http://eel.is/c++draft/expr.cast#4

Fznamznon added inline comments.Jul 5 2023, 9:45 AM
clang/test/SemaCXX/paren-list-agg-init.cpp
276–285

Thank you for the review!

Just to double check, so it says:

The conversions performed by ... *all named casts* can be performed using the cast notation of explicit type conversion.

Does that mean the c-style cast should produce the same thing? And, https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1975r0.html doesn't say anything about c-style casts because it is assumed that it should be able to do anything that static_cast can do?

gcc doesn't agree https://godbolt.org/z/Pfq8frdn9 . The funny thing is that the original bug report seems to be using some kind of gcc test.

Fznamznon updated this revision to Diff 538165.Jul 7 2023, 9:00 AM

Rebase, move the logic, modify c-style casts as well

aaron.ballman added inline comments.
clang/test/SemaCXX/paren-list-agg-init.cpp
276–285

Does that mean the c-style cast should produce the same thing? And, https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1975r0.html doesn't say anything about c-style casts because it is assumed that it should be able to do anything that static_cast can do?

Correct

gcc doesn't agree https://godbolt.org/z/Pfq8frdn9 . The funny thing is that the original bug report seems to be using some kind of gcc test.

MSVC and ICC both agree though, so I lean towards us being on the right path in allowing those casts. @hubert.reinterpretcast -- do you have thoughts?

This looks reasonable to me know. should we try to land that today?
Sorry i could not get to it earlier!

should we try to land that today?

I'm not sure. It causes failures in libc++ testing:

Failed Tests (3):
  llvm-libc++-shared.cfg.in :: std/ranges/range.access/end.pass.cpp
  llvm-libc++-shared.cfg.in :: std/ranges/range.access/rbegin.pass.cpp
  llvm-libc++-shared.cfg.in :: std/ranges/range.access/rend.pass.cpp

I haven't figured out why. Trying to compile:

#include <ranges>

using RangeCREndT = decltype(std::ranges::crend);
static_assert(!std::is_invocable_v<RangeCREndT, int (&)[]>);

fails with this patch only using libc++, libstdc++ is fine. I'm not sure it is my misunderstanding, bug in the patch or bug in libc++.

should we try to land that today?

I'm not sure. It causes failures in libc++ testing:

Failed Tests (3):
  llvm-libc++-shared.cfg.in :: std/ranges/range.access/end.pass.cpp
  llvm-libc++-shared.cfg.in :: std/ranges/range.access/rbegin.pass.cpp
  llvm-libc++-shared.cfg.in :: std/ranges/range.access/rend.pass.cpp

I haven't figured out why. Trying to compile:

#include <ranges>

using RangeCREndT = decltype(std::ranges::crend);
static_assert(!std::is_invocable_v<RangeCREndT, int (&)[]>);

fails with this patch only using libc++, libstdc++ is fine. I'm not sure it is my misunderstanding, bug in the patch or bug in libc++.

CC @ldionne @philnik @Mordante for libc++ opinions

should we try to land that today?

I'm not sure. It causes failures in libc++ testing:

Failed Tests (3):
  llvm-libc++-shared.cfg.in :: std/ranges/range.access/end.pass.cpp
  llvm-libc++-shared.cfg.in :: std/ranges/range.access/rbegin.pass.cpp
  llvm-libc++-shared.cfg.in :: std/ranges/range.access/rend.pass.cpp

I haven't figured out why. Trying to compile:

#include <ranges>

using RangeCREndT = decltype(std::ranges::crend);
static_assert(!std::is_invocable_v<RangeCREndT, int (&)[]>);

fails with this patch only using libc++, libstdc++ is fine. I'm not sure it is my misunderstanding, bug in the patch or bug in libc++.

CC @ldionne @philnik @Mordante for libc++ opinions

Thanks for the heads-up! From what I can tell, this could be a problem in the compiler, in is_invocable, in cend or in end. Unfortunately, I don't have the time right now to make a smaller reproducer and Louis isn't available. @Mordante do you have time to take a look at this? I will probably only have time to get to it by the end of the week or early next week.

I think getting a smaller reproducer would be useful indeed before talking about conformance, even though i think there is indeed something wrong there - an incomplete parameter should not be able to produce a valid call.

should we try to land that today?

I'm not sure. It causes failures in libc++ testing:

Failed Tests (3):
  llvm-libc++-shared.cfg.in :: std/ranges/range.access/end.pass.cpp
  llvm-libc++-shared.cfg.in :: std/ranges/range.access/rbegin.pass.cpp
  llvm-libc++-shared.cfg.in :: std/ranges/range.access/rend.pass.cpp

I haven't figured out why. Trying to compile:

#include <ranges>

using RangeCREndT = decltype(std::ranges::crend);
static_assert(!std::is_invocable_v<RangeCREndT, int (&)[]>);

fails with this patch only using libc++, libstdc++ is fine. I'm not sure it is my misunderstanding, bug in the patch or bug in libc++.

What do you mean with libstdc++ is fine?
The reduced tests passed with libcstd++ https://godbolt.org/z/nh9v8cedr
The change seems to break libc++.

Based on the bug and this patch I am correct that "transformation" of an array of unknown bounds to anarray of known bounds only happens with an explicit cast and not implicitly?

I have a strong suspicion that this patch changes the test to

#include <ranges>

using RangeCREndT = decltype(std::ranges::crend);
static_assert(!std::is_invocable_v<RangeCREndT, int (&)[1]>); // no longer an array of unknown bound so the assert fails

Can you look at the AST output of the failed example to verify my hypothesis? When you think libc++ is wrong can you then post this AST?

What do you mean with libstdc++ is fine?

What I mean is when I do (current patch is applied to clang):

source/llvm-project/build/bin/clang++ -std=c++20 t.cpp -c // compilation succeeds

But when I do (libc++ is built together with clang)

source/llvm-project/build/bin/clang++ -std=c++20 t.cpp -stdlib=libc++ -c
t.cpp:4:15: error: static assertion failed due to requirement '!std::is_invocable_v<const std::ranges::__crend::__fn, int (&)[]>'
    4 | static_assert(!std::is_invocable_v<RangeCREndT, int (&)[]>);
      |               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

Based on the bug and this patch I am correct that "transformation" of an array of unknown bounds to anarray of known bounds only happens with an explicit cast and not implicitly?

Yes, type is changed only for explicit casts.

I have a strong suspicion that this patch changes the test to

Well technically it only changes explicit casts, so no. With this patch this static assertion still fails

static_assert(std::is_same<int (&)[1], int(&)[]>::value);

What I'm seeing is that in libc++ there is a bunch of explicit static casts in ranges::__crend::__fn that endup transformed:

namespace ranges {
namespace __crend {                                                                    
struct __fn {
  template <class _Tp>
    requires is_lvalue_reference_v<_Tp&&>                                              
  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
  constexpr auto operator()(_Tp&& __t) const
    noexcept(noexcept(ranges::rend(static_cast<const remove_reference_t<_Tp>&>(__t))))
    -> decltype(      ranges::rend(static_cast<const remove_reference_t<_Tp>&>(__t)))  
    { return          ranges::rend(static_cast<const remove_reference_t<_Tp>&>(__t)); }
                                                                                       
  template <class _Tp>                                                                 
    requires is_rvalue_reference_v<_Tp&&>                                              
  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
  constexpr auto operator()(_Tp&& __t) const
    noexcept(noexcept(ranges::rend(static_cast<const _Tp&&>(__t))))                    
    -> decltype(      ranges::rend(static_cast<const _Tp&&>(__t)))
    { return          ranges::rend(static_cast<const _Tp&&>(__t)); }
};
} // namespace __crend

Is that expected?

Please note that other implementations would do the same transformation and that is the point of this bugfix:
https://godbolt.org/z/ff8f3YEbz

What do you mean with libstdc++ is fine?

What I mean is when I do (current patch is applied to clang):

source/llvm-project/build/bin/clang++ -std=c++20 t.cpp -c // compilation succeeds

I see what you mean now.

What I'm seeing is that in libc++ there is a bunch of explicit static casts in ranges::__crend::__fn that endup transformed:

namespace ranges {
namespace __crend {                                                                    
struct __fn {
  template <class _Tp>
    requires is_lvalue_reference_v<_Tp&&>                                              
  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
  constexpr auto operator()(_Tp&& __t) const
    noexcept(noexcept(ranges::rend(static_cast<const remove_reference_t<_Tp>&>(__t))))
    -> decltype(      ranges::rend(static_cast<const remove_reference_t<_Tp>&>(__t)))  
    { return          ranges::rend(static_cast<const remove_reference_t<_Tp>&>(__t)); }
                                                                                       
  template <class _Tp>                                                                 
    requires is_rvalue_reference_v<_Tp&&>                                              
  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
  constexpr auto operator()(_Tp&& __t) const
    noexcept(noexcept(ranges::rend(static_cast<const _Tp&&>(__t))))                    
    -> decltype(      ranges::rend(static_cast<const _Tp&&>(__t)))
    { return          ranges::rend(static_cast<const _Tp&&>(__t)); }
};
} // namespace __crend

Is that expected?

I'm not very familiar with ranges but looking at the standard the static_cast is not required. I don't think the original author intended this behaviour which you implement in this patch. (To be honest I'm a bit surprised by this change in the language. I wonder how many other (library) developers it will catch off-guard.)

I agree this is a bug in libc++ and we should fix it. As mentioned I'm not too familiar with ranges. Maybe @var-const has time, otherwise
we need to wait for @philnik.

I agree this is a bug in libc++ and we should fix it. As mentioned I'm not too familiar with ranges.

Thanks! I submitted https://github.com/llvm/llvm-project/issues/64250 so we don't forget about it.