Skip to content

Commit 3cb3c75

Browse files
committed
Rename Expr to Map
This way Map/map follows the pattern of Pick/pick, and we save names. This is all internal. * ra/expr.hh (Map): As stated. (pick_at, pick_star): Golfing in return types. Elsewhere reuse users in tests and such.
1 parent c801bc0 commit 3cb3c75

29 files changed

+332
-350
lines changed

TODO

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ checkbox [C-ct] flip TODO
7777
- This has become more feasible after changing the iterator interface from flat to saveload,
7878
main obstacle atm seems to be the need to support copy. <2023-11-20 Mon 12:45>
7979
- One can now create a ravel iterator from an IteratorConcept <2023-11-28 Tue 16:37>
80-
- [ ] gemv(conj(a), b) should work. Beat View-like selectors down an Expr??
80+
- [ ] gemv(conj(a), b) should work. Beat View-like selectors down a Map??
8181
- [ ] port some of the View ops to generic Iterator. reverse, transpose, etc. seem easy
82-
enough. Only it kind of bothers me that they need their own Expr-like types while on Views
83-
it's just a one time op. Propagating ops down Expr into leaf Views (a kind of beating) would
82+
enough. Only it kind of bothers me that they need their own Map-like types while on Views
83+
it's just a one time op. Propagating ops down Map into leaf Views (a kind of beating) would
8484
be better.
8585
- [X] Support operator <=> <2020-09-15 Tue 13:50>
8686
- https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96278 is annoying

docs/index.html

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/ra-ra.texi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1996,7 +1996,7 @@ These are array types that own their data in one way or another.
19961996

19971997
These are array views into data in memory, which may be writable. Any of the @b{Container} types can be treated as a @b{View}, but one may also create @b{View}s into memory that has been allocated independently.
19981998

1999-
@item @b{Iterator} --- @code{CellBig}, @code{CellSmall}, @code{Iota}, @code{Ptr}, @code{Scalar}, @code{Expr}, @code{Pick}
1999+
@item @b{Iterator} --- @code{CellBig}, @code{CellSmall}, @code{Iota}, @code{Ptr}, @code{Scalar}, @code{Map}, @code{Pick}
20002000

20012001
This is a traversable object. @b{Iterator}s are accepted by all the array functions such as @code{map}, @code{for_each}, etc. @code{map} produces an @b{Iterator} itself, so most array expressions are @b{Iterator}s. @b{Iterator}s are created from @b{View}s and from certain foreign array-like types primarily through the function @code{start}. This is done automatically when those types are used in array expressions.
20022002

ra/base.hh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ rank(auto const & v)
157157
RA_IS_DEF(is_pos, 0!=rank_s<A>())
158158
template <class A> concept is_ra_pos = is_ra<A> && is_pos<A>;
159159
template <class A> concept is_zero_or_scalar = (is_ra<A> && !is_pos<A>) || is_scalar<A>;
160-
// all args rank 0 (immediate application), but at least one ra:: (don't collide with the scalar version).
160+
// all args rank 0 (apply immediately), but at least one ra:: (disambiguate scalar version).
161161
RA_IS_DEF(is_special, false) // rank-0 types that we don't want reduced.
162162
template <class ... A> constexpr bool toreduce = (!is_scalar<A> || ...) && ((is_zero_or_scalar<A> && !is_special<A>) && ...);
163163
template <class ... A> constexpr bool tomap = ((is_ra_pos<A> || is_special<A>) || ...) && ((is_ra<A> || is_scalar<A> || is_fov<A> || is_builtin_array<A>) && ...);

ra/expr.hh

