SERVER-102171 Extend NaryOp to include Operations::Mult (#34583)

GitOrigin-RevId: da5bbacda11953f1a4a9cc7963cd1acb900930fb
This commit is contained in:
Projjal Chanda 2025-04-07 13:35:26 +01:00 committed by MongoDB Bot
parent 3fa256f424
commit 7d3e912236
16 changed files with 480 additions and 1170 deletions

View File

@ -294,6 +294,9 @@ vm::CodeFragment EPrimNary::compileDirect(CompileCtx& ctx) const {
case EPrimNary::add:
code.appendAdd(lhsParam, rhsParam);
break;
case EPrimNary::mul:
code.appendMul(lhsParam, rhsParam);
break;
default:
MONGO_UNREACHABLE;
}
@ -325,6 +328,9 @@ std::vector<DebugPrinter::Block> EPrimNary::debugPrint() const {
case EPrimNary::add:
ret.emplace_back("+");
break;
case EPrimNary::mul:
ret.emplace_back("*");
break;
default:
MONGO_UNREACHABLE;
}

View File

@ -337,9 +337,11 @@ public:
// Math operations.
add,
mul,
};
EPrimNary(Op op, std::vector<std::unique_ptr<EExpression>> args) : _op(op) {
tassert(10217100, "Expected at least two operands", args.size() >= 2);
_nodes.reserve(args.size());
for (auto&& arg : args) {
_nodes.emplace_back(std::move(arg));
@ -376,7 +378,7 @@ public:
// Math operations.
add, // TODO: remove with SERVER-100579
sub,
mul,
mul, // TODO: remove with SERVER-100579
div,
// Comparison operations. These operations support taking a third "collator" arg.

View File

@ -279,4 +279,56 @@ TEST_F(SBEPrimNaryTest, NaryAdd) {
}
}
TEST_F(SBEPrimNaryTest, NaryMult) {
auto& os = gctx->outStream();
std::vector<std::unique_ptr<value::ViewOfValueAccessor>> accessors;
value::SlotVector slotIds;
int depth = 3;
int numSlots = 1 << depth;
for (int i = 0; i < numSlots; i++) {
accessors.emplace_back(std::make_unique<value::ViewOfValueAccessor>());
accessors.back()->reset(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(i + 1));
slotIds.push_back(bindAccessor(accessors.back().get()));
}
auto expr = makeNaryExpr(EPrimNary::Op::mul, slotIds);
printInputExpression(os, *expr);
auto compiledExpr = compileExpression(*expr);
printCompiledExpression(os, *compiledExpr);
{
auto [tag, val] = runCompiledExpression(compiledExpr.get());
value::ValueGuard guard(tag, val);
TypedValue expected = makeInt64(40320);
ASSERT_THAT(std::make_pair(tag, val), ValueEq(expected));
}
for (int idx = 0; idx < numSlots; ++idx) {
accessors[idx]->reset(value::TypeTags::Nothing, 0);
auto [tag, val] = runCompiledExpression(compiledExpr.get());
value::ValueGuard guard(tag, val);
TypedValue expected = makeNothing();
ASSERT_THAT(std::make_pair(tag, val), ValueEq(expected));
accessors[idx]->reset(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(idx + 1));
}
for (int idx = 0; idx < numSlots; ++idx) {
accessors[idx]->reset(value::TypeTags::NumberDouble,
value::bitcastFrom<double>(static_cast<double>(idx + 1)));
auto [tag, val] = runCompiledExpression(compiledExpr.get());
value::ValueGuard guard(tag, val);
TypedValue expected = makeDouble(40320.0);
ASSERT_THAT(std::make_pair(tag, val), ValueEq(expected));
accessors[idx]->reset(value::TypeTags::NumberInt64, value::bitcastFrom<int64_t>(idx + 1));
}
}
} // namespace mongo::sbe

View File

@ -248,8 +248,9 @@ public:
NaryOp(Operations inOp, ABTVector exprs) : Base(std::move(exprs)), _op(inOp) {
tassert(10199600,
"operation doesn't allow multiple operands",
_op == Operations::And || _op == Operations::Or || _op == Operations::Add);
tassert(10199601, "operation needs at least two operands", nodes().size() >= 2);
_op == Operations::And || _op == Operations::Or || _op == Operations::Add ||
_op == Operations::Mult);
tassert(10199601, "operation needs at least one operand", nodes().size() >= 1);
for (auto&& expr : nodes()) {
assertExprSort(expr);
}

