Skip to content

Commit ca887aa

Browse files
DinoVmeta-codesync[bot]
authored andcommitted
BinaryOpCache: multiply (*) inline-cache support
Summary: Add multiply (`*`) support to BinaryOpCache on top of the add-only base. The BinaryOpCached HIR node, the switch-dispatch machinery, and codegen are op-agnostic and unchanged here; this commit only adds the multiply operation. Included: - FOREACH_MULTIPLY_SPECIALIZATION and its folding into FOREACH_BINARY_OP_SPECIALIZATION, generating the kMul* / kUninitializedMultiply / kMultiplyGeneric Specialization enum values. - Multiply op helpers: longMul, compactLongMul, floatMul, complexMul, and the sequence*long repeats (listMul, strMul, tupleMul via sequenceRepeat); multiplyGeneric and populateAndInvokeMultiply. - selectInitialSpecialization / invoke / specializedTypes multiply arms. - simplify rule extended to also rewrite a generic BinaryOp<Multiply> into BinaryOpCached<Multiply>. - Multiply tests (inline_cache_test.cpp multiply specialization/compute/deopt cases, hir_simplify_test.cpp GenericMultiplyBecomesBinaryOpCached). Reviewed By: yoney Differential Revision: D109363776 fbshipit-source-id: 71f24c4f8620db15aa2ed6285c2bdc98b380fc0e
1 parent 594b892 commit ca887aa

7 files changed

Lines changed: 353 additions & 46 deletions

File tree

