Merge pull request #7178 from ethereum/calldataSizeTest

Split calldataEncodedSize.
This commit is contained in:
chriseth 2019-08-08 16:20:52 +02:00 committed by GitHub
commit 16efcfdbb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 194 additions and 123 deletions

View File

@ -22,6 +22,7 @@ Compiler Features:
Bugfixes: Bugfixes:
* ABI decoder: Ensure that decoded arrays always point to distinct memory locations. * ABI decoder: Ensure that decoded arrays always point to distinct memory locations.
* Code Generator: Treat dynamically encoded but statically sized arrays and structs in calldata properly.
* SMTChecker: Fix internal error when inlining functions that contain tuple expressions. * SMTChecker: Fix internal error when inlining functions that contain tuple expressions.
* SMTChecker: Fix pointer knowledge erasing in loops. * SMTChecker: Fix pointer knowledge erasing in loops.
* SMTChecker: Fix internal error when using compound bitwise assignment operators inside branches. * SMTChecker: Fix internal error when using compound bitwise assignment operators inside branches.

View File

@ -1647,15 +1647,13 @@ bool ArrayType::validForCalldata() const
if (auto arrayBaseType = dynamic_cast<ArrayType const*>(baseType())) if (auto arrayBaseType = dynamic_cast<ArrayType const*>(baseType()))
if (!arrayBaseType->validForCalldata()) if (!arrayBaseType->validForCalldata())
return false; return false;
return unlimitedCalldataEncodedSize(true) <= numeric_limits<unsigned>::max(); return isDynamicallySized() || unlimitedStaticCalldataSize(true) <= numeric_limits<unsigned>::max();
} }
bigint ArrayType::unlimitedCalldataEncodedSize(bool _padded) const bigint ArrayType::unlimitedStaticCalldataSize(bool _padded) const
{ {
if (isDynamicallySized()) solAssert(!isDynamicallySized(), "");
return 32; bigint size = bigint(length()) * calldataStride();
// Array elements are always padded.
bigint size = bigint(length()) * (isByteArray() ? 1 : baseType()->calldataEncodedSize(true));
if (_padded) if (_padded)
size = ((size + 31) / 32) * 32; size = ((size + 31) / 32) * 32;
return size; return size;
@ -1663,7 +1661,20 @@ bigint ArrayType::unlimitedCalldataEncodedSize(bool _padded) const
unsigned ArrayType::calldataEncodedSize(bool _padded) const unsigned ArrayType::calldataEncodedSize(bool _padded) const
{ {
bigint size = unlimitedCalldataEncodedSize(_padded); solAssert(!isDynamicallyEncoded(), "");
bigint size = unlimitedStaticCalldataSize(_padded);
solAssert(size <= numeric_limits<unsigned>::max(), "Array size does not fit unsigned.");
return unsigned(size);
}
unsigned ArrayType::calldataEncodedTailSize() const
{
solAssert(isDynamicallyEncoded(), "");
if (isDynamicallySized())
// We do not know the dynamic length itself, but at least the uint256 containing the
// length must still be present.
return 32;
bigint size = unlimitedStaticCalldataSize(false);
solAssert(size <= numeric_limits<unsigned>::max(), "Array size does not fit unsigned."); solAssert(size <= numeric_limits<unsigned>::max(), "Array size does not fit unsigned.");
return unsigned(size); return unsigned(size);
} }
@ -1832,10 +1843,11 @@ TypeResult ArrayType::interfaceType(bool _inLibrary) const
return result; return result;
} }
u256 ArrayType::memorySize() const u256 ArrayType::memoryDataSize() const
{ {
solAssert(!isDynamicallySized(), ""); solAssert(!isDynamicallySized(), "");
solAssert(m_location == DataLocation::Memory, ""); solAssert(m_location == DataLocation::Memory, "");
solAssert(!isByteArray(), "");
bigint size = bigint(m_length) * m_baseType->memoryHeadSize(); bigint size = bigint(m_length) * m_baseType->memoryHeadSize();
solAssert(size <= numeric_limits<unsigned>::max(), "Array size does not fit u256."); solAssert(size <= numeric_limits<unsigned>::max(), "Array size does not fit u256.");
return u256(size); return u256(size);
@ -1992,20 +2004,33 @@ bool StructType::operator==(Type const& _other) const
return ReferenceType::operator==(other) && other.m_struct == m_struct; return ReferenceType::operator==(other) && other.m_struct == m_struct;
} }
unsigned StructType::calldataEncodedSize(bool) const unsigned StructType::calldataEncodedSize(bool) const
{ {
solAssert(!isDynamicallyEncoded(), "");
unsigned size = 0; unsigned size = 0;
for (auto const& member: members(nullptr)) for (auto const& member: members(nullptr))
if (!member.type->canLiveOutsideStorage()) {
return 0; solAssert(member.type->canLiveOutsideStorage(), "");
else // Struct members are always padded.
{ size += member.type->calldataEncodedSize();
// Struct members are always padded. }
unsigned memberSize = member.type->calldataEncodedSize(true); return size;
if (memberSize == 0) }
return 0;
size += memberSize;
} unsigned StructType::calldataEncodedTailSize() const
{
solAssert(isDynamicallyEncoded(), "");
unsigned size = 0;
for (auto const& member: members(nullptr))
{
solAssert(member.type->canLiveOutsideStorage(), "");
// Struct members are always padded.
size += member.type->calldataHeadSize();
}
return size; return size;
} }
@ -2017,12 +2042,8 @@ unsigned StructType::calldataOffsetOfMember(std::string const& _member) const
solAssert(member.type->canLiveOutsideStorage(), ""); solAssert(member.type->canLiveOutsideStorage(), "");
if (member.name == _member) if (member.name == _member)
return offset; return offset;
{ // Struct members are always padded.
// Struct members are always padded. offset += member.type->calldataHeadSize();
unsigned memberSize = member.type->calldataEncodedSize(true);
solAssert(memberSize != 0, "");
offset += memberSize;
}
} }
solAssert(false, "Struct member not found."); solAssert(false, "Struct member not found.");
} }
@ -2040,7 +2061,7 @@ bool StructType::isDynamicallyEncoded() const
return false; return false;
} }
u256 StructType::memorySize() const u256 StructType::memoryDataSize() const
{ {
u256 size; u256 size;
for (auto const& t: memoryMemberTypes()) for (auto const& t: memoryMemberTypes())

View File

@ -208,16 +208,32 @@ public:
virtual bool operator==(Type const& _other) const { return category() == _other.category(); } virtual bool operator==(Type const& _other) const { return category() == _other.category(); }
virtual bool operator!=(Type const& _other) const { return !this->operator ==(_other); } virtual bool operator!=(Type const& _other) const { return !this->operator ==(_other); }
/// @returns number of bytes used by this type when encoded for CALL. If it is a dynamic type, /// @returns number of bytes used by this type when encoded for CALL. Cannot be used for
/// returns the size of the pointer (usually 32). Returns 0 if the type cannot be encoded /// dynamically encoded types.
/// in calldata. /// Always returns a value greater than zero and throws if the type cannot be encoded in calldata
/// (or is dynamically encoded).
/// If @a _padded then it is assumed that each element is padded to a multiple of 32 bytes. /// If @a _padded then it is assumed that each element is padded to a multiple of 32 bytes.
virtual unsigned calldataEncodedSize(bool _padded) const { (void)_padded; return 0; } virtual unsigned calldataEncodedSize(bool _padded) const { (void)_padded; solAssert(false, ""); }
/// Convenience version of @see calldataEncodedSize(bool)
unsigned calldataEncodedSize() const { return calldataEncodedSize(true); }
/// @returns the distance between two elements of this type in a calldata array, tuple or struct.
/// For statically encoded types this is the same as calldataEncodedSize(true).
/// For dynamically encoded types this is the distance between two tail pointers, i.e. 32.
/// Always returns a value greater than zero and throws if the type cannot be encoded in calldata.
unsigned calldataHeadSize() const { return isDynamicallyEncoded() ? 32 : calldataEncodedSize(true); }
/// @returns the (minimal) size of the calldata tail for this type. Can only be used for
/// dynamically encoded types. For dynamically-sized arrays this is 32 (the size of the length),
/// for statically-sized, but dynamically encoded arrays this is 32*length(), for structs
/// this is the sum of the calldataHeadSize's of its members.
/// Always returns a value greater than zero and throws if the type cannot be encoded in calldata
/// (or is not dynamically encoded).
virtual unsigned calldataEncodedTailSize() const { solAssert(false, ""); }
/// @returns the size of this data type in bytes when stored in memory. For memory-reference /// @returns the size of this data type in bytes when stored in memory. For memory-reference
/// types, this is the size of the memory pointer. /// types, this is the size of the memory pointer.
virtual unsigned memoryHeadSize() const { return calldataEncodedSize(); } virtual unsigned memoryHeadSize() const { return calldataEncodedSize(); }
/// Convenience version of @see calldataEncodedSize(bool) /// @returns the size of this data type in bytes when stored in memory. For memory-reference
unsigned calldataEncodedSize() const { return calldataEncodedSize(true); } /// types, this is the size of the actual data area, if it is statically-sized.
virtual u256 memoryDataSize() const { return calldataEncodedSize(); }
/// @returns true if the type is a dynamic array /// @returns true if the type is a dynamic array
virtual bool isDynamicallySized() const { return false; } virtual bool isDynamicallySized() const { return false; }
/// @returns true if the type is dynamically encoded in the ABI /// @returns true if the type is dynamically encoded in the ABI
@ -634,6 +650,10 @@ public:
return nullptr; return nullptr;
} }
unsigned memoryHeadSize() const override { return 32; } unsigned memoryHeadSize() const override { return 32; }
u256 memoryDataSize() const override = 0;
unsigned calldataEncodedSize(bool) const override = 0;
unsigned calldataEncodedTailSize() const override = 0;
/// @returns a copy of this type with location (recursively) changed to @a _location, /// @returns a copy of this type with location (recursively) changed to @a _location,
/// whereas isPointer is only shallowly changed - the deep copy is always a bound reference. /// whereas isPointer is only shallowly changed - the deep copy is always a bound reference.
@ -701,7 +721,8 @@ public:
BoolResult isExplicitlyConvertibleTo(Type const& _convertTo) const override; BoolResult isExplicitlyConvertibleTo(Type const& _convertTo) const override;
std::string richIdentifier() const override; std::string richIdentifier() const override;
bool operator==(Type const& _other) const override; bool operator==(Type const& _other) const override;
unsigned calldataEncodedSize(bool _padded) const override; unsigned calldataEncodedSize(bool) const override;
unsigned calldataEncodedTailSize() const override;
bool isDynamicallySized() const override { return m_hasDynamicLength; } bool isDynamicallySized() const override { return m_hasDynamicLength; }
bool isDynamicallyEncoded() const override; bool isDynamicallyEncoded() const override;
u256 storageSize() const override; u256 storageSize() const override;
@ -724,12 +745,12 @@ public:
bool isString() const { return m_arrayKind == ArrayKind::String; } bool isString() const { return m_arrayKind == ArrayKind::String; }
Type const* baseType() const { solAssert(!!m_baseType, ""); return m_baseType; } Type const* baseType() const { solAssert(!!m_baseType, ""); return m_baseType; }
u256 const& length() const { return m_length; } u256 const& length() const { return m_length; }
u256 memorySize() const; u256 memoryDataSize() const override;
std::unique_ptr<ReferenceType> copyForLocation(DataLocation _location, bool _isPointer) const override; std::unique_ptr<ReferenceType> copyForLocation(DataLocation _location, bool _isPointer) const override;
/// The offset to advance in calldata to move from one array element to the next. /// The offset to advance in calldata to move from one array element to the next.
unsigned calldataStride() const { return isByteArray() ? 1 : m_baseType->calldataEncodedSize(); } unsigned calldataStride() const { return isByteArray() ? 1 : m_baseType->calldataHeadSize(); }
/// The offset to advance in memory to move from one array element to the next. /// The offset to advance in memory to move from one array element to the next.
unsigned memoryStride() const { return isByteArray() ? 1 : m_baseType->memoryHeadSize(); } unsigned memoryStride() const { return isByteArray() ? 1 : m_baseType->memoryHeadSize(); }
/// The offset to advance in storage to move from one array element to the next. /// The offset to advance in storage to move from one array element to the next.
@ -741,7 +762,7 @@ private:
/// String is interpreted as a subtype of Bytes. /// String is interpreted as a subtype of Bytes.
enum class ArrayKind { Ordinary, Bytes, String }; enum class ArrayKind { Ordinary, Bytes, String };
bigint unlimitedCalldataEncodedSize(bool _padded) const; bigint unlimitedStaticCalldataSize(bool _padded) const;
///< Byte arrays ("bytes") and strings have different semantics from ordinary arrays. ///< Byte arrays ("bytes") and strings have different semantics from ordinary arrays.
ArrayKind m_arrayKind = ArrayKind::Ordinary; ArrayKind m_arrayKind = ArrayKind::Ordinary;
@ -829,9 +850,10 @@ public:
BoolResult isImplicitlyConvertibleTo(Type const& _convertTo) const override; BoolResult isImplicitlyConvertibleTo(Type const& _convertTo) const override;
std::string richIdentifier() const override; std::string richIdentifier() const override;
bool operator==(Type const& _other) const override; bool operator==(Type const& _other) const override;
unsigned calldataEncodedSize(bool _padded) const override; unsigned calldataEncodedSize(bool) const override;
unsigned calldataEncodedTailSize() const override;
bool isDynamicallyEncoded() const override; bool isDynamicallyEncoded() const override;
u256 memorySize() const; u256 memoryDataSize() const override;
u256 storageSize() const override; u256 storageSize() const override;
bool canLiveOutsideStorage() const override { return true; } bool canLiveOutsideStorage() const override { return true; }
std::string toString(bool _short) const override; std::string toString(bool _short) const override;