Lines changed: 44 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ struct Match<checkp, std::tuple<P ...>, mp::int_list<I ...>>
389389
#pragma GCC diagnostic warning "-Warray-bounds"
390390
dim_t ls = len(k);
391391
#pragma GCC diagnostic pop
392-
if (((k<ra::rank(std::get<I>(t)) && ls!=choose_len(std::get<I>(t).len(k), ls)) || ...)) {
392+
if (((k<ra::rank(get<I>(t)) && ls!=choose_len(get<I>(t).len(k), ls)) || ...)) {
393393
return false;
394394
}
395395
}
@@ -419,7 +419,7 @@ struct Match<checkp, std::tuple<P ...>, mp::int_list<I ...>>
419419
rank() const requires (ANY==rs)
420420
{
421421
rank_t r = BAD;
422-
((r = choose_rank(r, ra::rank(std::get<I>(t)))), ...);
422+
((r = choose_rank(r, ra::rank(get<I>(t)))), ...);
423423
assert(ANY!=r); // not at runtime
424424
return r;
425425
}
@@ -445,34 +445,34 @@ struct Match<checkp, std::tuple<P ...>, mp::int_list<I ...>>
445445
auto f = [&k](dim_t s, auto const & a) {
446446
return k<ra::rank(a) ? choose_len(s, a.len(k)) : s;
447447
};
448-
dim_t s = BAD; ((s>=0 ? s : s = f(s, std::get<I>(t))), ...);
448+
dim_t s = BAD; ((s>=0 ? s : s = f(s, get<I>(t))), ...);
449449
assert(ANY!=s); // not at runtime
450450
return s;
451451
}
452452
// could preserve static, but ply doesn't use it atm.
453453
constexpr auto
454454
step(int i) const
455455
{
456-
return std::make_tuple(std::get<I>(t).step(i) ...);
456+
return std::make_tuple(get<I>(t).step(i) ...);
457457
}
458458
constexpr void
459459
adv(rank_t k, dim_t d)
460460
{
461-
(std::get<I>(t).adv(k, d), ...);
461+
(get<I>(t).adv(k, d), ...);
462462
}
463463
constexpr bool
464464
keep(dim_t st, int z, int j) const requires (!(requires { P::keep(st, z, j); } && ...))
465465
{
466-
return (std::get<I>(t).keep(st, z, j) && ...);
466+
return (get<I>(t).keep(st, z, j) && ...);
467467
}
468468
constexpr static bool
469469
keep(dim_t st, int z, int j) requires (requires { P::keep(st, z, j); } && ...)
470470
{
471471
return (std::decay_t<P>::keep(st, z, j) && ...);
472472
}
473-
constexpr auto save() const { return std::make_tuple(std::get<I>(t).save() ...); }
474-
constexpr void load(auto const & pp) { ((std::get<I>(t).load(std::get<I>(pp))), ...); }
475-
constexpr void mov(auto const & s) { ((std::get<I>(t).mov(std::get<I>(s))), ...); }
473+
constexpr auto save() const { return std::make_tuple(get<I>(t).save() ...); }
474+
constexpr void load(auto const & pp) { ((get<I>(t).load(get<I>(pp))), ...); }
475+
constexpr void mov(auto const & s) { ((get<I>(t).mov(get<I>(s))), ...); }
476476
};
477477

478478

@@ -633,12 +633,17 @@ struct Framematch_def<V, std::tuple<Ti ...>, std::tuple<Ri ...>, skip>
633633
// explicit agreement checks
634634
// ---------------
635635

636+
template <bool checkp, class ... P>
637+
constexpr auto
638+
match(P && ... p) { return Match<checkp, std::tuple<P ...>> { RA_FWD(p) ... }; }
639+
640+
template <class ... P>
636641
constexpr bool
637-
agree(auto && ... p) { return agree_(ra::start(RA_FWD(p)) ...); }
642+
agree(P && ... p) { return match<false>(ra::start(RA_FWD(p)) ...).check(); }
638643

639-
// 0: fail, 1: rt, 2: pass
644+
template <class ... P>
640645
constexpr int
641-
agree_s(auto && ... p) { return agree_s_(ra::start(RA_FWD(p)) ...); }
646+
agree_s(P && ... p) { return decltype(match<false>(ra::start(RA_FWD(p)) ...))::check_s(); }
642647

643648
template <class Op, class ... P> requires (is_verb<Op>)
644649
constexpr bool
@@ -648,14 +653,6 @@ template <class Op, class ... P> requires (!is_verb<Op>)
648653
constexpr bool
649654
agree_op(Op && op, P && ... p) { return agree(RA_FWD(p) ...); }
650655