View File

@ -265,6 +265,9 @@ std::unique_ptr<sbe::EExpression> SBEExpressionLowering::transport(
case Operations::Add:
sbeOp = sbe::EPrimNary::add;
break;
case Operations::Mult:
sbeOp = sbe::EPrimNary::mul;
break;
default:
MONGO_UNREACHABLE;
}

View File

@ -463,29 +463,44 @@ void ExpressionConstEval::transport(optimizer::ABT& n,
}
break;
}
case optimizer::Operations::Add: {
case optimizer::Operations::Add:
case optimizer::Operations::Mult: {
auto it = args.begin();
if (!it->cast<optimizer::Constant>()) {
return;
}
it++;
for (; it < args.end(); it++) {
optimizer::ABT& rhs = *it;
auto rhsConst = rhs.cast<optimizer::Constant>();
if (!rhsConst) {
break;
if (it->cast<optimizer::Constant>()) {
it++;
for (; it < args.end(); it++) {
optimizer::ABT& rhs = *it;
auto rhsConst = rhs.cast<optimizer::Constant>();
if (!rhsConst) {
break;
}
optimizer::ABT& lhs = *(it - 1);
auto lhsConst = lhs.cast<optimizer::Constant>();
auto [lhsTag, lhsValue] = lhsConst->get();
auto [rhsTag, rhsValue] = rhsConst->get();
auto performOp = [&](sbe::value::TypeTags lhsTag,
sbe::value::Value lhsValue,
sbe::value::TypeTags rhsTag,
sbe::value::Value rhsValue) {
switch (op.op()) {
case optimizer::Operations::Add:
return sbe::value::genericAdd(lhsTag, lhsValue, rhsTag, rhsValue);
case optimizer::Operations::Mult:
return sbe::value::genericMul(lhsTag, lhsValue, rhsTag, rhsValue);
default:
MONGO_UNREACHABLE;
}
};
auto [_, resultType, resultValue] =
performOp(lhsTag, lhsValue, rhsTag, rhsValue);
swapAndUpdate(rhs,
optimizer::make<optimizer::Constant>(resultType, resultValue));
}
optimizer::ABT& lhs = *(it - 1);
auto lhsConst = lhs.cast<optimizer::Constant>();
auto [lhsTag, lhsValue] = lhsConst->get();
auto [rhsTag, rhsValue] = rhsConst->get();
auto [_, resultType, resultValue] =
sbe::value::genericAdd(lhsTag, lhsValue, rhsTag, rhsValue);
swapAndUpdate(rhs, optimizer::make<optimizer::Constant>(resultType, resultValue));
args.erase(args.begin(), it - 1);
}
args.erase(args.begin(), it - 1);
invariant(args.size() > 0);
if (args.size() == 1) {
swapAndUpdate(n, std::exchange(args[0], optimizer::make<optimizer::Blackhole>()));

View File

@ -118,9 +118,6 @@ optimizer::ABT makeBinaryOp(optimizer::Operations binaryOp,
optimizer::ABT makeNaryOp(optimizer::Operations op, optimizer::ABTVector args) {
tassert(10199700, "Expected at least one argument", !args.empty());
if (feature_flags::gFeatureFlagSbeUpgradeBinaryTrees.isEnabled()) {
if (args.size() == 1) {
return std::move(args[0]);
}
return optimizer::make<optimizer::NaryOp>(op, std::move(args));
} else {
return std::accumulate(

View File

@ -2361,7 +2361,8 @@ public:
auto arity = expr->getChildren().size();
_context->ensureArity(arity);
if (arity < kArgumentCountForBinaryTree) {
if (arity < kArgumentCountForBinaryTree ||
feature_flags::gFeatureFlagSbeUpgradeBinaryTrees.isEnabled()) {
visitFast(expr);
return;
}
@ -2446,11 +2447,7 @@ public:
makeBooleanOpTree(optimizer::Operations::Or, std::move(checkExprsNull));
auto checkNumberAllArguments =
makeBooleanOpTree(optimizer::Operations::And, std::move(checkExprsNumber));
auto multiplication = std::accumulate(
names.begin() + 1, names.end(), makeVariable(names.front()), [](auto&& acc, auto&& ex) {
return optimizer::make<optimizer::BinaryOp>(
optimizer::Operations::Mult, std::move(acc), makeVariable(ex));
});
auto multiplication = makeNaryOp(optimizer::Operations::Mult, std::move(variables));
auto multiplyExpr = buildABTMultiBranchConditionalFromCaseValuePairs(
{ABTCaseValuePair{std::move(checkNullAnyArgument), optimizer::Constant::null()},

View File

@ -121,6 +121,11 @@ TEST(SbeStageBuilderConstEvalTest, ConstEval) {
tree = _nary("Add", "1"_cint64, "2"_cint64, "3"_cint64)._n;
result = constEval(tree);
ASSERT_EQ(result->getValueInt64(), 6);
// 2 * 3 * 4
tree = _nary("Mult", "2"_cint64, "3"_cint64, "4"_cint64)._n;
result = constEval(tree);
ASSERT_EQ(result->getValueInt64(), 24);
}
@ -149,6 +154,11 @@ TEST(SbeStageBuilderConstEvalTest, ConstEval3) {
tree = _nary("Add", "1.5"_cdouble, "0.5"_cdouble, "1.0"_cdouble)._n;
result = constEval(tree);
ASSERT_EQ(result->getValueDouble(), 3.0);
// 1.5 * 0.5 * 1.0
tree = _nary("Mult", "1.5"_cdouble, "0.5"_cdouble, "1.0"_cdouble)._n;
result = constEval(tree);
ASSERT_EQ(result->getValueDouble(), 0.75);
}
TEST(SbeStageBuilderConstEvalTest, ConstEval4) {
@ -477,6 +487,61 @@ TEST(ConstEvalTest, NaryAddMultFold) {
"| Variable [x]\n"
"Const [3]\n",
abt);
abt = _nary("Add", "x"_var)._n;
evaluator.optimize(abt);
ASSERT_EXPLAIN_V2_AUTO( // NOLINT
"Variable [x]\n",
abt);
abt = _nary("Mult", "1"_cint64, "x"_var, "y"_var, "z"_var)._n;
evaluator.optimize(abt);
ASSERT_EXPLAIN_V2_AUTO( // NOLINT
"NaryOp [Mult]\n"
"| | | Variable [z]\n"
"| | Variable [y]\n"
"| Variable [x]\n"
"Const [1]\n",
abt);
abt = _nary("Mult", "1"_cint64, "2"_cint64, "y"_var, "z"_var)._n;
evaluator.optimize(abt);
ASSERT_EXPLAIN_V2_AUTO( // NOLINT
"NaryOp [Mult]\n"
"| | Variable [z]\n"
"| Variable [y]\n"
"Const [2]\n",
abt);
abt = _nary("Mult", "1"_cint64, "2"_cint64, "3"_cint64, "z"_var)._n;
evaluator.optimize(abt);
ASSERT_EXPLAIN_V2_AUTO( // NOLINT
"NaryOp [Mult]\n"
"| Variable [z]\n"
"Const [6]\n",
abt);
abt = _nary("Mult", "1"_cint64, "2"_cint64, "3"_cint64, "4"_cint64)._n;
evaluator.optimize(abt);
ASSERT_EXPLAIN_V2_AUTO( // NOLINT
"Const [24]\n",
abt);
abt = _nary("Mult", "1"_cint64, "2"_cint64, "x"_var, "3"_cint64, "4"_cint64)._n;
evaluator.optimize(abt);
ASSERT_EXPLAIN_V2_AUTO( // NOLINT
"NaryOp [Mult]\n"
"| | | Const [4]\n"
"| | Const [3]\n"
"| Variable [x]\n"
"Const [2]\n",
abt);
abt = _nary("Mult", "2"_cint64)._n;
evaluator.optimize(abt);
ASSERT_EXPLAIN_V2_AUTO( // NOLINT
"Const [2]\n",
abt);
}
TEST(ConstEvalTest, ConstantEquality) {

View File

@ -445,5 +445,37 @@ TEST_F(AbtToSbeExpression, LowerNaryAdd) {
ASSERT_EQ(sbe::value::bitcastTo<int64_t>(resultVal), 125);
}
}
TEST_F(AbtToSbeExpression, LowerNaryMult) {
{
auto tree = make<NaryOp>(Operations::Mult,
ABTVector{Constant::int64(1),
Constant::int64(5),
Constant::int64(10),
Constant::int64(20),
Constant::int64(100)});
auto [resultTag, resultVal] = evalExpr(tree, boost::none);
ASSERT_EQ(sbe::value::TypeTags::NumberInt64, resultTag);
ASSERT_EQ(sbe::value::bitcastTo<int64_t>(resultVal), 100000);
}
{
auto tree = make<NaryOp>(
Operations::Mult,
ABTVector{make<BinaryOp>(Operations::Sub, Constant::int64(10), make<Variable>("var")),
make<BinaryOp>(Operations::Sub, Constant::int64(20), make<Variable>("var")),
make<BinaryOp>(Operations::Sub, Constant::int64(30), make<Variable>("var")),
make<BinaryOp>(Operations::Sub, Constant::int64(40), make<Variable>("var")),
make<BinaryOp>(Operations::Sub, Constant::int64(50), make<Variable>("var"))});
auto [resultTag, resultVal] =
evalExpr(tree, std::pair{ProjectionName{"var"}, sbe::value::makeIntOrLong(5)});
ASSERT_EQ(sbe::value::TypeTags::NumberInt64, resultTag);
ASSERT_EQ(sbe::value::bitcastTo<int64_t>(resultVal), 2953125);
}
}
} // namespace
} // namespace mongo::stage_builder::abt

View File

@ -526,5 +526,34 @@ TEST(TypeCheckerTest, TypeCheckNaryAdd) {
}
}
TEST(TypeCheckerTest, TypeCheckNaryMult) {
{
auto tree = make<NaryOp>(
Operations::Mult,
ABTVector{
Constant::int32(1), Constant::int32(2), Constant::int32(3), Constant::int32(4)});
auto signature = TypeChecker{}.typeCheck(tree);
ASSERT_EQ(signature.typesMask, TypeSignature::kNumericType.typesMask);
}
{
auto tree = make<NaryOp>(
Operations::Mult,
ABTVector{
Constant::int32(1), Constant::int32(2), Constant::int32(3), Constant::nothing()});
auto signature = TypeChecker{}.typeCheck(tree);
ASSERT_EQ(signature.typesMask,
(TypeSignature::kNothingType.include(TypeSignature::kNumericType)).typesMask);
}
{
auto tree = make<NaryOp>(
Operations::Mult,
ABTVector{
Constant::int32(1), Constant::int32(2), Constant::int32(3), make<Variable>("var")});
auto signature = TypeChecker{}.typeCheck(tree);
ASSERT_EQ(signature.typesMask,
(TypeSignature::kNumericType.include(TypeSignature::kNothingType)).typesMask);
}
}
} // namespace
} // namespace mongo::stage_builder

View File

@ -4335,6 +4335,166 @@ TEST(VectorizerTest, ConvertMult) {
ASSERT_TRUE(processed.expr.has_value());
assertArithmeticOperationScalarScalar(opStr, processed);
}
{
auto treeBlocks = make<NaryOp>(
op, ABTVector{make<Variable>("var1"), make<Variable>("var2"), make<Variable>("var3")});
Vectorizer::VariableTypes bindings;
bindings.emplace(
"var1"_sd,
std::make_pair(TypeSignature::kBlockType.include(TypeSignature::kAnyScalarType),
boost::none));
bindings.emplace(
"var2"_sd,
std::make_pair(TypeSignature::kBlockType.include(TypeSignature::kAnyScalarType),
boost::none));
bindings.emplace(
"var3"_sd,
std::make_pair(TypeSignature::kBlockType.include(TypeSignature::kAnyScalarType),
boost::none));
sbe::value::FrameIdGenerator generator;
auto processed = Vectorizer{&generator, Vectorizer::Purpose::Project}.vectorize(
treeBlocks, bindings, boost::none);
ASSERT_TRUE(processed.expr.has_value());
assertArithmeticOperationBlockBlockBlock(fnStr, processed);
}
{
auto treeBlockScalarScalar = make<NaryOp>(
op, ABTVector{make<Variable>("var"), Constant::int32(9), Constant::int32(20)});
Vectorizer::VariableTypes bindings;
bindings.emplace(
"var"_sd,
std::make_pair(TypeSignature::kBlockType.include(TypeSignature::kAnyScalarType),
boost::none));
sbe::value::FrameIdGenerator generator;
auto processed = Vectorizer{&generator, Vectorizer::Purpose::Project}.vectorize(
treeBlockScalarScalar, bindings, boost::none);
ASSERT_TRUE(processed.expr.has_value());
assertArithmeticOperationBlockScalarScalar(fnStr, opStr, processed);
}
{
auto treeScalarBlockScalar = make<NaryOp>(
op, ABTVector{Constant::int32(9), make<Variable>("var"), Constant::int32(20)});
Vectorizer::VariableTypes bindings;
bindings.emplace(
"var"_sd,
std::make_pair(TypeSignature::kBlockType.include(TypeSignature::kAnyScalarType),
boost::none));
sbe::value::FrameIdGenerator generator;
auto processed = Vectorizer{&generator, Vectorizer::Purpose::Project}.vectorize(
treeScalarBlockScalar, bindings, boost::none);
ASSERT_TRUE(processed.expr.has_value());
assertArithmeticOperationScalarBlockScalar(fnStr, processed);
}
{
auto treeScalarScalarBlock = make<NaryOp>(
op, ABTVector{Constant::int32(9), Constant::int32(20), make<Variable>("var")});
Vectorizer::VariableTypes bindings;
bindings.emplace(
"var"_sd,
std::make_pair(TypeSignature::kBlockType.include(TypeSignature::kAnyScalarType),
boost::none));
sbe::value::FrameIdGenerator generator;
auto processed = Vectorizer{&generator, Vectorizer::Purpose::Project}.vectorize(
treeScalarScalarBlock, bindings, boost::none);
ASSERT_TRUE(processed.expr.has_value());
assertArithmeticOperationScalarScalarBlock(fnStr, processed);
}
{
auto treeScalarScalarScalar = make<NaryOp>(
op, ABTVector{Constant::int32(9), Constant::int32(20), Constant::int32(100)});
Vectorizer::VariableTypes bindings;
sbe::value::FrameIdGenerator generator;
auto processed = Vectorizer{&generator, Vectorizer::Purpose::Project}.vectorize(
treeScalarScalarScalar, bindings, boost::none);
ASSERT_TRUE(processed.expr.has_value());
assertArithmeticOperationScalarScalarScalar(opStr, processed);
}
{
auto treeBlockBlockScalar = make<NaryOp>(
op, ABTVector{make<Variable>("var1"), make<Variable>("var2"), Constant::int32(9)});
Vectorizer::VariableTypes bindings;
bindings.emplace(
"var"_sd,
std::make_pair(TypeSignature::kBlockType.include(TypeSignature::kAnyScalarType),
boost::none));
bindings.emplace(
"var2"_sd,
std::make_pair(TypeSignature::kBlockType.include(TypeSignature::kAnyScalarType),
boost::none));
sbe::value::FrameIdGenerator generator;
auto processed = Vectorizer{&generator, Vectorizer::Purpose::Project}.vectorize(
treeBlockBlockScalar, bindings, boost::none);
ASSERT_TRUE(processed.expr.has_value());
assertArithmeticOperationBlockBlockScalar(fnStr, processed);
}
{
auto treeScalarBlockBlock = make<NaryOp>(
op, ABTVector{Constant::int32(9), make<Variable>("var1"), make<Variable>("var2")});
Vectorizer::VariableTypes bindings;
bindings.emplace(
"var"_sd,
std::make_pair(TypeSignature::kBlockType.include(TypeSignature::kAnyScalarType),
boost::none));
bindings.emplace(
"var2"_sd,
std::make_pair(TypeSignature::kBlockType.include(TypeSignature::kAnyScalarType),
boost::none));
sbe::value::FrameIdGenerator generator;
auto processed = Vectorizer{&generator, Vectorizer::Purpose::Project}.vectorize(
treeScalarBlockBlock, bindings, boost::none);
ASSERT_TRUE(processed.expr.has_value());
assertArithmeticOperationScalarBlockBlock(fnStr, processed);
}
{
auto treeBlockScalarBlock = make<NaryOp>(
op, ABTVector{make<Variable>("var1"), Constant::int32(9), make<Variable>("var2")});
Vectorizer::VariableTypes bindings;
bindings.emplace(
"var"_sd,
std::make_pair(TypeSignature::kBlockType.include(TypeSignature::kAnyScalarType),
boost::none));
bindings.emplace(
"var2"_sd,
std::make_pair(TypeSignature::kBlockType.include(TypeSignature::kAnyScalarType),
boost::none));
sbe::value::FrameIdGenerator generator;
auto processed = Vectorizer{&generator, Vectorizer::Purpose::Project}.vectorize(
treeBlockScalarBlock, bindings, boost::none);
ASSERT_TRUE(processed.expr.has_value());
assertArithmeticOperationBlockScalarBlock(fnStr, processed);
}
}
TEST(VectorizerTest, ConvertDiv) {

View File

@ -397,6 +397,15 @@ TypeSignature TypeChecker::operator()(optimizer::ABT& n,
}
return TypeSignature::kNumericType.include(sig.intersect(TypeSignature::kDateTimeType))
.include(sig.intersect(TypeSignature::kNothingType));
} else if (op.op() == optimizer::Operations::Mult) {
// The signature of the Mult is numeric plus Nothing.
TypeSignature sig = {};
for (auto& node : op.nodes()) {
TypeSignature nodeType = node.visit(*this, false);
sig = sig.include(nodeType);
}
return TypeSignature::kNumericType.include(sig.intersect(TypeSignature::kNothingType));
}
return TypeSignature::kAnyScalarType;
}

View File

@ -521,6 +521,7 @@ Vectorizer::Tree Vectorizer::vectorizeNaryHelper(const optimizer::NaryOp& op, si
case optimizer::Operations::Or:
return vectorizeLogicalOp(op.op(), lhsNode, rhsNode);
case optimizer::Operations::Add:
case optimizer::Operations::Mult:
return vectorizeArithmeticOp(op.op(), lhsNode, rhsNode);
default:
MONGO_UNREACHABLE;
@ -531,7 +532,8 @@ Vectorizer::Tree Vectorizer::operator()(const optimizer::ABT& n, const optimizer
switch (op.op()) {
case optimizer::Operations::And:
case optimizer::Operations::Or:
case optimizer::Operations::Add: {
case optimizer::Operations::Add:
case optimizer::Operations::Mult: {
return vectorizeNaryHelper(op, 0);
}
default:

View File

@ -0,0 +1,23 @@
# Golden test output of SBEPrimNaryTest/NaryMult
-- INPUT EXPRESSION:
(s1 * s2 * s3 * s4 * s5 * s6 * s7 * s8)
-- COMPILED EXPRESSION:
[0x0000-0x005d] stackSize: 1, maxStackSize: 2
0x0000: pushAccessVal(accessor: <accessor>);
0x0009: pushAccessVal(accessor: <accessor>);
0x0012: mul(popLhs: 1, moveFromLhs: 1, offsetLhs: 0, popRhs: 1, moveFromRhs: 1, offsetRhs: 0);
0x0015: pushAccessVal(accessor: <accessor>);
0x001e: mul(popLhs: 1, moveFromLhs: 1, offsetLhs: 0, popRhs: 1, moveFromRhs: 1, offsetRhs: 0);
0x0021: pushAccessVal(accessor: <accessor>);
0x002a: mul(popLhs: 1, moveFromLhs: 1, offsetLhs: 0, popRhs: 1, moveFromRhs: 1, offsetRhs: 0);
0x002d: pushAccessVal(accessor: <accessor>);
0x0036: mul(popLhs: 1, moveFromLhs: 1, offsetLhs: 0, popRhs: 1, moveFromRhs: 1, offsetRhs: 0);
0x0039: pushAccessVal(accessor: <accessor>);
0x0042: mul(popLhs: 1, moveFromLhs: 1, offsetLhs: 0, popRhs: 1, moveFromRhs: 1, offsetRhs: 0);
0x0045: pushAccessVal(accessor: <accessor>);
0x004e: mul(popLhs: 1, moveFromLhs: 1, offsetLhs: 0, popRhs: 1, moveFromRhs: 1, offsetRhs: 0);
0x0051: pushAccessVal(accessor: <accessor>);
0x005a: mul(popLhs: 1, moveFromLhs: 1, offsetLhs: 0, popRhs: 1, moveFromRhs: 1, offsetRhs: 0);