This is an archive of the discontinued LLVM Phabricator instance.

[libc++] add zip_view and views::zip for C++23
ClosedPublic

Authored by huixie90 on Mar 31 2022, 6:14 AM.

Details

Summary
  • add zip_view and views::zip for C++23
  • added unit tests
  • implemented section 5.6 (zip) in P2321R2

I used clang-format to format the files but they look nothing like the rest of the code base. Manually indenting each line to match the styles sounds like an impossible task. Is there any clang-format file which can format it reasonable similar to the rest of the code base so that I can manually format the rest lines that look weird?

Diff Detail

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes
huixie90 added inline comments.Apr 6 2022, 3:34 PM
libcxx/test/std/ranges/range.adaptors/range.zip/begin.pass.cpp
43

fair enough, i will remove all usage of the macro

47

fair enough. I am ok with either way. To make you feel better, I changed all the same_as to is_same_v

libcxx/test/std/ranges/range.adaptors/range.zip/ctad.compile.pass.cpp
34–35

I used to do std::declval a lot in this kind of tests, but in a previous review Arthur pointed out that the test is much less readable than just create a normal variable like normal user would do. So I think I agree with him in this case.

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/compare.pass.cpp
33

for the sake of simplicity. I declared other operators but not implemented. it is declared so that the random access concept check would pass.

If zip_view is going to use those operators, it would fail the compile time test because these unimplemented operators are not constexpr. Also, it will fail to link if they are used because there are no implementations.

It works for clang trunk and gcc 11.2. i will check if this approach can pass the CI

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/iter_swap.pass.cpp
22

sure I would create a iterator with customised iter_swap.

BTW, std::swap won't work with the current status of libcxx. This is because

using std::swap;
swap(*iter1, *iter2);

would not compile, as there is no swap overload that can take two prvalue of std::tuple<int&, int&>

If you use the triple-move implementation for std::swap, it won't work either.

Once we implemented the other section about tuple in this paper, it might make the swap version work

libcxx/test/std/ranges/range.adaptors/range.zip/sentinel/ctor.other.pass.cpp
58

The constructor is not explicit. see line63 I am testing the implicit conversions

libcxx/test/std/ranges/range.adaptors/range.zip/sentinel/eq.pass.cpp
60

sure. the test won't fail with other implementations because I am using LIBCPP_STATIC_ASSERT.

LIBCPP_STATIC_ASSERT is not the verification. It is just to double check I have a sentinel. The tests that testing simple-view-ness is in a different file.

the actual tests here are the lines below which exercise the ==

huixie90 updated this revision to Diff 421101.Apr 6 2022, 11:49 PM

add missing private_headers test

huixie90 updated this revision to Diff 421125.Apr 7 2022, 1:40 AM

fix CI failure

(Note: I started reviewing this and hope to finish the first pass by end of the week)

This looks really good so far. It's mostly nits from my side.

libcxx/docs/Status/ZipProjects.csv
10–14

Could you add the link to this patch?

libcxx/include/__ranges/zip_view.h
341
435
libcxx/test/std/ranges/range.adaptors/range.zip/borrowing.compile.pass.cpp
36–42

You don't have to put the static_asserts inside a function.

libcxx/test/std/ranges/range.adaptors/range.zip/ctad.compile.pass.cpp
34–35

OK, I don't have a strong opinion here. You can leave it as-is if you think it's easier to read.

libcxx/test/std/ranges/range.adaptors/range.zip/ctor.views.pass.cpp
21

It looks like you aren't checking the that zip accepts all the ranges types.

libcxx/test/std/ranges/range.adaptors/range.zip/end.pass.cpp
21–67

I really like this! Thanks!

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/compare.pass.cpp
33

That's also OK.

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/iter_move.pass.cpp
29

I think you aren't checking that a custom iter_move is called properly. This should be quite similar to the iter_swap test.

libcxx/test/std/ranges/range.adaptors/range.zip/range.concept.compile.pass.cpp
333

Could you add a newline?

libcxx/test/std/ranges/range.adaptors/range.zip/types.h
19–21

This header should only ever be included by the zip tests, right?