651-
template <class ... P>
652-
constexpr bool
653-
agree_(P && ... p) { return (Match<false, std::tuple<P ...>> { RA_FWD(p) ... }).check(); }
654-
655-
template <class ... P>
656-
constexpr int
657-
agree_s_(P && ... p) { return Match<false, std::tuple<P ...>>::check_s(); }
658-
659656
template <class V, class ... T, int ... i>
660657
constexpr bool
661658
agree_verb(mp::int_list<i ...>, V && v, T && ... t)
@@ -672,89 +669,82 @@ agree_verb(mp::int_list<i ...>, V && v, T && ... t)
672669
template <class E>
673670
decltype(auto) to_scalar(E && e)
674671
{
675-
if constexpr (1!=size_s(e)) {
672+
if constexpr (constexpr dim_t s=size_s(e); 1!=s) {
673+
static_assert(ANY==s, "Bad scalar conversion from shape.");
676674
RA_CHECK(1==size(e), "Bad scalar conversion from shape [", fmt(nstyle, ra::shape(e)), "].");
677675
}
678676
return *e;
679677
}
680678

681-
template <class Op, class T, class K=mp::iota<mp::len<T>>> struct Expr;
679+
template <class Op, class T, class K=mp::iota<mp::len<T>>> struct Map;
682680
template <class Op, IteratorConcept ... P, int ... I>
683-
struct Expr<Op, std::tuple<P ...>, mp::int_list<I ...>>: public Match<true, std::tuple<P ...>>
681+
struct Map<Op, std::tuple<P ...>, mp::int_list<I ...>>: public Match<true, std::tuple<P ...>>
684682
{
685683
using Match_ = Match<true, std::tuple<P ...>>;
686684
using Match_::t;
687685
Op op;
688686

689-
constexpr Expr(Op op_, P ... p_): Match_(p_ ...), op(op_) {} // [ra1]
690-
RA_ASSIGNOPS_SELF(Expr)
687+
constexpr Map(Op op_, P ... p_): Match_(p_ ...), op(op_) {} // [ra1]
688+
RA_ASSIGNOPS_SELF(Map)
691689
RA_ASSIGNOPS_DEFAULT_SET
692-
constexpr decltype(auto) at(auto const & j) const { return std::invoke(op, std::get<I>(t).at(j) ...); }
693-
constexpr decltype(auto) operator*() const { return std::invoke(op, *std::get<I>(t) ...); }
694-
constexpr operator decltype(std::invoke(op, *std::get<I>(t) ...)) () const { return to_scalar(*this); }
690+
constexpr decltype(auto) at(auto const & j) const { return std::invoke(op, get<I>(t).at(j) ...); }
691+
constexpr decltype(auto) operator*() const { return std::invoke(op, *get<I>(t) ...); }
692+
constexpr operator decltype(std::invoke(op, *get<I>(t) ...)) () const { return to_scalar(*this); }
695693
};
696694

697695
template <class Op, IteratorConcept ... P>
698-
constexpr bool is_special_def<Expr<Op, std::tuple<P ...>>> = (is_special<P> || ...);
696+
constexpr bool is_special_def<Map<Op, std::tuple<P ...>>> = (is_special<P> || ...);
699697

700-
template <class V, class ... T, int ... i>
698+
template <class Op, class ... P, int ... i>
701699
constexpr auto
702-
expr_verb(mp::int_list<i ...>, V && v, T && ... t)
700+
map_verb(mp::int_list<i ...>, Op && op, P && ... p)
703701
{
704-
using FM = Framematch<V, std::tuple<T ...>>;
705-
return expr(FM::op(RA_FWD(v)), reframe<mp::ref<typename FM::R, i>>(RA_FWD(t)) ...);
702+
using FM = Framematch<Op, std::tuple<P ...>>;
703+
return map_(FM::op(RA_FWD(op)), reframe<mp::ref<typename FM::R, i>>(RA_FWD(p)) ...);
706704
}
707705

708706
template <class Op, class ... P>
709707
constexpr auto
710-
expr(Op && op, P && ... p)
708+
map_(Op && op, P && ... p)
711709
{
712710
if constexpr (is_verb<Op>) {
713-
return expr_verb(mp::iota<sizeof...(P)> {}, RA_FWD(op), RA_FWD(p) ...);
711+
return map_verb(mp::iota<sizeof...(P)> {}, RA_FWD(op), RA_FWD(p) ...);
714712
} else {
715-
return Expr<Op, std::tuple<P ...>> { RA_FWD(op), RA_FWD(p) ... };
713+
return Map<Op, std::tuple<P ...>> { RA_FWD(op), RA_FWD(p) ... };
716714
}
717715
}
718716

719717
constexpr auto
720-
map(auto && op, auto && ... a) { return expr(RA_FWD(op), start(RA_FWD(a)) ...); }
718+
map(auto && op, auto && ... a) { return map_(RA_FWD(op), start(RA_FWD(a)) ...); }
721719

722720

723721
// ---------------------------
724722
// pick expression
725723
// ---------------------------
726724

727-
template <class T, class J> struct pick_at_type;
728-
template <class ... P, class J> struct pick_at_type<std::tuple<P ...>, J>
729-
{
730-
using type = std::common_reference_t<decltype(std::declval<P>().at(std::declval<J>())) ...>;
731-
};
725+
template <class J> struct type_at { template <class P> using type = decltype(std::declval<P>().at(std::declval<J>())); };
732726

733727
template <std::size_t I, class T, class J>
734-
constexpr pick_at_type<mp::drop1<std::decay_t<T>>, J>::type
728+
constexpr mp::apply<std::common_reference_t, mp::map<type_at<J>::template type, mp::drop1<std::decay_t<T>>>>
735729
pick_at(std::size_t p0, T && t, J const & j)
736730
{
737731
constexpr std::size_t N = mp::len<std::decay_t<T>> - 1;
738732
if constexpr (I < N) {
739-
return (p0==I) ? std::get<I+1>(t).at(j) : pick_at<I+1>(p0, t, j);
733+
return (p0==I) ? get<I+1>(t).at(j) : pick_at<I+1>(p0, t, j);
740734
} else {
741735
RA_CHECK(p0 < N, "Bad pick ", p0, " with ", N, " arguments."); std::abort();
742736
}
743737
}
744738

745-
template <class T> struct pick_star_type;
746-
template <class ... P> struct pick_star_type<std::tuple<P ...>>
747-
{
748-
using type = std::common_reference_t<decltype(*std::declval<P>()) ...>;
749-
};
739+
template <class P> using type_star = decltype(*std::declval<P>());
750740

751741
template <std::size_t I, class T>
752-
constexpr pick_star_type<mp::drop1<std::decay_t<T>>>::type
742+
constexpr mp::apply<std::common_reference_t, mp::map<type_star, mp::drop1<std::decay_t<T>>>>
753743
pick_star(std::size_t p0, T && t)
754744
{
755745
constexpr std::size_t N = mp::len<std::decay_t<T>> - 1;
756746
if constexpr (I < N) {
757-
return (p0==I) ? *(std::get<I+1>(t)) : pick_star<I+1>(p0, t);
747+
return (p0==I) ? *(get<I+1>(t)) : pick_star<I+1>(p0, t);
758748
} else {
759749
RA_CHECK(p0 < N, "Bad pick ", p0, " with ", N, " arguments."); std::abort();
760750
}
@@ -771,9 +761,9 @@ struct Pick<std::tuple<P ...>, mp::int_list<I ...>>: public Match<true, std::tup
771761
constexpr Pick(P ... p_): Match_(p_ ...) {} // [ra1]
772762
RA_ASSIGNOPS_SELF(Pick)
773763
RA_ASSIGNOPS_DEFAULT_SET
774-
constexpr decltype(auto) at(auto const & j) const { return pick_at<0>(std::get<0>(t).at(j), t, j); }
775-
constexpr decltype(auto) operator*() const { return pick_star<0>(*std::get<0>(t), t); }
776-
constexpr operator decltype(pick_star<0>(*std::get<0>(t), t)) () const { return to_scalar(*this); }
764+
constexpr decltype(auto) at(auto const & j) const { return pick_at<0>(get<0>(t).at(j), t, j); }
765+
constexpr decltype(auto) operator*() const { return pick_star<0>(*get<0>(t), t); }
766+
constexpr operator decltype(pick_star<0>(*get<0>(t), t)) () const { return to_scalar(*this); }
777767
};
778768

779769
template <IteratorConcept ... P>

ra/ply.hh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ struct WLen<Len>
4545
};
4646