cinderx/Jit/hir/builder.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2027,6 +2027,7 @@ void HIRBuilder::emitBinaryOp(
20272027
if (getConfig().specialized_opcodes) {
20282028
switch (bc_instr.specializedOpcode()) {
20292029
case BINARY_OP_ADD_INT:
2030+
case BINARY_OP_MULTIPLY_INT:
20302031
// If we have inline caches for binary ops enabled then we don't want
20312032
// to specialize on the last seen interpreter type. The binary cache
20322033
// ops perform no backoff so once a cache is installed it persists.
@@ -2037,7 +2038,6 @@ void HIRBuilder::emitBinaryOp(
20372038
tc.emit<GuardType>(right, TLongExact, right, tc.frame);
20382039
}
20392040
break;
2040-
case BINARY_OP_MULTIPLY_INT:
20412041
case BINARY_OP_SUBTRACT_INT:
20422042
tc.emit<GuardType>(left, TLongExact, left, tc.frame);
20432043
tc.emit<GuardType>(right, TLongExact, right, tc.frame);

cinderx/Jit/hir/simplify.cpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,10 +1098,11 @@ Register* simplifyBinaryOp(Env& env, const BinaryOp* instr) {
10981098
return env.emit<UnicodeConcat>(lhs, rhs, *instr->frameState());
10991099
}
11001100

1101-
// Generic add where the operand types aren't statically known: emit an
1102-
// inline-cached variant that specializes on the operand types seen at
1103-
// runtime (e.g. a fast path for int + int).
1104-
if (getConfig().binary_op_caches && op == BinaryOpKind::kAdd) {
1101+
// Generic add/multiply where the operand types aren't statically known: emit
1102+
// an inline-cached variant that specializes on the operand types seen at
1103+
// runtime (e.g. a fast path for int + int or int * int).
1104+
if (getConfig().binary_op_caches &&
1105+
(op == BinaryOpKind::kAdd || op == BinaryOpKind::kMultiply)) {
11051106
return env.emit<BinaryOpCached>(op, lhs, rhs, *instr->frameState());
11061107
}
11071108

cinderx/Jit/inline_cache.cpp

Lines changed: 151 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2103,7 +2103,7 @@ LoadModuleMethodCache::lookupSlowPath(BorrowedRef<> obj, BorrowedRef<> name) {
21032103
return {nullptr, nullptr};
21042104
}
21052105

2106-
// Single source of truth for BinaryOpCache's add specializations, in priority
2106+
// Single source of truth for BinaryOpCache's specializations, in priority
21072107
// order. Each row is X(Name, Lhs, Rhs, Ret, Op, Fallback):
21082108
// - Name unique identifier; becomes Specialization::k<Name>.
21092109
// - Lhs, Rhs, Ret operand and result types (SpecializedType values); their
@@ -2114,9 +2114,15 @@ LoadModuleMethodCache::lookupSlowPath(BorrowedRef<> obj, BorrowedRef<> name) {
21142114
// compact/compact/long when a result overflows the compact
21152115
// range, which in turn steps down to long/long/long when the
21162116
// operands stop being compact.
2117-
// - Op fast-path operation (e.g. longAdd, defined below).
2118-
// - Fallback the Specialization to step down to once this row stops
2119-
// matching.
2117+
// - Op fast-path operation (e.g. longAdd, defined in
2118+
// inline_cache.cpp).
2119+
// - Fallback the per-op Specialization to step down to once this row
2120+
// stops matching.
2121+
//
2122+
// These macros are defined here (rather than in inline_cache.cpp) so the
2123+
// Specialization enum below can be generated from the same lists that drive the
2124+
// dispatch switches. The Op/Fallback columns are only expanded inside
2125+
// inline_cache.cpp, so naming file-local op helpers here is fine.
21202126
#define FOREACH_ADD_SPECIALIZATION(X) \
21212127
X(AddCompactCompactCompact, \
21222128
CompactLong, \
@@ -2137,12 +2143,45 @@ LoadModuleMethodCache::lookupSlowPath(BorrowedRef<> obj, BorrowedRef<> name) {
21372143
X(AddTuple, Tuple, Tuple, Tuple, tupleAdd, kAddGeneric) \
21382144
X(AddComplex, Complex, Complex, Complex, complexAdd, kAddGeneric)
21392145

2146+
// Specializations for the multiply op. Note the (sequence, long) rows have
2147+
// distinct lhs/rhs/result types: list/str/tuple repeated by an integer count.
2148+
#define FOREACH_MULTIPLY_SPECIALIZATION(X) \
2149+
X(MulCompactCompactCompact, \
2150+
CompactLong, \
2151+
CompactLong, \
2152+
CompactLong, \
2153+
compactLongMul, \
2154+
kMulCompactCompactLong) \
2155+
X(MulCompactCompactLong, \
2156+
CompactLong, \
2157+
CompactLong, \
2158+
Long, \
2159+
compactLongMul, \
2160+
kMulLongLongLong) \
2161+
X(MulLongLongLong, Long, Long, Long, longMul, kMultiplyGeneric) \
2162+
X(MulFloat, Float, Float, Float, floatMul, kMultiplyGeneric) \
2163+
X(MulList, List, Long, List, listMul, kMultiplyGeneric) \
2164+
X(MulUnicode, Unicode, Long, Unicode, strMul, kMultiplyGeneric) \
2165+
X(MulTuple, Tuple, Long, Tuple, tupleMul, kMultiplyGeneric) \
2166+
X(MulComplex, Complex, Long, Complex, complexMul, kMultiplyGeneric)
2167+
2168+
// The full specialization list (add followed by multiply), used to generate the
2169+
// single Specialization enum and the specializedTypes() switch that covers all
2170+
// values.
2171+
#define FOREACH_BINARY_OP_SPECIALIZATION(X) \
2172+
FOREACH_ADD_SPECIALIZATION(X) \
2173+
FOREACH_MULTIPLY_SPECIALIZATION(X)
2174+
21402175
enum class BinaryOpCache::Specialization : uint8_t {
2141-
#define DECLARE_ADD_SPECIALIZATION(NAME, LHS, RHS, RET, OP, FALLBACK) k##NAME,
2176+
#define DECLARE_BINARY_OP_SPECIALIZATION(NAME, LHS, RHS, RET, OP, FALLBACK) \
2177+
k##NAME,
21422178
kUninitializedAdd,
21432179
kAddGeneric,
2144-
FOREACH_ADD_SPECIALIZATION(DECLARE_ADD_SPECIALIZATION)
2145-
#undef DECLARE_ADD_SPECIALIZATION
2180+
FOREACH_ADD_SPECIALIZATION(DECLARE_BINARY_OP_SPECIALIZATION)
2181+
kUninitializedMultiply,
2182+
kMultiplyGeneric,
2183+
FOREACH_MULTIPLY_SPECIALIZATION(DECLARE_BINARY_OP_SPECIALIZATION)
2184+
#undef DECLARE_BINARY_OP_SPECIALIZATION
21462185
};
21472186

21482187
BinaryOpCache::BinaryOpCache(cinderx::jit::hir::BinaryOpKind op)
@@ -2153,6 +2192,8 @@ BinaryOpCache::Specialization BinaryOpCache::selectInitialSpecialization(
21532192
switch (op) {
21542193
case cinderx::jit::hir::BinaryOpKind::kAdd:
21552194
return Specialization::kUninitializedAdd;
2195+
case cinderx::jit::hir::BinaryOpKind::kMultiply:
2196+
return Specialization::kUninitializedMultiply;
21562197
default:
21572198
throw std::runtime_error(
21582199
fmt::format(
@@ -2291,6 +2332,8 @@ PyObject* BinaryOpCache::invokeSpecialized(
22912332
}
22922333
#define POPULATE_ADD_SPECIALIZATION(...) \
22932334
POPULATE_BINARY_SPECIALIZATION(add, __VA_ARGS__)
2335+
#define POPULATE_MULTIPLY_SPECIALIZATION(...) \
2336+
POPULATE_BINARY_SPECIALIZATION(multiply, __VA_ARGS__)
22942337

22952338
// Emits one dispatch-switch arm that runs the specialization directly via
22962339
// invokeSpecialized<>, threading the Fallback value and the matching
@@ -2307,6 +2350,8 @@ PyObject* BinaryOpCache::invokeSpecialized(
23072350
&BinaryOpCache::DISPATCH>(lhs, rhs, cache);
23082351
#define DISPATCH_ADD_SPECIALIZATION(...) \
23092352
DISPATCH_BINARY_SPECIALIZATION(add, __VA_ARGS__)
2353+
#define DISPATCH_MULTIPLY_SPECIALIZATION(...) \
2354+
DISPATCH_BINARY_SPECIALIZATION(multiply, __VA_ARGS__)
23102355

23112356
// Emits one specializedTypes() switch arm mapping a specialization to its
23122357
// (lhs, rhs, return) operand types. A single enum lets one switch cover both
@@ -2353,13 +2398,78 @@ static inline PyObject* tupleAdd(PyObject* lhs, PyObject* rhs) {
23532398
return PyTuple_Type.tp_as_sequence->sq_concat(lhs, rhs);
23542399
}
23552400

2401+
static inline PyObject* longMul(PyObject* lhs, PyObject* rhs) {
2402+
#if PY_VERSION_HEX >= 0x030F0000
2403+
// _PyLong_Multiply was removed in 3.15. Both operands are exact ints here,
2404+
// so the public number slot is equivalent and returns a new reference.
2405+
return PyLong_Type.tp_as_number->nb_multiply(lhs, rhs);
2406+
#else
2407+
return _PyLong_Multiply(
2408+
reinterpret_cast<PyLongObject*>(lhs),
2409+
reinterpret_cast<PyLongObject*>(rhs));
2410+
#endif
2411+
}
2412+
2413+
// Fast path for two compact ints: multiply their machine-word values
2414+
// directly. Both operands are single-digit (guaranteed by _PyLong_IsCompact),
2415+
// so the product cannot overflow Py_ssize_t. The result may itself be
2416+
// non-compact; the compact/compact/compact specialization detects that via
2417+
// its return-type check and steps down to compact/compact/long.
2418+
static inline PyObject* compactLongMul(PyObject* lhs, PyObject* rhs) {
2419+
Py_ssize_t a = _PyLong_CompactValue(reinterpret_cast<PyLongObject*>(lhs));
2420+
Py_ssize_t b = _PyLong_CompactValue(reinterpret_cast<PyLongObject*>(rhs));
2421+
return PyLong_FromSsize_t(a * b);
2422+
}
2423+
2424+
static inline PyObject* floatMul(PyObject* lhs, PyObject* rhs) {
2425+
double a = reinterpret_cast<PyFloatObject*>(lhs)->ob_fval;
2426+
double b = reinterpret_cast<PyFloatObject*>(rhs)->ob_fval;
2427+
return PyFloat_FromDouble(a * b);
2428+
}
2429+
2430+
static inline PyObject* complexMul(PyObject* lhs, PyObject* rhs) {
2431+
// complex * long: the complex nb_multiply slot coerces the integer operand.
2432+
return PyComplex_Type.tp_as_number->nb_multiply(lhs, rhs);
2433+
}
2434+
2435+
// Sequence-repeat helpers for (sequence, long) multiplication. The repeat
2436+
// count is the integer rhs; an out-of-range count surfaces as an error from
2437+
// PyLong_AsSsize_t, matching the generic path.
2438+
static inline PyObject*
2439+
sequenceRepeat(PySequenceMethods* methods, PyObject* seq, PyObject* count) {
2440+
Py_ssize_t n = PyLong_AsSsize_t(count);
2441+
if (n == -1 && PyErr_Occurred()) {
2442+
return nullptr;
2443+
}
2444+
return methods->sq_repeat(seq, n);
2445+
}
2446+
2447+
static inline PyObject* listMul(PyObject* lhs, PyObject* rhs) {
2448+
return sequenceRepeat(PyList_Type.tp_as_sequence, lhs, rhs);
2449+
}
2450+
2451+
static inline PyObject* strMul(PyObject* lhs, PyObject* rhs) {
2452+
return sequenceRepeat(PyUnicode_Type.tp_as_sequence, lhs, rhs);
2453+
}
2454+
2455+
static inline PyObject* tupleMul(PyObject* lhs, PyObject* rhs) {
2456+
return sequenceRepeat(PyTuple_Type.tp_as_sequence, lhs, rhs);
2457+
}
2458+
23562459
PyObject* BinaryOpCache::addGeneric(
23572460
PyObject* lhs,
23582461
PyObject* rhs,
23592462
BinaryOpCache* /* cache */) {
23602463
return PyNumber_Add(lhs, rhs);
23612464
}
23622465

2466+
PyObject* BinaryOpCache::multiplyGeneric(
2467+
PyObject* lhs,
2468+
PyObject* rhs,
2469+
BinaryOpCache* /* cache */) {
2470+
return PyNumber_Multiply(lhs, rhs);
2471+
}
2472+
23632473
PyObject* BinaryOpCache::populateAndInvokeAdd(
23642474
PyObject* lhs,
23652475
PyObject* rhs,
@@ -2370,10 +2480,20 @@ PyObject* BinaryOpCache::populateAndInvokeAdd(
23702480
return addGeneric(lhs, rhs, cache);
23712481
}
23722482

2373-
// Dispatch on the cache's current specialization and run the corresponding add
2374-
// operation directly. The specialized arms are generated from
2375-
// FOREACH_ADD_SPECIALIZATION so this stays in sync with the Specialization
2376-
// enum. There is no indirect call through a function pointer.
2483+
PyObject* BinaryOpCache::populateAndInvokeMultiply(
2484+
PyObject* lhs,
2485+
PyObject* rhs,
2486+
BinaryOpCache* cache) {
2487+
FOREACH_MULTIPLY_SPECIALIZATION(POPULATE_MULTIPLY_SPECIALIZATION)
2488+
2489+
cache->specialization_ = Specialization::kMultiplyGeneric;
2490+
return multiplyGeneric(lhs, rhs, cache);
2491+
}
2492+
2493+
// Dispatch on the cache's current specialization and run the corresponding
2494+
// add operation directly. The arms cover only the add subset of the single
2495+
// Specialization enum (generated from FOREACH_ADD_SPECIALIZATION); multiply
2496+
// states never reach here because codegen calls add() only for kAdd caches.
23772497
PyObject*
23782498
BinaryOpCache::add(PyObject* lhs, PyObject* rhs, BinaryOpCache* cache) {
23792499
switch (cache->specialization_) {
@@ -2382,23 +2502,41 @@ BinaryOpCache::add(PyObject* lhs, PyObject* rhs, BinaryOpCache* cache) {
23822502
case Specialization::kAddGeneric:
23832503
return addGeneric(lhs, rhs, cache);
23842504
FOREACH_ADD_SPECIALIZATION(DISPATCH_ADD_SPECIALIZATION)
2505+
default:
2506+
JIT_ABORT("Unexpected specialization in BinaryOpCache::add");
2507+
}
2508+
}
2509+
2510+
// Dispatch on the cache's current specialization. Mirrors add() but over the
2511+
// multiply subset of the enum (FOREACH_MULTIPLY_SPECIALIZATION).
2512+
PyObject*
2513+
BinaryOpCache::multiply(PyObject* lhs, PyObject* rhs, BinaryOpCache* cache) {
2514+
switch (cache->specialization_) {
2515+
case Specialization::kUninitializedMultiply:
2516+
return populateAndInvokeMultiply(lhs, rhs, cache);
2517+
case Specialization::kMultiplyGeneric:
2518+
return multiplyGeneric(lhs, rhs, cache);
2519+
FOREACH_MULTIPLY_SPECIALIZATION(DISPATCH_MULTIPLY_SPECIALIZATION)
2520+
default:
2521+
JIT_ABORT("Unexpected specialization in BinaryOpCache::multiply");
23852522
}
2386-
JIT_ABORT("Unknown BinaryOpCache specialization");
23872523
}
23882524

23892525
BinaryOpCache::BinarySpecialization BinaryOpCache::specializedTypes() const {
23902526
switch (specialization_) {
23912527
case Specialization::kUninitializedAdd:
2528+
case Specialization::kUninitializedMultiply:
23922529
return BinarySpecialization{
23932530
SpecializedType::kUninitialized,
23942531
SpecializedType::kUninitialized,
23952532
SpecializedType::kUninitialized};
23962533
case Specialization::kAddGeneric:
2534+
case Specialization::kMultiplyGeneric:
23972535
return BinarySpecialization{
23982536
SpecializedType::kGeneric,
23992537
SpecializedType::kGeneric,
24002538
SpecializedType::kGeneric};
2401-
FOREACH_ADD_SPECIALIZATION(SPECIALIZATION_TYPES_ENTRY)
2539+
FOREACH_BINARY_OP_SPECIALIZATION(SPECIALIZATION_TYPES_ENTRY)
24022540
}
24032541
JIT_ABORT("Unknown BinaryOpCache specialization");
24042542
}

cinderx/Jit/inline_cache.h

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -486,21 +486,29 @@ enum class SpecializedType : uint8_t {
486486
// A cache for an individual BinaryOpCached instruction.
487487
//
488488
// Implements an inline cache for binary operations as a small state machine.
489-
// A cache starts in the populate state, which checks the inputs for known cache
490-
// types on the first invocation, then transitions specialization_ to the
491-
// matching specialized state, or to a generic state when no SpecializedType
492-
// applies.
489+
// A single Specialization enum covers both add and multiply states, but add and
490+
// multiply have separate dispatch entry points (add() / multiply()) that each
491+
// switch over their op's subset of the enum. A cache is constructed for a
492+
// single op; it starts in that op's populate state, which checks the inputs for
493+
// known cache types on the first invocation, then transitions specialization_
494+
// to the matching specialized state, or to a generic state when no
495+
// SpecializedType applies.
493496
//
494-
// Codegen emits a direct call to add(), which switches on specialization_ and
495-
// calls the matching specialized operation directly -- there is no indirect
496-
// call through a function pointer.
497+
// Codegen emits a direct call to add() (for kAdd) or multiply() (for
498+
// kMultiply); each switches on specialization_ and calls the matching
499+
// specialized operation directly -- there is no indirect call through a
500+
// function pointer.
497501
class BinaryOpCache {
498502
public:
499503
// Identifies which specialization the cache has settled on, i.e. which
500-
// operation add() dispatches to. The k<Name> values are auto-generated from
501-
// FOREACH_ADD_SPECIALIZATION (in inline_cache.cpp); kUninitializedAdd is the
502-
// initial (lazily specializing) populate state and kAddGeneric is the
503-
// permanent generic fallback. Defined out-of-line in inline_cache.cpp.
504+
// operation add()/multiply() dispatches to. A single enum holds both ops'
505+
// states: the k<Name> values are auto-generated from
506+
// FOREACH_BINARY_OP_SPECIALIZATION, the kUninitialized* values are the
507+
// initial (lazily specializing) populate states, and
508+
// kAddGeneric/kMultiplyGeneric are the permanent generic fallbacks. add()
509+
// only ever observes the add subset and multiply() the multiply subset, but a
510+
// single enum lets specializedTypes() switch over all values without a
511+
// discriminant.
504512
enum class Specialization : uint8_t;
505513

506514
// The (lhs, rhs, return) operand/result types a cache has specialized to.
@@ -520,9 +528,11 @@ class BinaryOpCache {
520528
// op has no inline-cache support.
521529
explicit BinaryOpCache(cinderx::jit::hir::BinaryOpKind op);
522530

523-
// Dispatch entry point called directly by codegen for kAdd. Switches on the
524-
// cache's specialization enum and runs the corresponding operation directly.
531+
// Dispatch entry points called directly by codegen: add() for kAdd,
532+
// multiply() for kMultiply. Each switches on the cache's per-op
533+
// specialization enum and runs the corresponding operation directly.
525534
static PyObject* add(PyObject* lhs, PyObject* rhs, BinaryOpCache* cache);
535+
static PyObject* multiply(PyObject* lhs, PyObject* rhs, BinaryOpCache* cache);
526536

527537
// Returns the (lhs, rhs, return) operand types the cache has settled on
528538
// ({kUninitialized, ...} before the first call).
@@ -541,10 +551,19 @@ class BinaryOpCache {
541551
static PyObject*
542552
populateAndInvokeAdd(PyObject* lhs, PyObject* rhs, BinaryOpCache* cache);
543553

554+
// Initial entry point for the multiply op: inspects the operand types,
555+
// transitions the multiply specialization, and performs the operation.
556+
static PyObject*
557+
populateAndInvokeMultiply(PyObject* lhs, PyObject* rhs, BinaryOpCache* cache);
558+
544559
// Permanent generic fallback that just calls PyNumber_Add.
545560
static PyObject*
546561
addGeneric(PyObject* lhs, PyObject* rhs, BinaryOpCache* cache);
547562

563+
// Permanent generic fallback that just calls PyNumber_Multiply.
564+
static PyObject*
565+
multiplyGeneric(PyObject* lhs, PyObject* rhs, BinaryOpCache* cache);
566+
548567
// Specialized entry for a (lhs, rhs) -> ret triple. Guards that lhs passes
549568
// checkFor(LhsKind) and rhs passes checkFor(RhsKind) and, if so, runs the
550569
// fast-path Op. When the return type is refined (returnNeedsCheck), it also

cinderx/Jit/lir/generator.cpp

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3351,16 +3351,28 @@ LIRGenerator::TranslatedBlock LIRGenerator::translateOneBasicBlock(
33513351
case Opcode::kBinaryOpCached: {
33523352
auto instr = static_cast<const BinaryOpCached*>(&i);
33533353
BinaryOpCache* cache = getContext()->allocateBinaryOpCache(instr->op());
3354-
// Emit a direct call to the dispatch entry point add(), which switches
3355-
// on the cache's specialization enum -- there is no indirect call
3356-
// through a function pointer. allocateBinaryOpCache() already rejected
3357-
// any unsupported op kind.
3358-
bbb.appendCallInstruction(
3359-
instr->output(),
3360-
BinaryOpCache::add,
3361-
instr->left(),
3362-
instr->right(),
3363-
cache);
3354+
// Emit a direct call to the op-specific dispatch entry point: add() for
3355+
// kAdd, multiply() for kMultiply. Each switches on the cache's per-op
3356+
// specialization enum -- there is no indirect call through a function
3357+
// pointer. allocateBinaryOpCache() already rejected any other op kind.
3358+
if (instr->op() == BinaryOpKind::kMultiply) {
3359+
bbb.appendCallInstruction(
3360+
instr->output(),
3361+
BinaryOpCache::multiply,
3362+
instr->left(),
3363+
instr->right(),
3364+
cache);
3365+
} else {
3366+
JIT_DCHECK(
3367+
instr->op() == BinaryOpKind::kAdd,
3368+
"BinaryOpCached only supports add and multiply");
3369+
bbb.appendCallInstruction(
3370+
instr->output(),
3371+
BinaryOpCache::add,
3372+
instr->left(),
3373+
instr->right(),
3374+
cache);
3375+
}
33643376
break;
33653377
}
33663378
case Opcode::kLongBinaryOp: {

0 commit comments

Comments
 (0)