2015-09-16 14:56:30 +00:00
|
|
|
/*
|
2016-11-18 23:13:20 +00:00
|
|
|
This file is part of solidity.
|
2015-09-16 14:56:30 +00:00
|
|
|
|
2016-11-18 23:13:20 +00:00
|
|
|
solidity is free software: you can redistribute it and/or modify
|
2015-09-16 14:56:30 +00:00
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
2016-11-18 23:13:20 +00:00
|
|
|
solidity is distributed in the hope that it will be useful,
|
2015-09-16 14:56:30 +00:00
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
2016-11-18 23:13:20 +00:00
|
|
|
along with solidity. If not, see <http://www.gnu.org/licenses/>.
|
2015-09-16 14:56:30 +00:00
|
|
|
*/
|
2020-07-17 14:54:12 +00:00
|
|
|
// SPDX-License-Identifier: GPL-3.0
|
2015-09-16 14:56:30 +00:00
|
|
|
/**
|
|
|
|
* @author Christian <c@ethdev.com>
|
|
|
|
* @date 2015
|
|
|
|
* Evaluator for types of constant expressions.
|
|
|
|
*/
|
|
|
|
|
2015-10-20 22:21:52 +00:00
|
|
|
#include <libsolidity/analysis/ConstantEvaluator.h>
|
2018-12-17 11:30:08 +00:00
|
|
|
|
2015-10-20 22:21:52 +00:00
|
|
|
#include <libsolidity/ast/AST.h>
|
2019-04-15 13:33:39 +00:00
|
|
|
#include <libsolidity/ast/TypeProvider.h>
|
2018-11-14 13:59:30 +00:00
|
|
|
#include <liblangutil/ErrorReporter.h>
|
2015-09-16 14:56:30 +00:00
|
|
|
|
2021-09-16 14:33:28 +00:00
|
|
|
#include <limits>
|
|
|
|
|
2019-12-11 16:31:36 +00:00
|
|
|
using namespace solidity;
|
|
|
|
using namespace solidity::frontend;
|
2020-04-18 01:00:22 +00:00
|
|
|
using namespace solidity::langutil;
|
2015-09-16 14:56:30 +00:00
|
|
|
|
2020-11-16 11:19:52 +00:00
|
|
|
using TypedRational = ConstantEvaluator::TypedRational;
|
|
|
|
|
2020-11-18 16:54:30 +00:00
|
|
|
namespace
|
|
|
|
{
|
|
|
|
|
|
|
|
/// Check whether (_base ** _exp) fits into 4096 bits.
|
|
|
|
bool fitsPrecisionExp(bigint const& _base, bigint const& _exp)
|
|
|
|
{
|
|
|
|
if (_base == 0)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
solAssert(_base > 0, "");
|
|
|
|
|
2023-08-14 08:37:11 +00:00
|
|
|
std::size_t const bitsMax = 4096;
|
2020-11-18 16:54:30 +00:00
|
|
|
|
2023-08-14 08:37:11 +00:00
|
|
|
std::size_t mostSignificantBaseBit = static_cast<std::size_t>(boost::multiprecision::msb(_base));
|
2020-11-18 16:54:30 +00:00
|
|
|
if (mostSignificantBaseBit == 0) // _base == 1
|
|
|
|
return true;
|
|
|
|
if (mostSignificantBaseBit > bitsMax) // _base >= 2 ^ 4096
|
|
|
|
return false;
|
|
|
|
|
|
|
|
bigint bitsNeeded = _exp * (mostSignificantBaseBit + 1);
|
|
|
|
|
|
|
|
return bitsNeeded <= bitsMax;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Checks whether _mantissa * (2 ** _expBase10) fits into 4096 bits.
|
|
|
|
bool fitsPrecisionBase2(bigint const& _mantissa, uint32_t _expBase2)
|
|
|
|
{
|
|
|
|
return fitsPrecisionBaseX(_mantissa, 1.0, _expBase2);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-08-14 08:37:11 +00:00
|
|
|
std::optional<rational> ConstantEvaluator::evaluateBinaryOperator(Token _operator, rational const& _left, rational const& _right)
|
2020-11-18 16:54:30 +00:00
|
|
|
{
|
|
|
|
bool fractional = _left.denominator() != 1 || _right.denominator() != 1;
|
|
|
|
switch (_operator)
|
|
|
|
{
|
|
|
|
//bit operations will only be enabled for integers and fixed types that resemble integers
|
|
|
|
case Token::BitOr:
|
|
|
|
if (fractional)
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
else
|
|
|
|
return _left.numerator() | _right.numerator();
|
|
|
|
case Token::BitXor:
|
|
|
|
if (fractional)
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
else
|
|
|
|
return _left.numerator() ^ _right.numerator();
|
|
|
|
case Token::BitAnd:
|
|
|
|
if (fractional)
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
else
|
|
|
|
return _left.numerator() & _right.numerator();
|
|
|
|
case Token::Add: return _left + _right;
|
|
|
|
case Token::Sub: return _left - _right;
|
|
|
|
case Token::Mul: return _left * _right;
|
|
|
|
case Token::Div:
|
|
|
|
if (_right == rational(0))
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
else
|
|
|
|
return _left / _right;
|
|
|
|
case Token::Mod:
|
|
|
|
if (_right == rational(0))
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
else if (fractional)
|
|
|
|
{
|
|
|
|
rational tempValue = _left / _right;
|
|
|
|
return _left - (tempValue.numerator() / tempValue.denominator()) * _right;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
return _left.numerator() % _right.numerator();
|
|
|
|
break;
|
|
|
|
case Token::Exp:
|
|
|
|
{
|
|
|
|
if (_right.denominator() != 1)
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
bigint const& exp = _right.numerator();
|
|
|
|
|
|
|
|
// x ** 0 = 1
|
|
|
|
// for 0, 1 and -1 the size of the exponent doesn't have to be restricted
|
|
|
|
if (exp == 0)
|
|
|
|
return 1;
|
|
|
|
else if (_left == 0 || _left == 1)
|
|
|
|
return _left;
|
|
|
|
else if (_left == -1)
|
|
|
|
{
|
|
|
|
bigint isOdd = abs(exp) & bigint(1);
|
|
|
|
return 1 - 2 * isOdd.convert_to<int>();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2023-08-14 08:37:11 +00:00
|
|
|
if (abs(exp) > std::numeric_limits<uint32_t>::max())
|
|
|
|
return std::nullopt; // This will need too much memory to represent.
|
2020-11-18 16:54:30 +00:00
|
|
|
|
|
|
|
uint32_t absExp = bigint(abs(exp)).convert_to<uint32_t>();
|
|
|
|
|
|
|
|
if (!fitsPrecisionExp(abs(_left.numerator()), absExp) || !fitsPrecisionExp(abs(_left.denominator()), absExp))
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
|
|
|
|
static auto const optimizedPow = [](bigint const& _base, uint32_t _exponent) -> bigint {
|
|
|
|
if (_base == 1)
|
|
|
|
return 1;
|
|
|
|
else if (_base == -1)
|
|
|
|
return 1 - 2 * static_cast<int>(_exponent & 1);
|
|
|
|
else
|
|
|
|
return boost::multiprecision::pow(_base, _exponent);
|
|
|
|
};
|
|
|
|
|
|
|
|
bigint numerator = optimizedPow(_left.numerator(), absExp);
|
|
|
|
bigint denominator = optimizedPow(_left.denominator(), absExp);
|
|
|
|
|
|
|
|
if (exp >= 0)
|
|
|
|
return makeRational(numerator, denominator);
|
|
|
|
else
|
|
|
|
// invert
|
|
|
|
return makeRational(denominator, numerator);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case Token::SHL:
|
|
|
|
{
|
|
|
|
if (fractional)
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
else if (_right < 0)
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
|
|
|
else if (_right > std::numeric_limits<uint32_t>::max())
|
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
if (_left.numerator() == 0)
|
|
|
|
return 0;
|
|
|
|
else
|
|
|
|
{
|
|
|
|
uint32_t exponent = _right.numerator().convert_to<uint32_t>();
|
|
|
|
if (!fitsPrecisionBase2(abs(_left.numerator()), exponent))
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
return _left.numerator() * boost::multiprecision::pow(bigint(2), exponent);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// NOTE: we're using >> (SAR) to denote right shifting. The type of the LValue
|
|
|
|
// determines the resulting type and the type of shift (SAR or SHR).
|
|
|
|
case Token::SAR:
|
|
|
|
{
|
|
|
|
if (fractional)
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
else if (_right < 0)
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
|
|
|
else if (_right > std::numeric_limits<uint32_t>::max())
|
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
if (_left.numerator() == 0)
|
|
|
|
return 0;
|
|
|
|
else
|
|
|
|
{
|
|
|
|
uint32_t exponent = _right.numerator().convert_to<uint32_t>();
|
|
|
|
if (exponent > boost::multiprecision::msb(boost::multiprecision::abs(_left.numerator())))
|
|
|
|
return _left.numerator() < 0 ? -1 : 0;
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (_left.numerator() < 0)
|
|
|
|
// Add 1 to the negative value before dividing to get a result that is strictly too large,
|
|
|
|
// then subtract 1 afterwards to round towards negative infinity.
|
|
|
|
// This is the same algorithm as used in ExpressionCompiler::appendShiftOperatorCode(...).
|
|
|
|
// To see this note that for negative x, xor(x,all_ones) = (-x-1) and
|
|
|
|
// therefore xor(div(xor(x,all_ones), exp(2, shift_amount)), all_ones) is
|
|
|
|
// -(-x - 1) / 2^shift_amount - 1, which is the same as
|
|
|
|
// (x + 1) / 2^shift_amount - 1.
|
|
|
|
return rational((_left.numerator() + 1) / boost::multiprecision::pow(bigint(2), exponent) - bigint(1), 1);
|
|
|
|
else
|
|
|
|
return rational(_left.numerator() / boost::multiprecision::pow(bigint(2), exponent), 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-14 08:37:11 +00:00
|
|
|
std::optional<rational> ConstantEvaluator::evaluateUnaryOperator(Token _operator, rational const& _input)
|
2020-11-18 16:54:30 +00:00
|
|
|
{
|
|
|
|
switch (_operator)
|
|
|
|
{
|
|
|
|
case Token::BitNot:
|
|
|
|
if (_input.denominator() != 1)
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
else
|
|
|
|
return ~_input.numerator();
|
|
|
|
case Token::Sub:
|
|
|
|
return -_input;
|
|
|
|
default:
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-18 16:54:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-14 11:10:22 +00:00
|
|
|
namespace
|
|
|
|
{
|
|
|
|
|
2023-08-14 08:37:11 +00:00
|
|
|
std::optional<TypedRational> convertType(rational const& _value, Type const& _type)
|
2015-09-16 14:56:30 +00:00
|
|
|
{
|
2020-11-16 11:19:52 +00:00
|
|
|
if (_type.category() == Type::Category::RationalNumber)
|
|
|
|
return TypedRational{TypeProvider::rationalNumber(_value), _value};
|
|
|
|
else if (auto const* integerType = dynamic_cast<IntegerType const*>(&_type))
|
|
|
|
{
|
|
|
|
if (_value > integerType->maxValue() || _value < integerType->minValue())
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2020-11-16 11:19:52 +00:00
|
|
|
else
|
|
|
|
return TypedRational{&_type, _value.numerator() / _value.denominator()};
|
|
|
|
}
|
|
|
|
else
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2015-09-16 14:56:30 +00:00
|
|
|
}
|
|
|
|
|
2023-08-14 08:37:11 +00:00
|
|
|
std::optional<TypedRational> convertType(std::optional<TypedRational> const& _value, Type const& _type)
|
2015-09-16 14:56:30 +00:00
|
|
|
{
|
2023-08-14 08:37:11 +00:00
|
|
|
return _value ? convertType(_value->value, _type) : std::nullopt;
|
2015-09-16 14:56:30 +00:00
|
|
|
}
|
|
|
|
|
2023-08-14 08:37:11 +00:00
|
|
|
std::optional<TypedRational> constantToTypedValue(Type const& _type)
|
2015-09-16 14:56:30 +00:00
|
|
|
{
|
2020-11-16 11:19:52 +00:00
|
|
|
if (_type.category() == Type::Category::RationalNumber)
|
|
|
|
return TypedRational{&_type, dynamic_cast<RationalNumberType const&>(_type).value()};
|
|
|
|
else
|
2023-08-14 08:37:11 +00:00
|
|
|
return std::nullopt;
|
2015-09-16 14:56:30 +00:00
|
|
|
}
|
2017-10-28 11:03:11 +00:00
|
|
|
|
2020-12-14 11:10:22 +00:00
|
|
|
}
|
|
|
|
|
2023-08-14 08:37:11 +00:00
|
|
|
std::optional<TypedRational> ConstantEvaluator::evaluate(
|
2020-11-16 11:19:52 +00:00
|
|
|
langutil::ErrorReporter& _errorReporter,
|
|
|
|
Expression const& _expr
|
|
|
|
)
|
2017-10-28 11:03:11 +00:00
|
|
|
{
|
2020-11-16 11:19:52 +00:00
|
|
|
return ConstantEvaluator{_errorReporter}.evaluate(_expr);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-08-14 08:37:11 +00:00
|
|
|
std::optional<TypedRational> ConstantEvaluator::evaluate(ASTNode const& _node)
|
2020-11-16 11:19:52 +00:00
|
|
|
{
|
|
|
|
if (!m_values.count(&_node))
|
|
|
|
{
|
|
|
|
if (auto const* varDecl = dynamic_cast<VariableDeclaration const*>(&_node))
|
|
|
|
{
|
|
|
|
solAssert(varDecl->isConstant(), "");
|
2020-12-09 10:31:25 +00:00
|
|
|
// In some circumstances, we do not yet have a type for the variable.
|
|
|
|
if (!varDecl->value() || !varDecl->type())
|
2023-08-14 08:37:11 +00:00
|
|
|
m_values[&_node] = std::nullopt;
|
2020-11-16 11:19:52 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
m_depth++;
|
|
|
|
if (m_depth > 32)
|
|
|
|
m_errorReporter.fatalTypeError(
|
|
|
|
5210_error,
|
|
|
|
varDecl->location(),
|
|
|
|
"Cyclic constant definition (or maximum recursion depth exhausted)."
|
|
|
|
);
|
|
|
|
m_values[&_node] = convertType(evaluate(*varDecl->value()), *varDecl->type());
|
|
|
|
m_depth--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (auto const* expression = dynamic_cast<Expression const*>(&_node))
|
|
|
|
{
|
|
|
|
expression->accept(*this);
|
|
|
|
if (!m_values.count(&_node))
|
2023-08-14 08:37:11 +00:00
|
|
|
m_values[&_node] = std::nullopt;
|
2020-11-16 11:19:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return m_values.at(&_node);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ConstantEvaluator::endVisit(UnaryOperation const& _operation)
|
|
|
|
{
|
2023-08-14 08:37:11 +00:00
|
|
|
std::optional<TypedRational> value = evaluate(_operation.subExpression());
|
2020-11-16 11:19:52 +00:00
|
|
|
if (!value)
|
2017-11-22 12:41:33 +00:00
|
|
|
return;
|
2017-10-28 11:03:11 +00:00
|
|
|
|
2021-03-22 16:12:05 +00:00
|
|
|
Type const* resultType = value->type->unaryOperatorResult(_operation.getOperator());
|
2020-11-16 11:19:52 +00:00
|
|
|
if (!resultType)
|
|
|
|
return;
|
|
|
|
value = convertType(value, *resultType);
|
2017-11-17 16:55:07 +00:00
|
|
|
if (!value)
|
2017-11-22 12:41:33 +00:00
|
|
|
return;
|
2020-11-16 11:19:52 +00:00
|
|
|
|
2023-08-14 08:37:11 +00:00
|
|
|
if (std::optional<rational> result = evaluateUnaryOperator(_operation.getOperator(), value->value))
|
2017-11-17 16:55:07 +00:00
|
|
|
{
|
2023-08-14 08:37:11 +00:00
|
|
|
std::optional<TypedRational> convertedValue = convertType(*result, *resultType);
|
2020-11-16 11:19:52 +00:00
|
|
|
if (!convertedValue)
|
|
|
|
m_errorReporter.fatalTypeError(
|
|
|
|
3667_error,
|
|
|
|
_operation.location(),
|
|
|
|
"Arithmetic error when computing constant value."
|
|
|
|
);
|
|
|
|
m_values[&_operation] = convertedValue;
|
2017-10-28 11:03:11 +00:00
|
|
|
}
|
2017-11-22 11:54:23 +00:00
|
|
|
}
|
|
|
|
|
2020-11-16 11:19:52 +00:00
|
|
|
void ConstantEvaluator::endVisit(BinaryOperation const& _operation)
|
2018-04-11 15:27:06 +00:00
|
|
|
{
|
2023-08-14 08:37:11 +00:00
|
|
|
std::optional<TypedRational> left = evaluate(_operation.leftExpression());
|
|
|
|
std::optional<TypedRational> right = evaluate(_operation.rightExpression());
|
2020-11-16 11:19:52 +00:00
|
|
|
if (!left || !right)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// If this is implemented in the future: Comparison operators have a "binaryOperatorResult"
|
|
|
|
// that is non-bool, but the result has to be bool.
|
|
|
|
if (TokenTraits::isCompareOp(_operation.getOperator()))
|
|
|
|
return;
|
|
|
|
|
2021-03-22 16:12:05 +00:00
|
|
|
Type const* resultType = left->type->binaryOperatorResult(_operation.getOperator(), right->type);
|
2020-11-16 11:19:52 +00:00
|
|
|
if (!resultType)
|
|
|
|
{
|
|
|
|
m_errorReporter.fatalTypeError(
|
|
|
|
6020_error,
|
|
|
|
_operation.location(),
|
|
|
|
"Operator " +
|
2023-08-14 08:37:11 +00:00
|
|
|
std::string(TokenTraits::toString(_operation.getOperator())) +
|
2020-11-16 11:19:52 +00:00
|
|
|
" not compatible with types " +
|
|
|
|
left->type->toString() +
|
|
|
|
" and " +
|
|
|
|
right->type->toString()
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
left = convertType(left, *resultType);
|
|
|
|
right = convertType(right, *resultType);
|
|
|
|
if (!left || !right)
|
|
|
|
return;
|
|
|
|
|
2023-08-14 08:37:11 +00:00
|
|
|
if (std::optional<rational> value = evaluateBinaryOperator(_operation.getOperator(), left->value, right->value))
|
2020-11-16 11:19:52 +00:00
|
|
|
{
|
2023-08-14 08:37:11 +00:00
|
|
|
std::optional<TypedRational> convertedValue = convertType(*value, *resultType);
|
2020-11-16 11:19:52 +00:00
|
|
|
if (!convertedValue)
|
|
|
|
m_errorReporter.fatalTypeError(
|
|
|
|
2643_error,
|
|
|
|
_operation.location(),
|
|
|
|
"Arithmetic error when computing constant value."
|
|
|
|
);
|
|
|
|
m_values[&_operation] = convertedValue;
|
|
|
|
}
|
2018-04-11 15:27:06 +00:00
|
|
|
}
|
|
|
|
|
2020-11-16 11:19:52 +00:00
|
|
|
void ConstantEvaluator::endVisit(Literal const& _literal)
|
2017-11-22 11:54:23 +00:00
|
|
|
{
|
2020-11-16 11:19:52 +00:00
|
|
|
if (Type const* literalType = TypeProvider::forLiteral(_literal))
|
|
|
|
m_values[&_literal] = constantToTypedValue(*literalType);
|
2017-11-22 11:54:23 +00:00
|
|
|
}
|
|
|
|
|
2020-11-16 11:19:52 +00:00
|
|
|
void ConstantEvaluator::endVisit(Identifier const& _identifier)
|
2017-11-22 11:54:23 +00:00
|
|
|
{
|
2020-11-16 11:19:52 +00:00
|
|
|
VariableDeclaration const* variableDeclaration = dynamic_cast<VariableDeclaration const*>(_identifier.annotation().referencedDeclaration);
|
|
|
|
if (variableDeclaration && variableDeclaration->isConstant())
|
|
|
|
m_values[&_identifier] = evaluate(*variableDeclaration);
|
2017-11-22 11:54:23 +00:00
|
|
|
}
|
|
|
|
|
2020-11-16 11:19:52 +00:00
|
|
|
void ConstantEvaluator::endVisit(TupleExpression const& _tuple)
|
2017-11-22 11:54:23 +00:00
|
|
|
{
|
2020-11-16 11:19:52 +00:00
|
|
|
if (!_tuple.isInlineArray() && _tuple.components().size() == 1)
|
|
|
|
m_values[&_tuple] = evaluate(*_tuple.components().front());
|
2017-10-28 11:03:11 +00:00
|
|
|
}
|