4747
template <class Op, IteratorConcept ... P, int ... I> requires (has_len<P> || ...)
48-
struct WLen<Expr<Op, std::tuple<P ...>, mp::int_list<I ...>>>
48+
struct WLen<Map<Op, std::tuple<P ...>, mp::int_list<I ...>>>
4949
{
5050
constexpr static decltype(auto)
5151
f(auto ln, auto && e)
5252
{
53-
return expr(RA_FWD(e).op, wlen(ln, std::get<I>(RA_FWD(e).t)) ...);
53+
return map_(RA_FWD(e).op, wlen(ln, std::get<I>(RA_FWD(e).t)) ...);
5454
}
5555
};
5656

ra/ra.hh

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ template <class T> constexpr void cast(ra::noarg);
3838
using std::max, std::min, std::abs, std::fma, std::sqrt, std::pow, std::exp, std::swap,
3939
std::isfinite, std::isinf, std::isnan, std::clamp, std::lerp, std::conj, std::expm1;
4040

41-
#define FOR_FLOAT(T) \
42-
constexpr T conj(T x) { return x; } \
43-
FOR_EACH(FOR_FLOAT, float, double)
41+
#define FOR_FLOAT(T) \
42+
constexpr T conj(T x) { return x; } \
43+
FOR_EACH(FOR_FLOAT, float, double)
4444
#undef FOR_FLOAT
4545