huixie90 updated this revision to Diff 422871.Apr 14 2022, 8:10 AM
huixie90 marked 9 inline comments as done.

addressed review comments

libcxx/test/std/ranges/range.adaptors/range.zip/ctor.views.pass.cpp
21

added more tests for bidi, random_access, contiguous.
output only ranges are not supported by the zip_view class. There was already a test in range.concept.compile.pass.cpp to check that output_range isn't supported.

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/iter_move.pass.cpp
29

Could you please elaborate? The iter_move test looks really similar to iter_swap test

philnik accepted this revision as: philnik.Apr 14 2022, 8:18 AM

This LGTM. Let's see what @var-const thinks.

libcxx/docs/Status/ZipProjects.csv
10–13
libcxx/test/std/ranges/range.adaptors/range.zip/iterator/iter_move.pass.cpp
29

Yes, sorry. I don't know what I was looking for, but it looks good.

huixie90 updated this revision to Diff 422885.Apr 14 2022, 9:03 AM
huixie90 marked 2 inline comments as done.

fix doc

var-const requested changes to this revision.Apr 21 2022, 4:54 PM

Sorry about the slow review. Thanks a lot for working on this huge patch!

One comment I have that applies throughout most tests -- can you please add a brief comment to each test case to explain what exactly it tests? Even a short description makes things easier to skim through. Additionally, could you please separate the test cases by blank lines? It's a minor nit, but I think it makes it a little easier to see where one test case ends and another one starts.

libcxx/include/__ranges/zip_view.h
85

Is specifying the complicated return type necessary here (i.e., could this function get away with just auto)?

287

Nit: a few of these functions should be formatted so that the opening brace is on the same line as the function name.

342

Nit: can __bs be a little more descriptive?

395

Why is this implementation different from the overload above (and from the Standard)?

430

Hmm, is this equivalent to the Standard's wording?

The exception specification is equivalent to the logical AND of the following expressions:

noexcept(ranges::iter_swap(std::get<i>(l.current_), std::get<i>(r.current_)))

for every integer  `0 ≤ i < sizeof...(Views)`.
472

Optional: since this pattern is used twice (here and in __iterator), consider splitting it into a function (e.g. __tuple_zip_any_of).

514

Hmm, looking at the latest draft, it says

Given a pack of subexpressions `Es...`, the expression `views​::​zip(Es...)` is expression-equivalent to
<...>
(2.2) otherwise, `zip_­view<views​::​all_­t<decltype((Es))>...>(Es...)`.

This does not appear equivalent (missing views::all_t).

libcxx/test/std/ranges/range.adaptors/range.zip/begin.pass.cpp
14

Is a case where neither of the overloads is available? (i.e., is it possible for a type to not satisfy range<const Views> && ... while still satisfying the class constraints?) If yes, can you add a compile-time test to check that case?

libcxx/test/std/ranges/range.adaptors/range.zip/borrowing.compile.pass.cpp
36

Nit: s/borrwed/borrowed/.

libcxx/test/std/ranges/range.adaptors/range.zip/cpo.pass.cpp
13

This file should also check piping zip. Can you take a look at test/std/ranges/range.adaptors/range.filter/adaptor.pass.cpp and implement the relevant checks from that test here?

Also, I'd rename it into adaptor.pass.cpp for consistency with other views.

libcxx/test/std/ranges/range.adaptors/range.zip/ctor.default.pass.cpp
33

Consider renaming to NoDefaultCtrView.

42

Hmm, I don't see a constraint on either the default constructor or the class itself that requires Views to be default-constructible. I'm probably missing it -- can you please check?

libcxx/test/std/ranges/range.adaptors/range.zip/ctor.views.pass.cpp
28

Wouldn't std::convertible_to work?

40

Seems like this could be UB? Consider making moves a one-element array.

61

Nit: s/stack/the local variable/ -- the parameter is also on the stack, after all.

67