View File

@ -88,7 +88,7 @@ string ABIFunctions::tupleEncoder(
elementTempl("pos", to_string(headPos)); elementTempl("pos", to_string(headPos));
elementTempl("abiEncode", abiEncodingFunction(*_givenTypes[i], *_targetTypes[i], options)); elementTempl("abiEncode", abiEncodingFunction(*_givenTypes[i], *_targetTypes[i], options));
encodeElements += elementTempl.render(); encodeElements += elementTempl.render();
headPos += dynamic ? 0x20 : _targetTypes[i]->calldataEncodedSize(); headPos += _targetTypes[i]->calldataHeadSize();
stackPos += sizeOnStack; stackPos += sizeOnStack;
} }
solAssert(headPos == headSize_, ""); solAssert(headPos == headSize_, "");
@ -225,7 +225,7 @@ string ABIFunctions::tupleDecoder(TypePointers const& _types, bool _fromMemory)
elementTempl("pos", to_string(headPos)); elementTempl("pos", to_string(headPos));
elementTempl("abiDecode", abiDecodingFunction(*_types[i], _fromMemory, true)); elementTempl("abiDecode", abiDecodingFunction(*_types[i], _fromMemory, true));
decodeElements += elementTempl.render(); decodeElements += elementTempl.render();
headPos += dynamic ? 0x20 : decodingTypes[i]->calldataEncodedSize(); headPos += decodingTypes[i]->calldataHeadSize();
} }
templ("valueReturnParams", boost::algorithm::join(valueReturnParams, ", ")); templ("valueReturnParams", boost::algorithm::join(valueReturnParams, ", "));
templ("arrow", valueReturnParams.empty() ? "" : "->"); templ("arrow", valueReturnParams.empty() ? "" : "->");
@ -691,6 +691,7 @@ string ABIFunctions::abiEncodingFunctionCompactStorageArray(
// Multiple items per slot // Multiple items per slot
solAssert(_from.baseType()->storageBytes() <= 16, ""); solAssert(_from.baseType()->storageBytes() <= 16, "");
solAssert(!_from.baseType()->isDynamicallyEncoded(), ""); solAssert(!_from.baseType()->isDynamicallyEncoded(), "");
solAssert(!_to.baseType()->isDynamicallyEncoded(), "");
solAssert(_from.baseType()->isValueType(), ""); solAssert(_from.baseType()->isValueType(), "");
bool dynamic = _to.isDynamicallyEncoded(); bool dynamic = _to.isDynamicallyEncoded();
size_t storageBytes = _from.baseType()->storageBytes(); size_t storageBytes = _from.baseType()->storageBytes();
@ -715,7 +716,7 @@ string ABIFunctions::abiEncodingFunctionCompactStorageArray(
let data := sload(srcPtr) let data := sload(srcPtr)
<#items> <#items>
<encodeToMemoryFun>(<extractFromSlot>(data), pos) <encodeToMemoryFun>(<extractFromSlot>(data), pos)
pos := add(pos, <elementEncodedSize>) pos := add(pos, <stride>)
</items> </items>
srcPtr := add(srcPtr, 1) srcPtr := add(srcPtr, 1)
} }
@ -726,7 +727,7 @@ string ABIFunctions::abiEncodingFunctionCompactStorageArray(
<#items> <#items>
if <inRange> { if <inRange> {
<encodeToMemoryFun>(<extractFromSlot>(data), pos) <encodeToMemoryFun>(<extractFromSlot>(data), pos)
pos := add(pos, <elementEncodedSize>) pos := add(pos, <stride>)
itemCounter := add(itemCounter, 1) itemCounter := add(itemCounter, 1)
} }
</items> </items>
@ -753,9 +754,7 @@ string ABIFunctions::abiEncodingFunctionCompactStorageArray(
else else
templ("useSpill", "0"); templ("useSpill", "0");
templ("itemsPerSlot", to_string(itemsPerSlot)); templ("itemsPerSlot", to_string(itemsPerSlot));
// We use padded size because array elements are always padded. templ("stride", toCompactHexWithPrefix(_to.calldataStride()));
string elementEncodedSize = toCompactHexWithPrefix(_to.baseType()->calldataEncodedSize());
templ("elementEncodedSize", elementEncodedSize);
EncodingOptions subOptions(_options); EncodingOptions subOptions(_options);
subOptions.encodeFunctionFromStack = false; subOptions.encodeFunctionFromStack = false;
@ -914,7 +913,7 @@ string ABIFunctions::abiEncodingFunctionStruct(
); );
encodeTempl("memberValues", memberValues); encodeTempl("memberValues", memberValues);
encodeTempl("encodingOffset", toCompactHexWithPrefix(encodingOffset)); encodeTempl("encodingOffset", toCompactHexWithPrefix(encodingOffset));
encodingOffset += dynamicMember ? 0x20 : memberTypeTo->calldataEncodedSize(); encodingOffset += memberTypeTo->calldataHeadSize();
encodeTempl("abiEncode", abiEncodingFunction(*memberTypeFrom, *memberTypeTo, subOptions)); encodeTempl("abiEncode", abiEncodingFunction(*memberTypeFrom, *memberTypeTo, subOptions));
encode = encodeTempl.render(); encode = encodeTempl.render();
} }
@ -1080,8 +1079,8 @@ string ABIFunctions::abiDecodingFunctionValueType(Type const& _type, bool _fromM
solAssert(decodingType, ""); solAssert(decodingType, "");
solAssert(decodingType->sizeOnStack() == 1, ""); solAssert(decodingType->sizeOnStack() == 1, "");
solAssert(decodingType->isValueType(), ""); solAssert(decodingType->isValueType(), "");
solAssert(decodingType->calldataEncodedSize() == 32, "");
solAssert(!decodingType->isDynamicallyEncoded(), ""); solAssert(!decodingType->isDynamicallyEncoded(), "");
solAssert(decodingType->calldataEncodedSize() == 32, "");
string functionName = string functionName =
"abi_decode_" + "abi_decode_" +
@ -1135,7 +1134,7 @@ string ABIFunctions::abiDecodingFunctionArray(ArrayType const& _type, bool _from
let elementPos := <retrieveElementPos> let elementPos := <retrieveElementPos>
mstore(dst, <decodingFun>(elementPos, end)) mstore(dst, <decodingFun>(elementPos, end))
dst := add(dst, 0x20) dst := add(dst, 0x20)
src := add(src, <baseEncodedSize>) src := add(src, <stride>)
} }
} }
)" )"
@ -1145,6 +1144,8 @@ string ABIFunctions::abiDecodingFunctionArray(ArrayType const& _type, bool _from
templ("retrieveLength", !_type.isDynamicallySized() ? toCompactHexWithPrefix(_type.length()) : load + "(offset)"); templ("retrieveLength", !_type.isDynamicallySized() ? toCompactHexWithPrefix(_type.length()) : load + "(offset)");
templ("allocate", m_utils.allocationFunction()); templ("allocate", m_utils.allocationFunction());
templ("allocationSize", m_utils.arrayAllocationSizeFunction(_type)); templ("allocationSize", m_utils.arrayAllocationSizeFunction(_type));
string calldataStride = toCompactHexWithPrefix(_type.calldataStride());
templ("stride", calldataStride);
if (_type.isDynamicallySized()) if (_type.isDynamicallySized())
templ("storeLength", "mstore(array, length) offset := add(offset, 0x20) dst := add(dst, 0x20)"); templ("storeLength", "mstore(array, length) offset := add(offset, 0x20) dst := add(dst, 0x20)");
else else
@ -1153,14 +1154,11 @@ string ABIFunctions::abiDecodingFunctionArray(ArrayType const& _type, bool _from
{ {
templ("staticBoundsCheck", ""); templ("staticBoundsCheck", "");
templ("retrieveElementPos", "add(offset, " + load + "(src))"); templ("retrieveElementPos", "add(offset, " + load + "(src))");
templ("baseEncodedSize", "0x20");
} }
else else
{ {
string baseEncodedSize = toCompactHexWithPrefix(_type.baseType()->calldataEncodedSize()); templ("staticBoundsCheck", "if gt(add(src, mul(length, " + calldataStride + ")), end) { revert(0, 0) }");
templ("staticBoundsCheck", "if gt(add(src, mul(length, " + baseEncodedSize + ")), end) { revert(0, 0) }");
templ("retrieveElementPos", "src"); templ("retrieveElementPos", "src");
templ("baseEncodedSize", baseEncodedSize);
} }
templ("decodingFun", abiDecodingFunction(*_type.baseType(), _fromMemory, false)); templ("decodingFun", abiDecodingFunction(*_type.baseType(), _fromMemory, false));
return templ.render(); return templ.render();
@ -1172,8 +1170,8 @@ string ABIFunctions::abiDecodingFunctionCalldataArray(ArrayType const& _type)
solAssert(_type.dataStoredIn(DataLocation::CallData), ""); solAssert(_type.dataStoredIn(DataLocation::CallData), "");
if (!_type.isDynamicallySized()) if (!_type.isDynamicallySized())
solAssert(_type.length() < u256("0xffffffffffffffff"), ""); solAssert(_type.length() < u256("0xffffffffffffffff"), "");
solAssert(_type.baseType()->calldataEncodedSize() > 0, ""); solAssert(_type.calldataStride() > 0, "");
solAssert(_type.baseType()->calldataEncodedSize() < u256("0xffffffffffffffff"), ""); solAssert(_type.calldataStride() < u256("0xffffffffffffffff"), "");
string functionName = string functionName =
"abi_decode_" + "abi_decode_" +
@ -1188,7 +1186,7 @@ string ABIFunctions::abiDecodingFunctionCalldataArray(ArrayType const& _type)
length := calldataload(offset) length := calldataload(offset)
if gt(length, 0xffffffffffffffff) { revert(0, 0) } if gt(length, 0xffffffffffffffff) { revert(0, 0) }
arrayPos := add(offset, 0x20) arrayPos := add(offset, 0x20)
if gt(add(arrayPos, mul(length, <baseEncodedSize>)), end) { revert(0, 0) } if gt(add(arrayPos, mul(length, <stride>)), end) { revert(0, 0) }
} }
)"; )";
else else
@ -1196,13 +1194,13 @@ string ABIFunctions::abiDecodingFunctionCalldataArray(ArrayType const& _type)
// <readableTypeName> // <readableTypeName>
function <functionName>(offset, end) -> arrayPos { function <functionName>(offset, end) -> arrayPos {
arrayPos := offset arrayPos := offset
if gt(add(arrayPos, mul(<length>, <baseEncodedSize>)), end) { revert(0, 0) } if gt(add(arrayPos, mul(<length>, <stride>)), end) { revert(0, 0) }
} }
)"; )";
Whiskers w{templ}; Whiskers w{templ};
w("functionName", functionName); w("functionName", functionName);
w("readableTypeName", _type.toString(true)); w("readableTypeName", _type.toString(true));
w("baseEncodedSize", toCompactHexWithPrefix(_type.isByteArray() ? 1 : _type.baseType()->calldataEncodedSize())); w("stride", toCompactHexWithPrefix(_type.calldataStride()));
if (!_type.isDynamicallySized()) if (!_type.isDynamicallySized())
w("length", toCompactHexWithPrefix(_type.length())); w("length", toCompactHexWithPrefix(_type.length()));
return w.render(); return w.render();
@ -1246,7 +1244,6 @@ string ABIFunctions::abiDecodingFunctionByteArray(ArrayType const& _type, bool _
string ABIFunctions::abiDecodingFunctionCalldataStruct(StructType const& _type) string ABIFunctions::abiDecodingFunctionCalldataStruct(StructType const& _type)
{ {
solAssert(_type.dataStoredIn(DataLocation::CallData), ""); solAssert(_type.dataStoredIn(DataLocation::CallData), "");
solAssert(_type.calldataEncodedSize(true) != 0, "");
string functionName = string functionName =
"abi_decode_" + "abi_decode_" +
_type.identifier(); _type.identifier();
@ -1261,7 +1258,7 @@ string ABIFunctions::abiDecodingFunctionCalldataStruct(StructType const& _type)
)"}; )"};
w("functionName", functionName); w("functionName", functionName);
w("readableTypeName", _type.toString(true)); w("readableTypeName", _type.toString(true));
w("minimumSize", to_string(_type.calldataEncodedSize(true))); w("minimumSize", to_string(_type.isDynamicallyEncoded() ? _type.calldataEncodedTailSize() : _type.calldataEncodedSize(true)));
return w.render(); return w.render();
}); });
} }
@ -1291,8 +1288,8 @@ string ABIFunctions::abiDecodingFunctionStruct(StructType const& _type, bool _fr
templ("functionName", functionName); templ("functionName", functionName);
templ("readableTypeName", _type.toString(true)); templ("readableTypeName", _type.toString(true));
templ("allocate", m_utils.allocationFunction()); templ("allocate", m_utils.allocationFunction());
solAssert(_type.memorySize() < u256("0xffffffffffffffff"), ""); solAssert(_type.memoryDataSize() < u256("0xffffffffffffffff"), "");
templ("memorySize", toCompactHexWithPrefix(_type.memorySize())); templ("memorySize", toCompactHexWithPrefix(_type.memoryDataSize()));
size_t headPos = 0; size_t headPos = 0;
vector<map<string, string>> members; vector<map<string, string>> members;
for (auto const& member: _type.members(nullptr)) for (auto const& member: _type.members(nullptr))
@ -1322,7 +1319,7 @@ string ABIFunctions::abiDecodingFunctionStruct(StructType const& _type, bool _fr
members.push_back({}); members.push_back({});
members.back()["decode"] = memberTempl.render(); members.back()["decode"] = memberTempl.render();
members.back()["memberName"] = member.name; members.back()["memberName"] = member.name;
headPos += dynamic ? 0x20 : decodingType->calldataEncodedSize(); headPos += decodingType->calldataHeadSize();
} }
templ("members", members); templ("members", members);
templ("minimumSize", toCompactHexWithPrefix(headPos)); templ("minimumSize", toCompactHexWithPrefix(headPos));
@ -1376,8 +1373,8 @@ string ABIFunctions::calldataAccessFunction(Type const& _type)
return createFunction(functionName, [&]() { return createFunction(functionName, [&]() {
if (_type.isDynamicallyEncoded()) if (_type.isDynamicallyEncoded())
{ {
unsigned int baseEncodedSize = _type.calldataEncodedSize(); unsigned int tailSize = _type.calldataEncodedTailSize();
solAssert(baseEncodedSize > 1, ""); solAssert(tailSize > 1, "");
Whiskers w(R"( Whiskers w(R"(
function <functionName>(base_ref, ptr) -> <return> { function <functionName>(base_ref, ptr) -> <return> {
let rel_offset_of_tail := calldataload(ptr) let rel_offset_of_tail := calldataload(ptr)
@ -1390,13 +1387,12 @@ string ABIFunctions::calldataAccessFunction(Type const& _type)
{ {
auto const* arrayType = dynamic_cast<ArrayType const*>(&_type); auto const* arrayType = dynamic_cast<ArrayType const*>(&_type);
solAssert(!!arrayType, ""); solAssert(!!arrayType, "");
unsigned int calldataStride = arrayType->calldataStride();
w("handleLength", Whiskers(R"( w("handleLength", Whiskers(R"(
length := calldataload(value) length := calldataload(value)
value := add(value, 0x20) value := add(value, 0x20)
if gt(length, 0xffffffffffffffff) { revert(0, 0) } if gt(length, 0xffffffffffffffff) { revert(0, 0) }
if sgt(base_ref, sub(calldatasize(), mul(length, <calldataStride>))) { revert(0, 0) } if sgt(base_ref, sub(calldatasize(), mul(length, <calldataStride>))) { revert(0, 0) }
)")("calldataStride", toCompactHexWithPrefix(calldataStride)).render()); )")("calldataStride", toCompactHexWithPrefix(arrayType->calldataStride())).render());
w("return", "value, length"); w("return", "value, length");
} }
else else
@ -1404,7 +1400,7 @@ string ABIFunctions::calldataAccessFunction(Type const& _type)
w("handleLength", ""); w("handleLength", "");
w("return", "value"); w("return", "value");
} }
w("neededLength", toCompactHexWithPrefix(baseEncodedSize)); w("neededLength", toCompactHexWithPrefix(tailSize));
w("functionName", functionName); w("functionName", functionName);
return w.render(); return w.render();
} }
@ -1483,12 +1479,7 @@ size_t ABIFunctions::headSize(TypePointers const& _targetTypes)
{ {
size_t headSize = 0; size_t headSize = 0;
for (auto const& t: _targetTypes) for (auto const& t: _targetTypes)
{ headSize += t->calldataHeadSize();
if (t->isDynamicallyEncoded())
headSize += 0x20;
else
headSize += t->calldataEncodedSize();
}
return headSize; return headSize;
} }

View File

@ -243,7 +243,7 @@ void ArrayUtils::copyArrayToStorage(ArrayType const& _targetType, ArrayType cons
else if (_sourceType.location() == DataLocation::Memory) else if (_sourceType.location() == DataLocation::Memory)
_context << sourceBaseType->memoryHeadSize(); _context << sourceBaseType->memoryHeadSize();
else else
_context << sourceBaseType->calldataEncodedSize(true); _context << sourceBaseType->calldataHeadSize();
_context _context
<< Instruction::ADD << Instruction::ADD
<< swapInstruction(2 + byteOffsetSize); << swapInstruction(2 + byteOffsetSize);
@ -294,20 +294,13 @@ void ArrayUtils::copyArrayToMemory(ArrayType const& _sourceType, bool _padToWord
"Nested dynamic arrays not implemented here." "Nested dynamic arrays not implemented here."
); );
CompilerUtils utils(m_context); CompilerUtils utils(m_context);
unsigned baseSize = 1;
if (!_sourceType.isByteArray())
{
// We always pad the elements, regardless of _padToWordBoundaries.
baseSize = _sourceType.baseType()->calldataEncodedSize();
solAssert(baseSize >= 0x20, "");
}
if (_sourceType.location() == DataLocation::CallData) if (_sourceType.location() == DataLocation::CallData)
{ {
if (!_sourceType.isDynamicallySized()) if (!_sourceType.isDynamicallySized())
m_context << _sourceType.length(); m_context << _sourceType.length();
if (baseSize > 1) if (!_sourceType.isByteArray())
m_context << u256(baseSize) << Instruction::MUL; convertLengthToSize(_sourceType);
string routine = "calldatacopy(target, source, len)\n"; string routine = "calldatacopy(target, source, len)\n";
if (_padToWordBoundaries) if (_padToWordBoundaries)
@ -358,15 +351,14 @@ void ArrayUtils::copyArrayToMemory(ArrayType const& _sourceType, bool _padToWord
m_context << Instruction::SWAP1 << u256(32) << Instruction::ADD; m_context << Instruction::SWAP1 << u256(32) << Instruction::ADD;
m_context << Instruction::SWAP1; m_context << Instruction::SWAP1;
} }
// convert length to size if (!_sourceType.isByteArray())
if (baseSize > 1) convertLengthToSize(_sourceType);
m_context << u256(baseSize) << Instruction::MUL;
// stack: <target> <source> <size> // stack: <target> <source> <size>
m_context << Instruction::DUP1 << Instruction::DUP4 << Instruction::DUP4; m_context << Instruction::DUP1 << Instruction::DUP4 << Instruction::DUP4;
// We can resort to copying full 32 bytes only if // We can resort to copying full 32 bytes only if
// - the length is known to be a multiple of 32 or // - the length is known to be a multiple of 32 or
// - we will pad to full 32 bytes later anyway. // - we will pad to full 32 bytes later anyway.
if (((baseSize % 32) == 0) || _padToWordBoundaries) if (!_sourceType.isByteArray() || _padToWordBoundaries)
utils.memoryCopy32(); utils.memoryCopy32();
else else
utils.memoryCopy(); utils.memoryCopy();
@ -374,11 +366,8 @@ void ArrayUtils::copyArrayToMemory(ArrayType const& _sourceType, bool _padToWord
m_context << Instruction::SWAP1 << Instruction::POP; m_context << Instruction::SWAP1 << Instruction::POP;
// stack: <target> <size> // stack: <target> <size>
bool paddingNeeded = false; bool paddingNeeded = _padToWordBoundaries && _sourceType.isByteArray();
if (_sourceType.isDynamicallySized())
paddingNeeded = _padToWordBoundaries && ((baseSize % 32) != 0);
else
paddingNeeded = _padToWordBoundaries && (((_sourceType.length() * baseSize) % 32) != 0);
if (paddingNeeded) if (paddingNeeded)
{ {
// stack: <target> <size> // stack: <target> <size>
@ -455,10 +444,10 @@ void ArrayUtils::copyArrayToMemory(ArrayType const& _sourceType, bool _padToWord
m_context.appendJumpTo(loopEnd); m_context.appendJumpTo(loopEnd);
m_context << longByteArray; m_context << longByteArray;
} }
// compute memory end offset else
if (baseSize > 1)
// convert length to memory size // convert length to memory size
m_context << u256(baseSize) << Instruction::MUL; m_context << _sourceType.baseType()->memoryHeadSize() << Instruction::MUL;
m_context << Instruction::DUP3 << Instruction::ADD << Instruction::SWAP2; m_context << Instruction::DUP3 << Instruction::ADD << Instruction::SWAP2;
if (_sourceType.isDynamicallySized()) if (_sourceType.isDynamicallySized())
{ {
@ -515,7 +504,12 @@ void ArrayUtils::copyArrayToMemory(ArrayType const& _sourceType, bool _padToWord
// stack here: memory_end_offset storage_data_offset [storage_byte_offset] memory_offset // stack here: memory_end_offset storage_data_offset [storage_byte_offset] memory_offset
if (haveByteOffset) if (haveByteOffset)
m_context << Instruction::SWAP1 << Instruction::POP; m_context << Instruction::SWAP1 << Instruction::POP;
if (_padToWordBoundaries && baseSize % 32 != 0) if (!_sourceType.isByteArray())
{
solAssert(_sourceType.calldataStride() % 32 == 0, "");
solAssert(_sourceType.memoryStride() % 32 == 0, "");
}
if (_padToWordBoundaries && _sourceType.isByteArray())
{ {
// memory_end_offset - start is the actual length (we want to compute the ceil of). // memory_end_offset - start is the actual length (we want to compute the ceil of).
// memory_offset - start is its next multiple of 32, but it might be off by 32. // memory_offset - start is its next multiple of 32, but it might be off by 32.
@ -987,9 +981,9 @@ void ArrayUtils::convertLengthToSize(ArrayType const& _arrayType, bool _pad) con
if (!_arrayType.isByteArray()) if (!_arrayType.isByteArray())
{ {
if (_arrayType.location() == DataLocation::Memory) if (_arrayType.location() == DataLocation::Memory)
m_context << _arrayType.baseType()->memoryHeadSize(); m_context << _arrayType.memoryStride();
else else
m_context << _arrayType.baseType()->calldataEncodedSize(); m_context << _arrayType.calldataStride();
m_context << Instruction::MUL; m_context << Instruction::MUL;
} }
else if (_pad) else if (_pad)
@ -1067,10 +1061,7 @@ void ArrayUtils::accessIndex(ArrayType const& _arrayType, bool _doBoundsCheck, b
case DataLocation::CallData: case DataLocation::CallData:
if (!_arrayType.isByteArray()) if (!_arrayType.isByteArray())
{ {
if (_arrayType.baseType()->isDynamicallyEncoded()) m_context << _arrayType.calldataStride();
m_context << u256(0x20);
else
m_context << _arrayType.baseType()->calldataEncodedSize();
m_context << Instruction::MUL; m_context << Instruction::MUL;
} }
// stack: <base_ref> <index * size> // stack: <base_ref> <index * size>

View File

@ -98,16 +98,17 @@ void CompilerUtils::revertWithStringData(Type const& _argumentType)
void CompilerUtils::accessCalldataTail(Type const& _type) void CompilerUtils::accessCalldataTail(Type const& _type)
{ {
solAssert(_type.dataStoredIn(DataLocation::CallData), ""); solAssert(_type.dataStoredIn(DataLocation::CallData), "");
solAssert(_type.isDynamicallyEncoded(), "");
unsigned int baseEncodedSize = _type.calldataEncodedSize(); unsigned int tailSize = _type.calldataEncodedTailSize();
solAssert(baseEncodedSize > 1, ""); solAssert(tailSize > 1, "");
// returns the absolute offset of the tail in "base_ref" // returns the absolute offset of the tail in "base_ref"
m_context.appendInlineAssembly(Whiskers(R"({ m_context.appendInlineAssembly(Whiskers(R"({
let rel_offset_of_tail := calldataload(ptr_to_tail) let rel_offset_of_tail := calldataload(ptr_to_tail)
if iszero(slt(rel_offset_of_tail, sub(sub(calldatasize(), base_ref), sub(<neededLength>, 1)))) { revert(0, 0) } if iszero(slt(rel_offset_of_tail, sub(sub(calldatasize(), base_ref), sub(<neededLength>, 1)))) { revert(0, 0) }
base_ref := add(base_ref, rel_offset_of_tail) base_ref := add(base_ref, rel_offset_of_tail)
})")("neededLength", toCompactHexWithPrefix(baseEncodedSize)).render(), {"base_ref", "ptr_to_tail"}); })")("neededLength", toCompactHexWithPrefix(tailSize)).render(), {"base_ref", "ptr_to_tail"});
// stack layout: <absolute_offset_of_tail> <garbage> // stack layout: <absolute_offset_of_tail> <garbage>
if (!_type.isDynamicallySized()) if (!_type.isDynamicallySized())
@ -170,7 +171,7 @@ void CompilerUtils::loadFromMemoryDynamic(
solAssert(!_fromCalldata, ""); solAssert(!_fromCalldata, "");
solAssert(_padToWordBoundaries, ""); solAssert(_padToWordBoundaries, "");
if (_keepUpdatedMemoryOffset) if (_keepUpdatedMemoryOffset)
m_context << arrayType->memorySize() << Instruction::ADD; m_context << arrayType->memoryDataSize() << Instruction::ADD;
} }
else else
{ {
@ -251,7 +252,7 @@ void CompilerUtils::abiDecode(TypePointers const& _typeParameters, bool _fromMem
//@todo this does not yet support nested dynamic arrays //@todo this does not yet support nested dynamic arrays
size_t encodedSize = 0; size_t encodedSize = 0;
for (auto const& t: _typeParameters) for (auto const& t: _typeParameters)
encodedSize += t->decodingType()->calldataEncodedSize(true); encodedSize += t->decodingType()->calldataHeadSize();
m_context.appendInlineAssembly("{ if lt(len, " + to_string(encodedSize) + ") { revert(0, 0) } }", {"len"}); m_context.appendInlineAssembly("{ if lt(len, " + to_string(encodedSize) + ") { revert(0, 0) } }", {"len"});
m_context << Instruction::DUP2 << Instruction::ADD; m_context << Instruction::DUP2 << Instruction::ADD;
@ -321,7 +322,7 @@ void CompilerUtils::abiDecode(TypePointers const& _typeParameters, bool _fromMem
// Size has already been checked for this one. // Size has already been checked for this one.
moveIntoStack(2); moveIntoStack(2);
m_context << Instruction::DUP3; m_context << Instruction::DUP3;
m_context << u256(arrayType.calldataEncodedSize(true)) << Instruction::ADD; m_context << u256(arrayType.calldataHeadSize()) << Instruction::ADD;
} }
} }
else else
@ -358,7 +359,7 @@ void CompilerUtils::abiDecode(TypePointers const& _typeParameters, bool _fromMem
// size has already been checked // size has already been checked
// stack: input_end base_offset data_offset // stack: input_end base_offset data_offset
m_context << Instruction::DUP1; m_context << Instruction::DUP1;
m_context << u256(calldataType->calldataEncodedSize()) << Instruction::ADD; m_context << u256(calldataType->calldataHeadSize()) << Instruction::ADD;
} }
if (arrayType.location() == DataLocation::Memory) if (arrayType.location() == DataLocation::Memory)
{ {
@ -588,11 +589,6 @@ void CompilerUtils::zeroInitialiseMemoryArray(ArrayType const& _type)
} }
else else
{ {
// TODO: Potential optimization:
// When we create a new multi-dimensional dynamic array, each element
// is initialized to an empty array. It actually does not hurt
// to re-use exactly the same empty array for all elements. Currently,
// a new one is created each time.
auto repeat = m_context.newTag(); auto repeat = m_context.newTag();
m_context << repeat; m_context << repeat;
pushZeroValue(*_type.baseType()); pushZeroValue(*_type.baseType());
@ -1014,7 +1010,7 @@ void CompilerUtils::convertType(
{ {
CompilerUtils utils(_context); CompilerUtils utils(_context);
// stack: <source ref> // stack: <source ref>
utils.allocateMemory(typeOnStack->memorySize()); utils.allocateMemory(typeOnStack->memoryDataSize());
_context << Instruction::SWAP1 << Instruction::DUP2; _context << Instruction::SWAP1 << Instruction::DUP2;
// stack: <memory ptr> <source ref> <memory ptr> // stack: <memory ptr> <source ref> <memory ptr>
for (auto const& member: typeOnStack->members(nullptr)) for (auto const& member: typeOnStack->members(nullptr))
@ -1197,7 +1193,8 @@ void CompilerUtils::pushZeroValue(Type const& _type)
1, 1,
[type](CompilerContext& _context) { [type](CompilerContext& _context) {
CompilerUtils utils(_context); CompilerUtils utils(_context);
utils.allocateMemory(max(32u, type->calldataEncodedSize()));
utils.allocateMemory(max<u256>(32u, type->memoryDataSize()));
_context << Instruction::DUP1; _context << Instruction::DUP1;
if (auto structType = dynamic_cast<StructType const*>(type)) if (auto structType = dynamic_cast<StructType const*>(type))
@ -1364,6 +1361,8 @@ void CompilerUtils::storeStringData(bytesConstRef _data)
unsigned CompilerUtils::loadFromMemoryHelper(Type const& _type, bool _fromCalldata, bool _padToWords) unsigned CompilerUtils::loadFromMemoryHelper(Type const& _type, bool _fromCalldata, bool _padToWords)
{ {
solAssert(_type.isValueType(), "");
unsigned numBytes = _type.calldataEncodedSize(_padToWords); unsigned numBytes = _type.calldataEncodedSize(_padToWords);
bool isExternalFunctionType = false; bool isExternalFunctionType = false;
if (auto const* funType = dynamic_cast<FunctionType const*>(&_type)) if (auto const* funType = dynamic_cast<FunctionType const*>(&_type))
@ -1436,6 +1435,8 @@ unsigned CompilerUtils::prepareMemoryStore(Type const& _type, bool _padToWords)
"Memory store of types with stack size != 1 not allowed (Type: " + _type.toString(true) + ")." "Memory store of types with stack size != 1 not allowed (Type: " + _type.toString(true) + ")."
); );
solAssert(!_type.isDynamicallyEncoded(), "");
unsigned numBytes = _type.calldataEncodedSize(_padToWords); unsigned numBytes = _type.calldataEncodedSize(_padToWords);
solAssert( solAssert(

View File

@ -317,7 +317,7 @@ bool ExpressionCompiler::visit(TupleExpression const& _tuple)
ArrayType const& arrayType = dynamic_cast<ArrayType const&>(*_tuple.annotation().type); ArrayType const& arrayType = dynamic_cast<ArrayType const&>(*_tuple.annotation().type);
solAssert(!arrayType.isDynamicallySized(), "Cannot create dynamically sized inline array."); solAssert(!arrayType.isDynamicallySized(), "Cannot create dynamically sized inline array.");
utils().allocateMemory(max(u256(32u), arrayType.memorySize())); utils().allocateMemory(max(u256(32u), arrayType.memoryDataSize()));
m_context << Instruction::DUP1; m_context << Instruction::DUP1;
for (auto const& component: _tuple.components()) for (auto const& component: _tuple.components())
@ -526,7 +526,7 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall)
TypeType const& type = dynamic_cast<TypeType const&>(*_functionCall.expression().annotation().type); TypeType const& type = dynamic_cast<TypeType const&>(*_functionCall.expression().annotation().type);
auto const& structType = dynamic_cast<StructType const&>(*type.actualType()); auto const& structType = dynamic_cast<StructType const&>(*type.actualType());
utils().allocateMemory(max(u256(32u), structType.memorySize())); utils().allocateMemory(max(u256(32u), structType.memoryDataSize()));
m_context << Instruction::DUP1; m_context << Instruction::DUP1;
for (unsigned i = 0; i < arguments.size(); ++i) for (unsigned i = 0; i < arguments.size(); ++i)

View File

@ -652,11 +652,11 @@ string YulUtilFunctions::arrayConvertLengthToSize(ArrayType const& _type)
<?byteArray> <?byteArray>
size := length size := length
<!byteArray> <!byteArray>
size := <mul>(length, <elementSize>) size := <mul>(length, <stride>)
</byteArray> </byteArray>
})") })")
("functionName", functionName) ("functionName", functionName)
("elementSize", to_string(_type.location() == DataLocation::Memory ? baseType.memoryHeadSize() : baseType.calldataEncodedSize())) ("stride", to_string(_type.location() == DataLocation::Memory ? _type.memoryStride() : _type.calldataStride()))
("byteArray", _type.isByteArray()) ("byteArray", _type.isByteArray())
("mul", overflowCheckedIntMulFunction(*TypeProvider::uint256())) ("mul", overflowCheckedIntMulFunction(*TypeProvider::uint256()))
.render(); .render();
@ -812,10 +812,7 @@ string YulUtilFunctions::nextArrayElementFunction(ArrayType const& _type)
} }
case DataLocation::CallData: case DataLocation::CallData:
{ {
u256 size = u256 size = _type.calldataStride();
_type.baseType()->isDynamicallyEncoded() ?
32 :
_type.baseType()->calldataEncodedSize();
solAssert(size >= 32 && size % 32 == 0, ""); solAssert(size >= 32 && size % 32 == 0, "");
templ("advance", toCompactHexWithPrefix(size)); templ("advance", toCompactHexWithPrefix(size));
break; break;

View File

@ -0,0 +1,11 @@
pragma experimental ABIEncoderV2;
contract C {
function f(uint256[][2][] calldata x) external returns (uint256) {
x[0]; // trigger bounds checks
return 23;
}
}
// ----
// f(uint256[][2][]): 0x20, 0x01, 0x20, 0x40, 0x60, 0x00, 0x00 -> 23 # this is the common encoding for x.length == 1 && x[0][0].length == 0 && x[0][1].length == 0 #
// f(uint256[][2][]): 0x20, 0x01, 0x20, 0x00, 0x00 -> 23 # exotic, but still valid encoding #
// f(uint256[][2][]): 0x20, 0x01, 0x20, 0x00 -> FAILURE # invalid (too short) encoding, but no failure due to this PR #

View File

@ -0,0 +1,13 @@
pragma experimental ABIEncoderV2;
contract C {
function f(uint256[][2][] calldata x) external returns (uint256) {
return 42;
}
function g(uint256[][2][] calldata x) external returns (uint256) {
return this.f(x);
}
}
// ----
// g(uint256[][2][]): 0x20, 0x01, 0x20, 0x40, 0x60, 0x00, 0x00 -> 42
// g(uint256[][2][]): 0x20, 0x01, 0x20, 0x00, 0x00 -> 42
// g(uint256[][2][]): 0x20, 0x01, 0x20, 0x00 -> FAILURE

View File

@ -0,0 +1,23 @@
pragma experimental ABIEncoderV2;
contract C {
struct A {
uint256 a;
uint256[] b;
}
struct B {
A a;
uint256 b;
}
function g(B calldata b) external pure returns(uint256) {
return b.b;
}
function f() public view returns(uint256, uint256) {
uint256[] memory arr = new uint256[](20);
arr[0] = 31; arr[2] = 84;
B memory b = B(A(420, arr), 11);
return (b.b, this.g(b));
}
}
// ----
// f() -> 11, 11