4646
#define FOR_FLOAT(R) \
@@ -148,47 +148,47 @@ template <class X> concept iota_op = ra::is_zero_or_scalar<X> && std::is_arithme
148148

149149
template <is_iota I, iota_op J>
150150
constexpr auto
151-
optimize(Expr<std::plus<>, std::tuple<I, J>> && e)
151+
optimize(Map<std::plus<>, std::tuple<I, J>> && e)
152152
{ return ra::iota(ITEM(0).n, ITEM(0).i+ITEM(1), ITEM(0).s); }
153153

154154
template <iota_op I, is_iota J>
155155
constexpr auto
156-
optimize(Expr<std::plus<>, std::tuple<I, J>> && e)
156+
optimize(Map<std::plus<>, std::tuple<I, J>> && e)
157157
{ return ra::iota(ITEM(1).n, ITEM(0)+ITEM(1).i, ITEM(1).s); }
158158

159159
template <is_iota I, is_iota J>
160160
constexpr auto
161-
optimize(Expr<std::plus<>, std::tuple<I, J>> && e)
161+
optimize(Map<std::plus<>, std::tuple<I, J>> && e)
162162
{ return ra::iota(maybe_len(e), ITEM(0).i+ITEM(1).i, ITEM(0).s+ITEM(1).s); }
163163

164164
template <is_iota I, iota_op J>
165165
constexpr auto
166-
optimize(Expr<std::minus<>, std::tuple<I, J>> && e)
166+
optimize(Map<std::minus<>, std::tuple<I, J>> && e)
167167
{ return ra::iota(ITEM(0).n, ITEM(0).i-ITEM(1), ITEM(0).s); }
168168

169169
template <iota_op I, is_iota J>
170170
constexpr auto
171-
optimize(Expr<std::minus<>, std::tuple<I, J>> && e)
171+
optimize(Map<std::minus<>, std::tuple<I, J>> && e)
172172
{ return ra::iota(ITEM(1).n, ITEM(0)-ITEM(1).i, -ITEM(1).s); }
173173

174174
template <is_iota I, is_iota J>
175175
constexpr auto
176-
optimize(Expr<std::minus<>, std::tuple<I, J>> && e)
176+
optimize(Map<std::minus<>, std::tuple<I, J>> && e)
177177
{ return ra::iota(maybe_len(e), ITEM(0).i-ITEM(1).i, ITEM(0).s-ITEM(1).s); }
178178