Optional: these checks are so similar that they could be extracted into a helper function that takes two template arguments for the view types to create. But the current state is also fine since there are only 3 similar cases (extraction would allow you to add more cases easily if you'd like).

libcxx/test/std/ranges/range.adaptors/range.zip/end.pass.cpp
78

Shouldn't these check v.end() rather than v.begin()? (applies throughout)

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/arithmetic.pass.cpp
13

Please specify the constraints (I'd be okay with just writing something like:

// All the arithmetic operators have the constraint `requires all-random-access<Const, Views...>;`,
// except `operator-` which has the constraint `requires (sized_­sentinel_­for<iterator_t<maybe-const<Const, Views>>,
                                   iterator_t<maybe-const<Const, Views>>> && ...);`

(of course, double check the above for correctness if you decide to use it)

35

Nit: this blank line seems extraneous.

64

These checks seem unnecessary -- it's already checked at runtime above (applies below as well).

112

Nit: s/range/the ranges/ (here and below).

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/ctor.default.pass.cpp
35

Question: what does "default" and "non-default" mean in the context of these classes?

48

Similar question to zip_view's default constructor -- where does this constraint come from?

55

Nit: can this be = zip_iter<Default> (or just use zip_iter<Default> without an alias)?

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/ctor.other.pass.cpp
13

Missing requires clause.

34

Can you also check the case when the conversion is from non-const to const, but (!convertible_­to<iterator_t<Views>, iterator_t<maybe-const<Const, Views>>> && ...)?

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/decrement.pass.cpp
14

Nit: this synopsis is missing the requires clause.

24

Optional: probably overkill, but it could also check the post-decrement version (using "or").

64

What's the reason to not go from the end like in the previous test case?

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/increment.pass.cpp
83

Nit: s/forword/forward/.

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/iter_move.pass.cpp
13

Missing the noexcept clause (here and in iter_swap test).

37

Wouldn't it be easier to just create a local variable and use it here instead of declval?

43

Note: there's an ASSERT_NOT_NOEXCEPT macro, but using it is completely optional. I don't really have a preference.

46

iter_move must call ranges::iter_move, which is a customization point object. Can you please check that your specialization of iter_move is found via ADL even if it's in a different namespace (see some other views, e.g. lazy_split_view, for an example)?

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/iter_swap.pass.cpp
44

Check the inverse case as well? (i.e., !noexcept)

47

Same comment re. ADL as iter_move.

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/member_types.compile.pass.cpp
39

Nit: use PascalCase for consistency with names of the other classes in this file?

73

Can you also test the case when iterator_concept is bidirectional_iterator_tag?

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/subscript.pass.cpp
13

Note: in synopsis, there is no reason to mangle names -- i.e., please s/__n/n/. Can you please check other test files' synopses for similar issues?

14

Please check the constraint as well.

libcxx/test/std/ranges/range.adaptors/range.zip/range.concept.compile.pass.cpp
153

Nit: unnecessary blank line.

158

Nit: s/static_assert/static_asserts/ (or static assertions).

307

Can you use the output iterator from test_iterators.h instead?

libcxx/test/std/ranges/range.adaptors/range.zip/sentinel/ctor.other.pass.cpp
35

Nit: s/Converitble/Convertible/.

libcxx/test/std/ranges/range.adaptors/range.zip/sentinel/eq.pass.cpp
60

Nit: weird formatting.

80

Question: isn't this checked by the comparisons above?

82

Please add empty lines between the test cases and short comment describing what each test case does.

libcxx/test/std/ranges/range.adaptors/range.zip/sentinel/minus.pass.cpp
111

Nit: weird formatting for these concepts (clang-format doesn't support concepts properly, IIRC).

121

Please add short comments describing what each test case does.

libcxx/test/std/ranges/range.adaptors/range.zip/size.pass.cpp
55

Same nit re. empty lines and comments for each test case. Looks like it applies throughout the patch.

libcxx/test/std/ranges/range.adaptors/range.zip/types.h
24

Similar comments to join_view -- BufferView would be a more appropriate name, see if you can use std::span instead.

45

Good idea. We have some existing test code that we could simplify this way.

46

Nit: the opening brace goes on the same line as the function name (applies throughout the file, and possibly in other files as well).

310

Nit: extra semicolon.

var-const added inline comments.Apr 21 2022, 4:54 PM
libcxx/include/__ranges/zip_view.h
241

Is the following section implemented and tested?

If the invocation of any non-const member function of `iterator` exits via an exception, the iterator acquires a singular value.

(http://eel.is/c++draft/range.zip#iterator-3)

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/arithmetic.pass.cpp
98

Nit: s/each/each of the/.

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/compare.pass.cpp
13

Please add the constraints to the synopsis.

22

Also check for the presence of operator==?

24

Same nit as another file: it's a little inconsistent that this class name is in snake_case.

24

Nit: s/smaller/less/ (throughout the file). I think less is more commonly used in the arithmetic context.

34

Question: is defining operator[] necessary?

70

Is this declaration necessary? It seems to go against the purpose of the test, which IIUC verifies that having just operator== is sufficient.

75

Question: do these operators have to be declared?

100

Ultranit: s/SubRange/Subrange/ (the Standard considers it a single word).

111

These checks are verbose and, as far as I can see, same across the test cases -- can you move them to a helper function?

187

Please also check that operator== is unavailable, and ideally all other operators.

This revision now requires changes to proceed.Apr 21 2022, 4:54 PM
huixie90 updated this revision to Diff 424438.Apr 22 2022, 4:37 AM
huixie90 marked 48 inline comments as done.

address review comments

libcxx/include/__ranges/zip_view.h
85

I guess I'd have to spell this type in the return statement (currently it is just return {.....}).

241

AFAIK, doing anything with iterator of singular value would result in undefined behaviour (except for very few operations). one question is can we take advantage of the undefined behaviour and not do anything here?

One thing we could do is to value initialise all the underlying iterators. but what if underlying iterators are not default constructible?

287

it is done by clang-format. happy to format manually.

but imo it would be great to come up with a clang-format file that everyone is sort of OK-ish with it. It would save everyone's time

395

isn't this just delegating to the above? (thus same as above?)

430

I think so. In the standard, it basically says all the iter_swap(std::get<i>(l.current_), std::get<i>(r.current_) are noexcept. std::get itself is always noexcept, so the standard basically equivalent to all the ranges::iter_swap are noexcept.

Here the implementation is checking the same thing

514

good catch. added unit test to lock down this. for some reason, I relied on the user defined CTAD (which has the all_t thing, but forgot about the implicit CTAD from copy constructors which don't have all_t)

libcxx/test/std/ranges/range.adaptors/range.zip/begin.pass.cpp
14

I don't think it is possible.
To make non-const overload not exist, all Views have to model simple-view. simple-view implies range<const View>. so the const overload must exist in that case.

libcxx/test/std/ranges/range.adaptors/range.zip/cpo.pass.cpp
13

views::zip is not an "adaptor". It is only a CPO. So it doesn't support pipe operation.
Therefore the test is not named as adaptor.pass.cpp but cpo.pass.cpp

The name views​::​zip denotes a customization point object ([customization.point.object]).

libcxx/test/std/ranges/range.adaptors/range.zip/ctor.default.pass.cpp
42

I had the same puzzle before I implemented this and asked sg9. Why there is no "requires (default_initializable<iterator_t<maybe-const<const, views>>> && ...)

The answer is that it is implicitly required by the tuple's constructor. if any of the iterators are not default constructible, zip_iterator's =default would be implicitly deleted.

libcxx/test/std/ranges/range.adaptors/range.zip/ctor.views.pass.cpp
28

My original thought was convertible_to doesn't work for multiple arguments but zip_view constructor can take multiple arguments. so i implemented a multi-args version.
But then I forgot to test the multi-args case.

40

I don't think this is UB. AFAIK, this is how single_view is implemented

https://eel.is/c++draft/range.single#view-5

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/compare.pass.cpp
70

This particular test point was suggested by @philnik . His point is that testing "zip_iterator is never calling underlying iterator's >, >=, <=, !=
because the spec says zip_iterator's >= is calling zip_iterator's (not less_than). So tested this way: declare all the operations >, >=, <= etc to make it satisfy random_access iterator concept. but not define them. if the zip_iterator's >,>=, <=, etc isn't implemented as the way standard defined but calling underlying iterator's >,>=,<=, we will get a linker error for runtime tests and non-constant expression for compile time test.

added comments to explain this

75

This particular test point was suggested by @philnik . His point is that testing "zip_iterator is never calling underlying iterator's >, >=, <=, !=
because the spec says zip_iterator's >= is calling zip_iterator's (not less_than). So tested this way: declare all the operations >, >=, <= etc to make it satisfy random_access iterator concept. but not define them. if the zip_iterator's >,>=, <=, etc isn't implemented as the way standard defined but calling underlying iterator's >,>=,<=, we will get a linker error for runtime tests and non-constant expression for compile time test

added comments to explain this

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/ctor.default.pass.cpp
48

this comes implicitly from the tuple's constructor

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/decrement.pass.cpp
64

In this case, zip_view is not a common_range, end returns a sentinel which can't be decremented

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/iter_move.pass.cpp
46

happy to add the extra test.
Just FYI, this is somehow implicitly tested by the #include order. we include <ranges> before the "types.h". If it were not using ADL, it couldn't find our test iterator's iter_move because normal lookup requires the declaration to be seen first.

huixie90 updated this revision to Diff 424463.Apr 22 2022, 6:49 AM
huixie90 marked 7 inline comments as done.

addressed comments

var-const requested changes to this revision.Apr 22 2022, 10:01 PM

Almost LGTM, just a few nits.

libcxx/include/__ranges/zip_view.h
85

I mean, could you rely on CTAD? To be clear, it's just a question -- perhaps CTAD won't work here.

241

Perhaps we should test that the iterator is reassignable/destroyable, but perhaps it's overkill. Let's check with Louis @ldionne.

287

I fully agree, being able to use clang-format would be great (though note that it's not fully straightforward on our code base).

395

It's a little easier to compare when things are close to the Standard text (unless, of course, there's a reason to diverge), but it's a really small thing -- we can keep as is.

428

Very optional: this comment can fit in one line.

430

Sounds reasonable, but I'd ask Louis @ldionne to double-check just in case.

libcxx/test/std/ranges/range.adaptors/range.zip/begin.pass.cpp
52

A word seems to be missing -- "should _be_ at the begin position"?

libcxx/test/std/ranges/range.adaptors/range.zip/cpo.pass.cpp
13

Ah, makes sense, never mind.

libcxx/test/std/ranges/range.adaptors/range.zip/ctor.default.pass.cpp
42

Because it's not obvious, can you please add this as a comment?

libcxx/test/std/ranges/range.adaptors/range.zip/ctor.views.pass.cpp
28

Ah, right (you might also want to check that the default constructor is explicit, though it starts going into the overkill territory).

40

Thanks for pointing it out -- it made me find this post that quotes the relevant part of the Standard (updated to the current draft):

When an expression J that has integral type is added to... an expression P of pointer type...

<...>

(4.2) Otherwise, if P points to an array element i of an array object x with n elements ([dcl.array]),70 the expressions P + J and J + P... point to the (possibly-hypothetical) array element i + j of x if 0 ≤ i+j ≤ n...

<...>

  1. As specified in [basic.compound], an object that is not an array element is considered to belong to a single-element array for this purpose and a pointer past the last element of an array of n elements is considered to be equivalent to a pointer to a hypothetical array element n for this purpose.
67

(By the way, ignoring optional comments is fine -- that's why they're optional, after all -- but if you decide not to implement an optional suggestion, can you please add a comment to that effect? This would help me understand whether this is something you plan to come back to later or something you decided to push back on. Once again, pushing back is fine -- I just want to make sure I understand the state of the patch, i.e. whether all comments are addressed one way or the other)

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/arithmetic.pass.cpp
20

Nit: I think the meaning would be a little more obvious if s/which/which instead/. (I know the current text is my suggestion)

102

Nit: with the new sentence structure, I think this should be sentinels.

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/compare.pass.cpp
36

Some nits:

  • s/as/in;
  • consider in the way defined by the standard;
  • I think there should be no comma between defined and but;
  • consider s/but/but instead/;
  • (ultranit) please add a full stop at the end of the sentence.
130

Nit: s/Opeartors/Operators/.

239

Should it also call inequalityOperatorsDoNotExistTest?

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/ctor.other.pass.cpp
48

Is it possible to also add a static_assert to show that the convertible_­to constraint is not satisfied in this case? (Only if it's straightforward, it's basically to "test the test" and to make it a little more obvious what exactly it's testing)

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/iter_move.pass.cpp
46

Thanks for adding the test! Relying implicitly on the include order would be too subtle, IMO.

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/member_types.compile.pass.cpp
70

Nit: s/shoud/should/.

70

Nit: probably s/ranges/views/? That's the name of the template argument / constructor argument.

147

Nit: s/multiple/of multiple (here and below).

libcxx/test/std/ranges/range.adaptors/range.zip/sentinel/eq.pass.cpp
132

Thanks for adding all the comments! Much easier to skim through now.

libcxx/test/std/ranges/range.adaptors/range.zip/sentinel/minus.pass.cpp
160

Optional nit: s/minus/subtract/.

This revision now requires changes to proceed.Apr 22 2022, 10:01 PM
huixie90 updated this revision to Diff 424712.Apr 23 2022, 4:15 AM
huixie90 marked 23 inline comments as done.

address review comments

libcxx/include/__ranges/zip_view.h
85

Originally I relied on tuple's CTAD, but that crashed gcc 11.2
https://discord.com/channels/636084430946959380/636732894974312448/960574541254496337

Later I tried make_tuple. There are two issues.

  1. It is inconsistent with the rest of the code, where in other places, two elements thing are stored in a pair. Although this function is internal only, but still... consistency is good.
  1. CTAD/make_tuple decays. In case the function returns references, I would get tuple of values instead of tuple of references. Again, this is not an issue for now, because the usages of this helper function only return values (booleans and integers at the moment). But again, if one day this is used with references, it would become a problem silently.
241

I asked around and people seem to agree on that that paragraph is to empower implementations not to do anything by given less guarantees that things should work. But let's wait until Louis is back

287

cool. I will hand format for now

libcxx/test/std/ranges/range.adaptors/range.zip/ctor.views.pass.cpp
28

The default constructor is not explicit and it is tested already

40

thanks for the explanation

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/compare.pass.cpp
34

yes. this is one of the requirement of random_access_iterator

libcxx/test/std/ranges/range.adaptors/range.zip/iterator/ctor.other.pass.cpp
48

yes good idea. added static_assert right after the declaration of ConstIterIncompatibleView

var-const accepted this revision as: var-const.Apr 23 2022, 2:05 PM

LGTM -- thanks a lot for working on this huge patch! I'll leave the final approval for @ldionne.

libcxx/include/__ranges/zip_view.h
85

Oh, interesting, thanks for explaining. It's certainly not worth it to work around compiler bugs and such, the current state is fine.

libcxx/test/std/ranges/range.adaptors/range.zip/ctor.default.pass.cpp
40

Ultranit: capitalize.

ldionne accepted this revision.Apr 24 2022, 9:15 AM

I looked around and it seemed like this had a thorough review already. I just replied to the comments where I was @'ed.

Thanks to everyone involved for the work and especially @huixie90 for authoring the patch. This LGTM with the last few missing comments addressed.

libcxx/include/__ranges/zip_view.h
241

Sorry, I feel I'm missing some context, but it seems to me that a singular iterator should still be destroyable and assignable-to (unless there's something in the spec that contradicts that, but I don't think there is). If that's correct, then we should make sure that works and test it. Please LMK if what I'm saying doesn't make sense, since I'm commenting out of context.

430

Yeah, this sounds reasonable to me.

This revision is now accepted and ready to land.Apr 24 2022, 9:15 AM
huixie90 updated this revision to Diff 424792.Apr 24 2022, 11:10 AM
huixie90 marked 8 inline comments as done.

added unit test for singular case

huixie90 updated this revision to Diff 424819.Apr 24 2022, 11:04 PM

fix CI failure

I don't have access to push the commits.
Name: Hui Xie
Email: hui.xie1990@gmail.com

This revision was landed with ongoing or failed builds.Apr 25 2022, 3:22 AM
This revision was automatically updated to reflect the committed changes.