179179
template <is_iota I, iota_op J>
180180
constexpr auto
181-
optimize(Expr<std::multiplies<>, std::tuple<I, J>> && e)
181+
optimize(Map<std::multiplies<>, std::tuple<I, J>> && e)
182182
{ return ra::iota(ITEM(0).n, ITEM(0).i*ITEM(1), ITEM(0).s*ITEM(1)); }
183183

184184
template <iota_op I, is_iota J>
185185
constexpr auto
186-
optimize(Expr<std::multiplies<>, std::tuple<I, J>> && e)
186+
optimize(Map<std::multiplies<>, std::tuple<I, J>> && e)
187187
{ return ra::iota(ITEM(1).n, ITEM(0)*ITEM(1).i, ITEM(0)*ITEM(1).s); }
188188

189189
template <is_iota I>
190190
constexpr auto
191-
optimize(Expr<std::negate<>, std::tuple<I>> && e)
191+
optimize(Map<std::negate<>, std::tuple<I>> && e)
192192
{ return ra::iota(ITEM(0).n, -ITEM(0).i, -ITEM(0).s); }
193193

194194
#if RA_DO_OPT_SMALLVECTOR==1
@@ -202,7 +202,7 @@ static_assert(match_small<double, 4, ra::Cell<double, ic_t<std::array { Dim { 4,
202202
#define RA_OPT_SMALLVECTOR_OP(OP, NAME, T, N) \
203203
template <class A, class B> requires (match_small<T, N, A> && match_small<T, N, B>) \
204204
constexpr auto \
205-
optimize(ra::Expr<NAME, std::tuple<A, B>> && e) \
205+
optimize(Map<NAME, std::tuple<A, B>> && e) \
206206
{ \
207207
alignas (alignof(extvector<T, N>)) ra::Small<T, N> val; \
208208
*(extvector<T, N> *)(&val) = *(extvector<T, N> *)((ITEM(0).c.cp)) OP *(extvector<T, N> *)((ITEM(1).c.cp)); \
@@ -274,7 +274,7 @@ DEF_NAMED_UNARY_OP(!, std::logical_not<>)
274274
template <class ... A> requires (toreduce<A ...>) constexpr decltype(auto) \
275275
OP(A && ... a) { return OP(VALUE(RA_FWD(a)) ...); }
276276
#define DEF_FWD(QUALIFIED_OP, OP) \
277-
template <class ... A> requires (!tomap<A ...> && !toreduce<A ...>) constexpr decltype(auto) \
277+
template <class ... A> /* requires neither */ constexpr decltype(auto) \
278278
OP(A && ... a) { return QUALIFIED_OP(RA_FWD(a) ...); } \
279279
DEF_NAME(OP)
280280
#define DEF_USING(QUALIFIED_OP, OP) \
@@ -311,16 +311,15 @@ template <class T, class ... A>
311311
constexpr auto
312312
pack(A && ... a)
313313
{
314-
return map([](auto && ... a) { return T { a ... }; }, RA_FWD(a) ...);
314+
return map([](auto && ... a) { return T { RA_FWD(a) ... }; }, RA_FWD(a) ...);
315315
}
316316

317317
// FIXME needs nested array for I, but iter<-1> should work
318318
template <class A, class I>
319319
constexpr auto
320320
at(A && a, I && i)
321321
{
322-
return map([a = std::tuple<A>(RA_FWD(a))] (auto && i) -> decltype(auto) { return std::get<0>(a).at(i); },
323-
RA_FWD(i));
322+
return map([a = std::tuple<A>{RA_FWD(a)}] (auto && i) -> decltype(auto) { return get<0>(a).at(i); }, RA_FWD(i));
324323
}
325324

326325

@@ -425,7 +424,7 @@ amax(auto && a)
425424
}
426425

427426
// FIXME encapsulate this kind of reference-reduction.
428-
// FIXME expr/ply mechanism doesn't allow partial iteration (adv then continue).
427+
// FIXME ply mechanism doesn't allow partial iteration (adv then continue).
429428
template <class A, class Less = std::less<ncvalue_t<A>>>
430429
constexpr decltype(auto)
431430
refmin(A && a, Less && less = {})

0 commit comments

Comments
 (0)