mirror of
				https://github.com/ethereum/solidity
				synced 2023-10-03 13:03:40 +00:00 
			
		
		
		
	Merge pull request #132 from chriseth/tupleExpression
Tuple expressions and destructuring assignments
This commit is contained in:
		
						commit
						52eaa477d4
					
				| @ -1067,6 +1067,30 @@ private: | ||||
| 	ASTPointer<Expression> m_rightHandSide; | ||||
| }; | ||||
| 
 | ||||
| /**
 | ||||
|  * Tuple or just parenthesized expression. | ||||
|  * Examples: (1, 2), (x,), (x), () | ||||
|  * Individual components might be empty shared pointers (as in the second example). | ||||
|  * The respective types in lvalue context are: 2-tuple, 2-tuple (with wildcard), type of x, 0-tuple | ||||
|  * Not in lvalue context: 2-tuple, _1_-tuple, type of x, 0-tuple. | ||||
|  */ | ||||
| class TupleExpression: public Expression | ||||
| { | ||||
| public: | ||||
| 	TupleExpression( | ||||
| 		SourceLocation const& _location, | ||||
| 		std::vector<ASTPointer<Expression>> const& _components | ||||
| 	): | ||||
| 		Expression(_location), m_components(_components) {} | ||||
| 	virtual void accept(ASTVisitor& _visitor) override; | ||||
| 	virtual void accept(ASTConstVisitor& _visitor) const override; | ||||
| 
 | ||||
| 	std::vector<ASTPointer<Expression>> const& components() const { return m_components; } | ||||
| 
 | ||||
| private: | ||||
| 	std::vector<ASTPointer<Expression>> m_components; | ||||
| }; | ||||
| 
 | ||||
| /**
 | ||||
|  * Operation involving a unary operator, pre- or postfix. | ||||
|  * Examples: ++i, delete x or !true | ||||
|  | ||||
| @ -69,6 +69,7 @@ class VariableDeclarationStatement; | ||||
| class ExpressionStatement; | ||||
| class Expression; | ||||
| class Assignment; | ||||
| class TupleExpression; | ||||
| class UnaryOperation; | ||||
| class BinaryOperation; | ||||
| class FunctionCall; | ||||
|  | ||||
| @ -226,6 +226,12 @@ bool ASTJsonConverter::visit(Assignment const& _node) | ||||
| 	return true; | ||||
| } | ||||
| 
 | ||||
| bool ASTJsonConverter::visit(TupleExpression const&) | ||||
| { | ||||
| 	addJsonNode("TupleExpression",{}, true); | ||||
| 	return true; | ||||
| } | ||||
| 
 | ||||
| bool ASTJsonConverter::visit(UnaryOperation const& _node) | ||||
| { | ||||
| 	addJsonNode("UnaryOperation", | ||||
| @ -396,6 +402,11 @@ void ASTJsonConverter::endVisit(Assignment const&) | ||||
| 	goUp(); | ||||
| } | ||||
| 
 | ||||
| void ASTJsonConverter::endVisit(TupleExpression const&) | ||||
| { | ||||
| 	goUp(); | ||||
| } | ||||
| 
 | ||||
| void ASTJsonConverter::endVisit(UnaryOperation const&) | ||||
| { | ||||
| 	goUp(); | ||||
|  | ||||
| @ -68,6 +68,7 @@ public: | ||||
| 	bool visit(VariableDeclarationStatement const& _node) override; | ||||
| 	bool visit(ExpressionStatement const& _node) override; | ||||
| 	bool visit(Assignment const& _node) override; | ||||
| 	bool visit(TupleExpression const& _node) override; | ||||
| 	bool visit(UnaryOperation const& _node) override; | ||||
| 	bool visit(BinaryOperation const& _node) override; | ||||
| 	bool visit(FunctionCall const& _node) override; | ||||
| @ -99,6 +100,7 @@ public: | ||||
| 	void endVisit(VariableDeclarationStatement const&) override; | ||||
| 	void endVisit(ExpressionStatement const&) override; | ||||
| 	void endVisit(Assignment const&) override; | ||||
| 	void endVisit(TupleExpression const&) override; | ||||
| 	void endVisit(UnaryOperation const&) override; | ||||
| 	void endVisit(BinaryOperation const&) override; | ||||
| 	void endVisit(FunctionCall const&) override; | ||||
|  | ||||
| @ -256,6 +256,14 @@ bool ASTPrinter::visit(Assignment const& _node) | ||||
| 	return goDeeper(); | ||||
| } | ||||
| 
 | ||||
| bool ASTPrinter::visit(TupleExpression const& _node) | ||||
| { | ||||
| 	writeLine(string("TupleExpression")); | ||||
| 	printType(_node); | ||||
| 	printSourcePart(_node); | ||||
| 	return goDeeper(); | ||||
| } | ||||
| 
 | ||||
| bool ASTPrinter::visit(UnaryOperation const& _node) | ||||
| { | ||||
| 	writeLine(string("UnaryOperation (") + (_node.isPrefixOperation() ? "prefix" : "postfix") + | ||||
| @ -477,6 +485,11 @@ void ASTPrinter::endVisit(Assignment const&) | ||||
| 	m_indentation--; | ||||
| } | ||||
| 
 | ||||
| void ASTPrinter::endVisit(TupleExpression const&) | ||||
| { | ||||
| 	m_indentation--; | ||||
| } | ||||
| 
 | ||||
| void ASTPrinter::endVisit(UnaryOperation const&) | ||||
| { | ||||
| 	m_indentation--; | ||||
|  | ||||
| @ -76,6 +76,7 @@ public: | ||||
| 	bool visit(VariableDeclarationStatement const& _node) override; | ||||
| 	bool visit(ExpressionStatement const& _node) override; | ||||
| 	bool visit(Assignment const& _node) override; | ||||
| 	bool visit(TupleExpression const& _node) override; | ||||
| 	bool visit(UnaryOperation const& _node) override; | ||||
| 	bool visit(BinaryOperation const& _node) override; | ||||
| 	bool visit(FunctionCall const& _node) override; | ||||
| @ -115,6 +116,7 @@ public: | ||||
| 	void endVisit(VariableDeclarationStatement const&) override; | ||||
| 	void endVisit(ExpressionStatement const&) override; | ||||
| 	void endVisit(Assignment const&) override; | ||||
| 	void endVisit(TupleExpression const&) override; | ||||
| 	void endVisit(UnaryOperation const&) override; | ||||
| 	void endVisit(BinaryOperation const&) override; | ||||
| 	void endVisit(FunctionCall const&) override; | ||||
|  | ||||
| @ -73,6 +73,7 @@ public: | ||||
| 	virtual bool visit(VariableDeclarationStatement& _node) { return visitNode(_node); } | ||||
| 	virtual bool visit(ExpressionStatement& _node) { return visitNode(_node); } | ||||
| 	virtual bool visit(Assignment& _node) { return visitNode(_node); } | ||||
| 	virtual bool visit(TupleExpression& _node) { return visitNode(_node); } | ||||
| 	virtual bool visit(UnaryOperation& _node) { return visitNode(_node); } | ||||
| 	virtual bool visit(BinaryOperation& _node) { return visitNode(_node); } | ||||
| 	virtual bool visit(FunctionCall& _node) { return visitNode(_node); } | ||||
| @ -113,6 +114,7 @@ public: | ||||
| 	virtual void endVisit(VariableDeclarationStatement& _node) { endVisitNode(_node); } | ||||
| 	virtual void endVisit(ExpressionStatement& _node) { endVisitNode(_node); } | ||||
| 	virtual void endVisit(Assignment& _node) { endVisitNode(_node); } | ||||
| 	virtual void endVisit(TupleExpression& _node) { endVisitNode(_node); } | ||||
| 	virtual void endVisit(UnaryOperation& _node) { endVisitNode(_node); } | ||||
| 	virtual void endVisit(BinaryOperation& _node) { endVisitNode(_node); } | ||||
| 	virtual void endVisit(FunctionCall& _node) { endVisitNode(_node); } | ||||
| @ -165,6 +167,7 @@ public: | ||||
| 	virtual bool visit(VariableDeclarationStatement const& _node) { return visitNode(_node); } | ||||
| 	virtual bool visit(ExpressionStatement const& _node) { return visitNode(_node); } | ||||
| 	virtual bool visit(Assignment const& _node) { return visitNode(_node); } | ||||
| 	virtual bool visit(TupleExpression const& _node) { return visitNode(_node); } | ||||
| 	virtual bool visit(UnaryOperation const& _node) { return visitNode(_node); } | ||||
| 	virtual bool visit(BinaryOperation const& _node) { return visitNode(_node); } | ||||
| 	virtual bool visit(FunctionCall const& _node) { return visitNode(_node); } | ||||
| @ -205,6 +208,7 @@ public: | ||||
| 	virtual void endVisit(VariableDeclarationStatement const& _node) { endVisitNode(_node); } | ||||
| 	virtual void endVisit(ExpressionStatement const& _node) { endVisitNode(_node); } | ||||
| 	virtual void endVisit(Assignment const& _node) { endVisitNode(_node); } | ||||
| 	virtual void endVisit(TupleExpression const& _node) { endVisitNode(_node); } | ||||
| 	virtual void endVisit(UnaryOperation const& _node) { endVisitNode(_node); } | ||||
| 	virtual void endVisit(BinaryOperation const& _node) { endVisitNode(_node); } | ||||
| 	virtual void endVisit(FunctionCall const& _node) { endVisitNode(_node); } | ||||
|  | ||||
| @ -559,6 +559,24 @@ void Assignment::accept(ASTConstVisitor& _visitor) const | ||||
| 	_visitor.endVisit(*this); | ||||
| } | ||||
| 
 | ||||
| void TupleExpression::accept(ASTVisitor& _visitor) | ||||
| { | ||||
| 	if (_visitor.visit(*this)) | ||||
| 		for (auto const& component: m_components) | ||||
| 			if (component) | ||||
| 				component->accept(_visitor); | ||||
| 	_visitor.endVisit(*this); | ||||
| } | ||||
| 
 | ||||
| void TupleExpression::accept(ASTConstVisitor& _visitor) const | ||||
| { | ||||
| 	if (_visitor.visit(*this)) | ||||
| 		for (auto const& component: m_components) | ||||
| 			if (component) | ||||
| 				component->accept(_visitor); | ||||
| 	_visitor.endVisit(*this); | ||||
| } | ||||
| 
 | ||||
| void UnaryOperation::accept(ASTVisitor& _visitor) | ||||
| { | ||||
| 	if (_visitor.visit(*this)) | ||||
|  | ||||
| @ -597,13 +597,20 @@ bool Compiler::visit(Break const& _breakStatement) | ||||
| bool Compiler::visit(Return const& _return) | ||||
| { | ||||
| 	CompilerContext::LocationSetter locationSetter(m_context, _return); | ||||
| 	//@todo modifications are needed to make this work with functions returning multiple values
 | ||||
| 	if (Expression const* expression = _return.expression()) | ||||
| 	{ | ||||
| 		solAssert(_return.annotation().functionReturnParameters, "Invalid return parameters pointer."); | ||||
| 		VariableDeclaration const& firstVariable = *_return.annotation().functionReturnParameters->parameters().front(); | ||||
| 		compileExpression(*expression, firstVariable.annotation().type); | ||||
| 		CompilerUtils(m_context).moveToStackVariable(firstVariable); | ||||
| 		vector<ASTPointer<VariableDeclaration>> const& returnParameters = | ||||
| 			_return.annotation().functionReturnParameters->parameters(); | ||||
| 		TypePointers types; | ||||
| 		for (auto const& retVariable: returnParameters) | ||||
| 			types.push_back(retVariable->annotation().type); | ||||
| 
 | ||||
| 		TypePointer expectedType = types.size() == 1 ? types.front() : make_shared<TupleType>(types); | ||||
| 		compileExpression(*expression, expectedType); | ||||
| 
 | ||||
| 		for (auto const& retVariable: boost::adaptors::reverse(returnParameters)) | ||||
| 			CompilerUtils(m_context).moveToStackVariable(*retVariable); | ||||
| 	} | ||||
| 	for (unsigned i = 0; i < m_stackCleanupForReturn; ++i) | ||||
| 		m_context << eth::Instruction::POP; | ||||
| @ -637,6 +644,7 @@ bool Compiler::visit(VariableDeclarationStatement const& _variableDeclarationSta | ||||
| 		for (size_t i = 0; i < assignments.size(); ++i) | ||||
| 		{ | ||||
| 			size_t j = assignments.size() - i - 1; | ||||
| 			solAssert(!!valueTypes[j], ""); | ||||
| 			VariableDeclaration const* varDecl = assignments[j]; | ||||
| 			if (!varDecl) | ||||
| 				utils.popStackElement(*valueTypes[j]); | ||||
|  | ||||
| @ -550,6 +550,61 @@ void CompilerUtils::convertType(Type const& _typeOnStack, Type const& _targetTyp | ||||
| 		} | ||||
| 		break; | ||||
| 	} | ||||
| 	case Type::Category::Tuple: | ||||
| 	{ | ||||
| 		TupleType const& sourceTuple = dynamic_cast<TupleType const&>(_typeOnStack); | ||||
| 		TupleType const& targetTuple = dynamic_cast<TupleType const&>(_targetType); | ||||
| 		// fillRight: remove excess values at right side, !fillRight: remove eccess values at left side
 | ||||
| 		bool fillRight = !targetTuple.components().empty() && ( | ||||
| 			!targetTuple.components().back() || | ||||
| 			targetTuple.components().front() | ||||
| 		); | ||||
| 		unsigned depth = sourceTuple.sizeOnStack(); | ||||
| 		for (size_t i = 0; i < sourceTuple.components().size(); ++i) | ||||
| 		{ | ||||
| 			TypePointer sourceType = sourceTuple.components()[i]; | ||||
| 			TypePointer targetType; | ||||
| 			if (fillRight && i < targetTuple.components().size()) | ||||
| 				targetType = targetTuple.components()[i]; | ||||
| 			else if (!fillRight && targetTuple.components().size() + i >= sourceTuple.components().size()) | ||||
| 				targetType = targetTuple.components()[targetTuple.components().size() - (sourceTuple.components().size() - i)]; | ||||
| 			if (!sourceType) | ||||
| 			{ | ||||
| 				solAssert(!targetType, ""); | ||||
| 				continue; | ||||
| 			} | ||||
| 			unsigned sourceSize = sourceType->sizeOnStack(); | ||||
| 			unsigned targetSize = targetType ? targetType->sizeOnStack() : 0; | ||||
| 			if (!targetType || *sourceType != *targetType || _cleanupNeeded) | ||||
| 			{ | ||||
| 				if (targetType) | ||||
| 				{ | ||||
| 					if (sourceSize > 0) | ||||
| 						copyToStackTop(depth, sourceSize); | ||||
| 					convertType(*sourceType, *targetType, _cleanupNeeded); | ||||
| 				} | ||||
| 				if (sourceSize > 0 || targetSize > 0) | ||||
| 				{ | ||||
| 					// Move it back into its place.
 | ||||
| 					for (unsigned j = 0; j < min(sourceSize, targetSize); ++j) | ||||
| 						m_context << | ||||
| 							eth::swapInstruction(depth + targetSize - sourceSize) << | ||||
| 							eth::Instruction::POP; | ||||
| 					// Value shrank
 | ||||
| 					for (unsigned j = targetSize; j < sourceSize; ++j) | ||||
| 					{ | ||||
| 						moveToStackTop(depth - 1, 1); | ||||
| 						m_context << eth::Instruction::POP; | ||||
| 					} | ||||
| 					// Value grew
 | ||||
| 					if (targetSize > sourceSize) | ||||
| 						moveIntoStack(depth + targetSize - sourceSize, targetSize - sourceSize); | ||||
| 				} | ||||
| 			} | ||||
| 			depth -= sourceSize; | ||||
| 		} | ||||
| 		break; | ||||
| 	} | ||||
| 	default: | ||||
| 		// All other types should not be convertible to non-equal types.
 | ||||
| 		solAssert(_typeOnStack == _targetType, "Invalid type conversion requested."); | ||||
| @ -631,18 +686,20 @@ void CompilerUtils::copyToStackTop(unsigned _stackDepth, unsigned _itemSize) | ||||
| 		m_context << eth::dupInstruction(_stackDepth); | ||||
| } | ||||
| 
 | ||||
| void CompilerUtils::moveToStackTop(unsigned _stackDepth) | ||||
| void CompilerUtils::moveToStackTop(unsigned _stackDepth, unsigned _itemSize) | ||||
| { | ||||
| 	solAssert(_stackDepth <= 15, "Stack too deep, try removing local variables."); | ||||
| 	for (unsigned i = 0; i < _stackDepth; ++i) | ||||
| 		m_context << eth::swapInstruction(1 + i); | ||||
| 	for (unsigned j = 0; j < _itemSize; ++j) | ||||
| 		for (unsigned i = 0; i < _stackDepth + _itemSize - 1; ++i) | ||||
| 			m_context << eth::swapInstruction(1 + i); | ||||
| } | ||||
| 
 | ||||
| void CompilerUtils::moveIntoStack(unsigned _stackDepth) | ||||
| void CompilerUtils::moveIntoStack(unsigned _stackDepth, unsigned _itemSize) | ||||
| { | ||||
| 	solAssert(_stackDepth <= 16, "Stack too deep, try removing local variables."); | ||||
| 	for (unsigned i = _stackDepth; i > 0; --i) | ||||
| 		m_context << eth::swapInstruction(i); | ||||
| 	for (unsigned j = 0; j < _itemSize; ++j) | ||||
| 		for (unsigned i = _stackDepth; i > 0; --i) | ||||
| 			m_context << eth::swapInstruction(i + _itemSize - 1); | ||||
| } | ||||
| 
 | ||||
| void CompilerUtils::popStackElement(Type const& _type) | ||||
|  | ||||
| @ -124,10 +124,11 @@ public: | ||||
| 	/// Copies an item that occupies @a _itemSize stack slots from a stack depth of @a _stackDepth
 | ||||
| 	/// to the top of the stack.
 | ||||
| 	void copyToStackTop(unsigned _stackDepth, unsigned _itemSize); | ||||
| 	/// Moves a single stack element (with _stackDepth items on top of it) to the top of the stack.
 | ||||
| 	void moveToStackTop(unsigned _stackDepth); | ||||
| 	/// Moves a single stack element past @a _stackDepth other stack elements
 | ||||
| 	void moveIntoStack(unsigned _stackDepth); | ||||
| 	/// Moves an item that occupies @a _itemSize stack slots and has items occupying @a _stackDepth
 | ||||
| 	/// slots above it to the top of the stack.
 | ||||
| 	void moveToStackTop(unsigned _stackDepth, unsigned _itemSize = 1); | ||||
| 	/// Moves @a _itemSize elements past @a _stackDepth other stack elements
 | ||||
| 	void moveIntoStack(unsigned _stackDepth, unsigned _itemSize = 1); | ||||
| 	/// Removes the current value from the top of the stack.
 | ||||
| 	void popStackElement(Type const& _type); | ||||
| 	/// Removes element from the top of the stack _amount times.
 | ||||
|  | ||||
| @ -179,17 +179,12 @@ bool ExpressionCompiler::visit(Assignment const& _assignment) | ||||
| { | ||||
| 	CompilerContext::LocationSetter locationSetter(m_context, _assignment); | ||||
| 	_assignment.rightHandSide().accept(*this); | ||||
| 	TypePointer type = _assignment.rightHandSide().annotation().type; | ||||
| 	if (!_assignment.annotation().type->dataStoredIn(DataLocation::Storage)) | ||||
| 	{ | ||||
| 		utils().convertType(*type, *_assignment.annotation().type); | ||||
| 		type = _assignment.annotation().type; | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		utils().convertType(*type, *type->mobileType()); | ||||
| 		type = type->mobileType(); | ||||
| 	} | ||||
| 	// Perform some conversion already. This will convert storage types to memory and literals
 | ||||
| 	// to their actual type, but will not convert e.g. memory to storage.
 | ||||
| 	TypePointer type = _assignment.rightHandSide().annotation().type->closestTemporaryType( | ||||
| 		_assignment.leftHandSide().annotation().type | ||||
| 	); | ||||
| 	utils().convertType(*_assignment.rightHandSide().annotation().type, *type); | ||||
| 
 | ||||
| 	_assignment.leftHandSide().accept(*this); | ||||
| 	solAssert(!!m_currentLValue, "LValue not retrieved."); | ||||
| @ -221,6 +216,26 @@ bool ExpressionCompiler::visit(Assignment const& _assignment) | ||||
| 	return false; | ||||
| } | ||||
| 
 | ||||
| bool ExpressionCompiler::visit(TupleExpression const& _tuple) | ||||
| { | ||||
| 	vector<unique_ptr<LValue>> lvalues; | ||||
| 	for (auto const& component: _tuple.components()) | ||||
| 		if (component) | ||||
| 		{ | ||||
| 			component->accept(*this); | ||||
| 			if (_tuple.annotation().lValueRequested) | ||||
| 			{ | ||||
| 				solAssert(!!m_currentLValue, ""); | ||||
| 				lvalues.push_back(move(m_currentLValue)); | ||||
| 			} | ||||
| 		} | ||||
| 		else if (_tuple.annotation().lValueRequested) | ||||
| 			lvalues.push_back(unique_ptr<LValue>()); | ||||
| 	if (_tuple.annotation().lValueRequested) | ||||
| 		m_currentLValue.reset(new TupleObject(m_context, move(lvalues))); | ||||
| 	return false; | ||||
| } | ||||
| 
 | ||||
| bool ExpressionCompiler::visit(UnaryOperation const& _unaryOperation) | ||||
| { | ||||
| 	CompilerContext::LocationSetter locationSetter(m_context, _unaryOperation); | ||||
| @ -649,9 +664,8 @@ bool ExpressionCompiler::visit(FunctionCall const& _functionCall) | ||||
| 			// stack: newLength storageSlot slotOffset
 | ||||
| 			arguments[0]->accept(*this); | ||||
| 			// stack: newLength storageSlot slotOffset argValue
 | ||||
| 			TypePointer type = arguments[0]->annotation().type; | ||||
| 			utils().convertType(*type, *arrayType->baseType()); | ||||
| 			type = arrayType->baseType(); | ||||
| 			TypePointer type = arguments[0]->annotation().type->closestTemporaryType(arrayType->baseType()); | ||||
| 			utils().convertType(*arguments[0]->annotation().type, *type); | ||||
| 			utils().moveToStackTop(1 + type->sizeOnStack()); | ||||
| 			utils().moveToStackTop(1 + type->sizeOnStack()); | ||||
| 			// stack: newLength argValue storageSlot slotOffset
 | ||||
|  | ||||
| @ -72,6 +72,7 @@ public: | ||||
| 
 | ||||
| private: | ||||
| 	virtual bool visit(Assignment const& _assignment) override; | ||||
| 	virtual bool visit(TupleExpression const& _tuple) override; | ||||
| 	virtual bool visit(UnaryOperation const& _unaryOperation) override; | ||||
| 	virtual bool visit(BinaryOperation const& _binaryOperation) override; | ||||
| 	virtual bool visit(FunctionCall const& _functionCall) override; | ||||
|  | ||||
| @ -32,9 +32,9 @@ using namespace solidity; | ||||
| 
 | ||||
| 
 | ||||
| StackVariable::StackVariable(CompilerContext& _compilerContext, VariableDeclaration const& _declaration): | ||||
| 	LValue(_compilerContext, *_declaration.annotation().type), | ||||
| 	LValue(_compilerContext, _declaration.annotation().type.get()), | ||||
| 	m_baseStackOffset(m_context.baseStackOffsetOfVariable(_declaration)), | ||||
| 	m_size(m_dataType.sizeOnStack()) | ||||
| 	m_size(m_dataType->sizeOnStack()) | ||||
| { | ||||
| } | ||||
| 
 | ||||
| @ -70,23 +70,23 @@ void StackVariable::storeValue(Type const&, SourceLocation const& _location, boo | ||||
| 
 | ||||
| void StackVariable::setToZero(SourceLocation const& _location, bool) const | ||||
| { | ||||
| 	CompilerUtils(m_context).pushZeroValue(m_dataType); | ||||
| 	storeValue(m_dataType, _location, true); | ||||
| 	CompilerUtils(m_context).pushZeroValue(*m_dataType); | ||||
| 	storeValue(*m_dataType, _location, true); | ||||
| } | ||||
| 
 | ||||
| MemoryItem::MemoryItem(CompilerContext& _compilerContext, Type const& _type, bool _padded): | ||||
| 	LValue(_compilerContext, _type), | ||||
| 	LValue(_compilerContext, &_type), | ||||
| 	m_padded(_padded) | ||||
| { | ||||
| } | ||||
| 
 | ||||
| void MemoryItem::retrieveValue(SourceLocation const&, bool _remove) const | ||||
| { | ||||
| 	if (m_dataType.isValueType()) | ||||
| 	if (m_dataType->isValueType()) | ||||
| 	{ | ||||
| 		if (!_remove) | ||||
| 			m_context << eth::Instruction::DUP1; | ||||
| 		CompilerUtils(m_context).loadFromMemoryDynamic(m_dataType, false, m_padded, false); | ||||
| 		CompilerUtils(m_context).loadFromMemoryDynamic(*m_dataType, false, m_padded, false); | ||||
| 	} | ||||
| 	else | ||||
| 		m_context << eth::Instruction::MLOAD; | ||||
| @ -95,24 +95,24 @@ void MemoryItem::retrieveValue(SourceLocation const&, bool _remove) const | ||||
| void MemoryItem::storeValue(Type const& _sourceType, SourceLocation const&, bool _move) const | ||||
| { | ||||
| 	CompilerUtils utils(m_context); | ||||
| 	if (m_dataType.isValueType()) | ||||
| 	if (m_dataType->isValueType()) | ||||
| 	{ | ||||
| 		solAssert(_sourceType.isValueType(), ""); | ||||
| 		utils.moveIntoStack(_sourceType.sizeOnStack()); | ||||
| 		utils.convertType(_sourceType, m_dataType, true); | ||||
| 		utils.convertType(_sourceType, *m_dataType, true); | ||||
| 		if (!_move) | ||||
| 		{ | ||||
| 			utils.moveToStackTop(m_dataType.sizeOnStack()); | ||||
| 			utils.copyToStackTop(2, m_dataType.sizeOnStack()); | ||||
| 			utils.moveToStackTop(m_dataType->sizeOnStack()); | ||||
| 			utils.copyToStackTop(2, m_dataType->sizeOnStack()); | ||||
| 		} | ||||
| 		utils.storeInMemoryDynamic(m_dataType, m_padded); | ||||
| 		utils.storeInMemoryDynamic(*m_dataType, m_padded); | ||||
| 		m_context << eth::Instruction::POP; | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		solAssert(_sourceType == m_dataType, "Conversion not implemented for assignment to memory."); | ||||
| 		solAssert(_sourceType == *m_dataType, "Conversion not implemented for assignment to memory."); | ||||
| 
 | ||||
| 		solAssert(m_dataType.sizeOnStack() == 1, ""); | ||||
| 		solAssert(m_dataType->sizeOnStack() == 1, ""); | ||||
| 		if (!_move) | ||||
| 			m_context << eth::Instruction::DUP2 << eth::Instruction::SWAP1; | ||||
| 		// stack: [value] value lvalue
 | ||||
| @ -126,8 +126,8 @@ void MemoryItem::setToZero(SourceLocation const&, bool _removeReference) const | ||||
| 	CompilerUtils utils(m_context); | ||||
| 	if (!_removeReference) | ||||
| 		m_context << eth::Instruction::DUP1; | ||||
| 	utils.pushZeroValue(m_dataType); | ||||
| 	utils.storeInMemoryDynamic(m_dataType, m_padded); | ||||
| 	utils.pushZeroValue(*m_dataType); | ||||
| 	utils.storeInMemoryDynamic(*m_dataType, m_padded); | ||||
| 	m_context << eth::Instruction::POP; | ||||
| } | ||||
| 
 | ||||
| @ -139,21 +139,21 @@ StorageItem::StorageItem(CompilerContext& _compilerContext, VariableDeclaration | ||||
| } | ||||
| 
 | ||||
| StorageItem::StorageItem(CompilerContext& _compilerContext, Type const& _type): | ||||
| 	LValue(_compilerContext, _type) | ||||
| 	LValue(_compilerContext, &_type) | ||||
| { | ||||
| 	if (m_dataType.isValueType()) | ||||
| 	if (m_dataType->isValueType()) | ||||
| 	{ | ||||
| 		solAssert(m_dataType.storageSize() == m_dataType.sizeOnStack(), ""); | ||||
| 		solAssert(m_dataType.storageSize() == 1, "Invalid storage size."); | ||||
| 		solAssert(m_dataType->storageSize() == m_dataType->sizeOnStack(), ""); | ||||
| 		solAssert(m_dataType->storageSize() == 1, "Invalid storage size."); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| void StorageItem::retrieveValue(SourceLocation const&, bool _remove) const | ||||
| { | ||||
| 	// stack: storage_key storage_offset
 | ||||
| 	if (!m_dataType.isValueType()) | ||||
| 	if (!m_dataType->isValueType()) | ||||
| 	{ | ||||
| 		solAssert(m_dataType.sizeOnStack() == 1, "Invalid storage ref size."); | ||||
| 		solAssert(m_dataType->sizeOnStack() == 1, "Invalid storage ref size."); | ||||
| 		if (_remove) | ||||
| 			m_context << eth::Instruction::POP; // remove byte offset
 | ||||
| 		else | ||||
| @ -162,22 +162,22 @@ void StorageItem::retrieveValue(SourceLocation const&, bool _remove) const | ||||
| 	} | ||||
| 	if (!_remove) | ||||
| 		CompilerUtils(m_context).copyToStackTop(sizeOnStack(), sizeOnStack()); | ||||
| 	if (m_dataType.storageBytes() == 32) | ||||
| 	if (m_dataType->storageBytes() == 32) | ||||
| 		m_context << eth::Instruction::POP << eth::Instruction::SLOAD; | ||||
| 	else | ||||
| 	{ | ||||
| 		m_context | ||||
| 			<< eth::Instruction::SWAP1 << eth::Instruction::SLOAD << eth::Instruction::SWAP1 | ||||
| 			<< u256(0x100) << eth::Instruction::EXP << eth::Instruction::SWAP1 << eth::Instruction::DIV; | ||||
| 		if (m_dataType.category() == Type::Category::FixedBytes) | ||||
| 			m_context << (u256(0x1) << (256 - 8 * m_dataType.storageBytes())) << eth::Instruction::MUL; | ||||
| 		if (m_dataType->category() == Type::Category::FixedBytes) | ||||
| 			m_context << (u256(0x1) << (256 - 8 * m_dataType->storageBytes())) << eth::Instruction::MUL; | ||||
| 		else if ( | ||||
| 			m_dataType.category() == Type::Category::Integer && | ||||
| 			dynamic_cast<IntegerType const&>(m_dataType).isSigned() | ||||
| 			m_dataType->category() == Type::Category::Integer && | ||||
| 			dynamic_cast<IntegerType const&>(*m_dataType).isSigned() | ||||
| 		) | ||||
| 			m_context << u256(m_dataType.storageBytes() - 1) << eth::Instruction::SIGNEXTEND; | ||||
| 			m_context << u256(m_dataType->storageBytes() - 1) << eth::Instruction::SIGNEXTEND; | ||||
| 		else | ||||
| 			m_context << ((u256(0x1) << (8 * m_dataType.storageBytes())) - 1) << eth::Instruction::AND; | ||||
| 			m_context << ((u256(0x1) << (8 * m_dataType->storageBytes())) - 1) << eth::Instruction::AND; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @ -185,11 +185,11 @@ void StorageItem::storeValue(Type const& _sourceType, SourceLocation const& _loc | ||||
| { | ||||
| 	CompilerUtils utils(m_context); | ||||
| 	// stack: value storage_key storage_offset
 | ||||
| 	if (m_dataType.isValueType()) | ||||
| 	if (m_dataType->isValueType()) | ||||
| 	{ | ||||
| 		solAssert(m_dataType.storageBytes() <= 32, "Invalid storage bytes size."); | ||||
| 		solAssert(m_dataType.storageBytes() > 0, "Invalid storage bytes size."); | ||||
| 		if (m_dataType.storageBytes() == 32) | ||||
| 		solAssert(m_dataType->storageBytes() <= 32, "Invalid storage bytes size."); | ||||
| 		solAssert(m_dataType->storageBytes() > 0, "Invalid storage bytes size."); | ||||
| 		if (m_dataType->storageBytes() == 32) | ||||
| 		{ | ||||
| 			// offset should be zero
 | ||||
| 			m_context << eth::Instruction::POP; | ||||
| @ -207,24 +207,24 @@ void StorageItem::storeValue(Type const& _sourceType, SourceLocation const& _loc | ||||
| 			// stack: value storege_ref multiplier old_full_value
 | ||||
| 			// clear bytes in old value
 | ||||
| 			m_context | ||||
| 				<< eth::Instruction::DUP2 << ((u256(1) << (8 * m_dataType.storageBytes())) - 1) | ||||
| 				<< eth::Instruction::DUP2 << ((u256(1) << (8 * m_dataType->storageBytes())) - 1) | ||||
| 				<< eth::Instruction::MUL; | ||||
| 			m_context << eth::Instruction::NOT << eth::Instruction::AND; | ||||
| 			// stack: value storage_ref multiplier cleared_value
 | ||||
| 			m_context | ||||
| 				<< eth::Instruction::SWAP1 << eth::Instruction::DUP4; | ||||
| 			// stack: value storage_ref cleared_value multiplier value
 | ||||
| 			if (m_dataType.category() == Type::Category::FixedBytes) | ||||
| 			if (m_dataType->category() == Type::Category::FixedBytes) | ||||
| 				m_context | ||||
| 					<< (u256(0x1) << (256 - 8 * dynamic_cast<FixedBytesType const&>(m_dataType).numBytes())) | ||||
| 					<< (u256(0x1) << (256 - 8 * dynamic_cast<FixedBytesType const&>(*m_dataType).numBytes())) | ||||
| 					<< eth::Instruction::SWAP1 << eth::Instruction::DIV; | ||||
| 			else if ( | ||||
| 				m_dataType.category() == Type::Category::Integer && | ||||
| 				dynamic_cast<IntegerType const&>(m_dataType).isSigned() | ||||
| 				m_dataType->category() == Type::Category::Integer && | ||||
| 				dynamic_cast<IntegerType const&>(*m_dataType).isSigned() | ||||
| 			) | ||||
| 				// remove the higher order bits
 | ||||
| 				m_context | ||||
| 					<< (u256(1) << (8 * (32 - m_dataType.storageBytes()))) | ||||
| 					<< (u256(1) << (8 * (32 - m_dataType->storageBytes()))) | ||||
| 					<< eth::Instruction::SWAP1 | ||||
| 					<< eth::Instruction::DUP2 | ||||
| 					<< eth::Instruction::MUL | ||||
| @ -239,23 +239,24 @@ void StorageItem::storeValue(Type const& _sourceType, SourceLocation const& _loc | ||||
| 	else | ||||
| 	{ | ||||
| 		solAssert( | ||||
| 			_sourceType.category() == m_dataType.category(), | ||||
| 			_sourceType.category() == m_dataType->category(), | ||||
| 			"Wrong type conversation for assignment."); | ||||
| 		if (m_dataType.category() == Type::Category::Array) | ||||
| 		if (m_dataType->category() == Type::Category::Array) | ||||
| 		{ | ||||
| 			m_context << eth::Instruction::POP; // remove byte offset
 | ||||
| 			ArrayUtils(m_context).copyArrayToStorage( | ||||
| 						dynamic_cast<ArrayType const&>(m_dataType), | ||||
| 						dynamic_cast<ArrayType const&>(_sourceType)); | ||||
| 				dynamic_cast<ArrayType const&>(*m_dataType), | ||||
| 				dynamic_cast<ArrayType const&>(_sourceType) | ||||
| 			); | ||||
| 			if (_move) | ||||
| 				m_context << eth::Instruction::POP; | ||||
| 		} | ||||
| 		else if (m_dataType.category() == Type::Category::Struct) | ||||
| 		else if (m_dataType->category() == Type::Category::Struct) | ||||
| 		{ | ||||
| 			// stack layout: source_ref target_ref target_offset
 | ||||
| 			// note that we have structs, so offset should be zero and are ignored
 | ||||
| 			m_context << eth::Instruction::POP; | ||||
| 			auto const& structType = dynamic_cast<StructType const&>(m_dataType); | ||||
| 			auto const& structType = dynamic_cast<StructType const&>(*m_dataType); | ||||
| 			auto const& sourceType = dynamic_cast<StructType const&>(_sourceType); | ||||
| 			solAssert( | ||||
| 				structType.structDefinition() == sourceType.structDefinition(), | ||||
| @ -313,18 +314,18 @@ void StorageItem::storeValue(Type const& _sourceType, SourceLocation const& _loc | ||||
| 
 | ||||
| void StorageItem::setToZero(SourceLocation const&, bool _removeReference) const | ||||
| { | ||||
| 	if (m_dataType.category() == Type::Category::Array) | ||||
| 	if (m_dataType->category() == Type::Category::Array) | ||||
| 	{ | ||||
| 		if (!_removeReference) | ||||
| 			CompilerUtils(m_context).copyToStackTop(sizeOnStack(), sizeOnStack()); | ||||
| 		ArrayUtils(m_context).clearArray(dynamic_cast<ArrayType const&>(m_dataType)); | ||||
| 		ArrayUtils(m_context).clearArray(dynamic_cast<ArrayType const&>(*m_dataType)); | ||||
| 	} | ||||
| 	else if (m_dataType.category() == Type::Category::Struct) | ||||
| 	else if (m_dataType->category() == Type::Category::Struct) | ||||
| 	{ | ||||
| 		// stack layout: storage_key storage_offset
 | ||||
| 		// @todo this can be improved: use StorageItem for non-value types, and just store 0 in
 | ||||
| 		// all slots that contain value types later.
 | ||||
| 		auto const& structType = dynamic_cast<StructType const&>(m_dataType); | ||||
| 		auto const& structType = dynamic_cast<StructType const&>(*m_dataType); | ||||
| 		for (auto const& member: structType.members()) | ||||
| 		{ | ||||
| 			// zero each member that is not a mapping
 | ||||
| @ -342,10 +343,10 @@ void StorageItem::setToZero(SourceLocation const&, bool _removeReference) const | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		solAssert(m_dataType.isValueType(), "Clearing of unsupported type requested: " + m_dataType.toString()); | ||||
| 		solAssert(m_dataType->isValueType(), "Clearing of unsupported type requested: " + m_dataType->toString()); | ||||
| 		if (!_removeReference) | ||||
| 			CompilerUtils(m_context).copyToStackTop(sizeOnStack(), sizeOnStack()); | ||||
| 		if (m_dataType.storageBytes() == 32) | ||||
| 		if (m_dataType->storageBytes() == 32) | ||||
| 		{ | ||||
| 			// offset should be zero
 | ||||
| 			m_context | ||||
| @ -361,7 +362,7 @@ void StorageItem::setToZero(SourceLocation const&, bool _removeReference) const | ||||
| 			// stack: storege_ref multiplier old_full_value
 | ||||
| 			// clear bytes in old value
 | ||||
| 			m_context | ||||
| 				<< eth::Instruction::SWAP1 << ((u256(1) << (8 * m_dataType.storageBytes())) - 1) | ||||
| 				<< eth::Instruction::SWAP1 << ((u256(1) << (8 * m_dataType->storageBytes())) - 1) | ||||
| 				<< eth::Instruction::MUL; | ||||
| 			m_context << eth::Instruction::NOT << eth::Instruction::AND; | ||||
| 			// stack: storage_ref cleared_value
 | ||||
| @ -374,7 +375,7 @@ void StorageItem::setToZero(SourceLocation const&, bool _removeReference) const | ||||
| static FixedBytesType byteType(1); | ||||
| 
 | ||||
| StorageByteArrayElement::StorageByteArrayElement(CompilerContext& _compilerContext): | ||||
| 	LValue(_compilerContext, byteType) | ||||
| 	LValue(_compilerContext, &byteType) | ||||
| { | ||||
| } | ||||
| 
 | ||||
| @ -427,7 +428,7 @@ void StorageByteArrayElement::setToZero(SourceLocation const&, bool _removeRefer | ||||
| } | ||||
| 
 | ||||
| StorageArrayLength::StorageArrayLength(CompilerContext& _compilerContext, const ArrayType& _arrayType): | ||||
| 	LValue(_compilerContext, *_arrayType.memberType("length")), | ||||
| 	LValue(_compilerContext, _arrayType.memberType("length").get()), | ||||
| 	m_arrayType(_arrayType) | ||||
| { | ||||
| 	solAssert(m_arrayType.isDynamicallySized(), ""); | ||||
| @ -455,3 +456,91 @@ void StorageArrayLength::setToZero(SourceLocation const&, bool _removeReference) | ||||
| 		m_context << eth::Instruction::DUP1; | ||||
| 	ArrayUtils(m_context).clearDynamicArray(m_arrayType); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| TupleObject::TupleObject( | ||||
| 	CompilerContext& _compilerContext, | ||||
| 	std::vector<std::unique_ptr<LValue>>&& _lvalues | ||||
| ): | ||||
| 	LValue(_compilerContext), m_lvalues(move(_lvalues)) | ||||
| { | ||||
| } | ||||
| 
 | ||||
| unsigned TupleObject::sizeOnStack() const | ||||
| { | ||||
| 	unsigned size = 0; | ||||
| 	for (auto const& lv: m_lvalues) | ||||
| 		if (lv) | ||||
| 			size += lv->sizeOnStack(); | ||||
| 	return size; | ||||
| } | ||||
| 
 | ||||
| void TupleObject::retrieveValue(SourceLocation const& _location, bool _remove) const | ||||
| { | ||||
| 	unsigned initialDepth = sizeOnStack(); | ||||
| 	unsigned initialStack = m_context.stackHeight(); | ||||
| 	for (auto const& lv: m_lvalues) | ||||
| 		if (lv) | ||||
| 		{ | ||||
| 			solAssert(initialDepth + m_context.stackHeight() >= initialStack, ""); | ||||
| 			unsigned depth = initialDepth + m_context.stackHeight() - initialStack; | ||||
| 			if (lv->sizeOnStack() > 0) | ||||
| 			{ | ||||
| 				if (_remove && depth > lv->sizeOnStack()) | ||||
| 					CompilerUtils(m_context).moveToStackTop(depth, depth - lv->sizeOnStack()); | ||||
| 				else if (!_remove && depth > 0) | ||||
| 					CompilerUtils(m_context).copyToStackTop(depth, lv->sizeOnStack()); | ||||
| 			} | ||||
| 			lv->retrieveValue(_location, true); | ||||
| 		} | ||||
| } | ||||
| 
 | ||||
| void TupleObject::storeValue(Type const& _sourceType, SourceLocation const& _location, bool) const | ||||
| { | ||||
| 	// values are below the lvalue references
 | ||||
| 	unsigned valuePos = sizeOnStack(); | ||||
| 	TypePointers const& valueTypes = dynamic_cast<TupleType const&>(_sourceType).components(); | ||||
| 	solAssert(valueTypes.size() == m_lvalues.size(), ""); | ||||
| 	// valuePos .... refPos ...
 | ||||
| 	// We will assign from right to left to optimize stack layout.
 | ||||
| 	for (size_t i = 0; i < m_lvalues.size(); ++i) | ||||
| 	{ | ||||
| 		unique_ptr<LValue> const& lvalue = m_lvalues[m_lvalues.size() - i - 1]; | ||||
| 		TypePointer const& valType = valueTypes[valueTypes.size() - i - 1]; | ||||
| 		unsigned stackHeight = m_context.stackHeight(); | ||||
| 		solAssert(!valType == !lvalue, ""); | ||||
| 		if (!lvalue) | ||||
| 			continue; | ||||
| 		valuePos += valType->sizeOnStack(); | ||||
| 		// copy value to top
 | ||||
| 		CompilerUtils(m_context).copyToStackTop(valuePos, valType->sizeOnStack()); | ||||
| 		// move lvalue ref above value
 | ||||
| 		CompilerUtils(m_context).moveToStackTop(valType->sizeOnStack(), lvalue->sizeOnStack()); | ||||
| 		lvalue->storeValue(*valType, _location, true); | ||||
| 		valuePos += m_context.stackHeight() - stackHeight; | ||||
| 	} | ||||
| 	// As the type of an assignment to a tuple type is the empty tuple, we always move.
 | ||||
| 	CompilerUtils(m_context).popStackElement(_sourceType); | ||||
| } | ||||
| 
 | ||||
| void TupleObject::setToZero(SourceLocation const& _location, bool _removeReference) const | ||||
| { | ||||
| 	if (_removeReference) | ||||
| 	{ | ||||
| 		for (size_t i = 0; i < m_lvalues.size(); ++i) | ||||
| 			if (m_lvalues[m_lvalues.size() - i]) | ||||
| 				m_lvalues[m_lvalues.size() - i]->setToZero(_location, true); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		unsigned depth = sizeOnStack(); | ||||
| 		for (auto const& val: m_lvalues) | ||||
| 			if (val) | ||||
| 			{ | ||||
| 				if (val->sizeOnStack() > 0) | ||||
| 					CompilerUtils(m_context).copyToStackTop(depth, val->sizeOnStack()); | ||||
| 				val->setToZero(_location, false); | ||||
| 				depth -= val->sizeOnStack(); | ||||
| 			} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -23,6 +23,7 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include <memory> | ||||
| #include <vector> | ||||
| #include <libevmasm/SourceLocation.h> | ||||
| #include <libsolidity/ArrayUtils.h> | ||||
| 
 | ||||
| @ -33,6 +34,7 @@ namespace solidity | ||||
| 
 | ||||
| class Declaration; | ||||
| class Type; | ||||
| class TupleType; | ||||
| class ArrayType; | ||||
| class CompilerContext; | ||||
| class VariableDeclaration; | ||||
| @ -43,7 +45,7 @@ class VariableDeclaration; | ||||
| class LValue | ||||
| { | ||||
| protected: | ||||
| 	LValue(CompilerContext& _compilerContext, Type const& _dataType): | ||||
| 	explicit LValue(CompilerContext& _compilerContext, Type const* _dataType = nullptr): | ||||
| 		m_context(_compilerContext), m_dataType(_dataType) {} | ||||
| 
 | ||||
| public: | ||||
| @ -68,7 +70,7 @@ public: | ||||
| 
 | ||||
| protected: | ||||
| 	CompilerContext& m_context; | ||||
| 	Type const& m_dataType; | ||||
| 	Type const* m_dataType; | ||||
| }; | ||||
| 
 | ||||
| /**
 | ||||
| @ -193,5 +195,30 @@ private: | ||||
| 	ArrayType const& m_arrayType; | ||||
| }; | ||||
| 
 | ||||
| /**
 | ||||
|  * Tuple object that can itself hold several LValues. | ||||
|  */ | ||||
| class TupleObject: public LValue | ||||
| { | ||||
| public: | ||||
| 	/// Constructs the LValue assuming that the other LValues are present on the stack.
 | ||||
| 	/// Empty unique_ptrs are possible if e.g. some values should be ignored during assignment.
 | ||||
| 	TupleObject(CompilerContext& _compilerContext, std::vector<std::unique_ptr<LValue>>&& _lvalues); | ||||
| 	virtual unsigned sizeOnStack() const; | ||||
| 	virtual void retrieveValue(SourceLocation const& _location, bool _remove = false) const override; | ||||
| 	virtual void storeValue( | ||||
| 		Type const& _sourceType, | ||||
| 		SourceLocation const& _location = SourceLocation(), | ||||
| 		bool _move = false | ||||
| 	) const override; | ||||
| 	virtual void setToZero( | ||||
| 		SourceLocation const& _location = SourceLocation(), | ||||
| 		bool _removeReference = true | ||||
| 	) const override; | ||||
| 
 | ||||
| private: | ||||
| 	std::vector<std::unique_ptr<LValue>> m_lvalues; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -806,6 +806,7 @@ ASTPointer<VariableDeclarationStatement> Parser::parseVariableDeclarationStateme | ||||
| 				) | ||||
| 				{ | ||||
| 					ASTNodeFactory varDeclNodeFactory(*this); | ||||
| 					varDeclNodeFactory.markEndPosition(); | ||||
| 					ASTPointer<ASTString> name = expectIdentifierToken(); | ||||
| 					var = varDeclNodeFactory.createNode<VariableDeclaration>( | ||||
| 						ASTPointer<TypeName>(), | ||||
| @ -1009,10 +1010,25 @@ ASTPointer<Expression> Parser::parsePrimaryExpression() | ||||
| 		break; | ||||
| 	case Token::LParen: | ||||
| 	{ | ||||
| 		// Tuple or parenthesized expression.
 | ||||
| 		// Special cases: () is empty tuple type, (x) is not a real tuple, (x,) is one-dimensional tuple
 | ||||
| 		m_scanner->next(); | ||||
| 		ASTPointer<Expression> expression = parseExpression(); | ||||
| 		vector<ASTPointer<Expression>> components; | ||||
| 		if (m_scanner->currentToken() != Token::RParen) | ||||
| 			while (true) | ||||
| 			{ | ||||
| 				if (m_scanner->currentToken() != Token::Comma && m_scanner->currentToken() != Token::RParen) | ||||
| 					components.push_back(parseExpression()); | ||||
| 				else | ||||
| 					components.push_back(ASTPointer<Expression>()); | ||||
| 				if (m_scanner->currentToken() == Token::RParen) | ||||
| 					break; | ||||
| 				else if (m_scanner->currentToken() == Token::Comma) | ||||
| 					m_scanner->next(); | ||||
| 			} | ||||
| 		nodeFactory.markEndPosition(); | ||||
| 		expectToken(Token::RParen); | ||||
| 		return expression; | ||||
| 		return nodeFactory.createNode<TupleExpression>(components); | ||||
| 	} | ||||
| 	default: | ||||
| 		if (Token::isElementaryTypeName(token)) | ||||
|  | ||||
| @ -560,13 +560,31 @@ void TypeChecker::endVisit(Return const& _return) | ||||
| 		return; | ||||
| 	ParameterList const* params = _return.annotation().functionReturnParameters; | ||||
| 	if (!params) | ||||
| 	{ | ||||
| 		typeError(_return, "Return arguments not allowed."); | ||||
| 		return; | ||||
| 	} | ||||
| 	TypePointers returnTypes; | ||||
| 	for (auto const& var: params->parameters()) | ||||
| 		returnTypes.push_back(type(*var)); | ||||
| 	if (auto tupleType = dynamic_cast<TupleType const*>(type(*_return.expression()).get())) | ||||
| 	{ | ||||
| 		if (tupleType->components().size() != params->parameters().size()) | ||||
| 			typeError(_return, "Different number of arguments in return statement than in returns declaration."); | ||||
| 		else if (!tupleType->isImplicitlyConvertibleTo(TupleType(returnTypes))) | ||||
| 			typeError( | ||||
| 				*_return.expression(), | ||||
| 				"Return argument type " + | ||||
| 				type(*_return.expression())->toString() + | ||||
| 				" is not implicitly convertible to expected type " + | ||||
| 				TupleType(returnTypes).toString(false) + | ||||
| 				"." | ||||
| 			); | ||||
| 	} | ||||
| 	else if (params->parameters().size() != 1) | ||||
| 		typeError(_return, "Different number of arguments in return statement than in returns declaration."); | ||||
| 	else | ||||
| 	{ | ||||
| 		// this could later be changed such that the paramaters type is an anonymous struct type,
 | ||||
| 		// but for now, we only allow one return parameter
 | ||||
| 		TypePointer const& expected = type(*params->parameters().front()); | ||||
| 		if (!type(*_return.expression())->isImplicitlyConvertibleTo(*expected)) | ||||
| 			typeError( | ||||
| @ -590,7 +608,7 @@ bool TypeChecker::visit(VariableDeclarationStatement const& _statement) | ||||
| 		VariableDeclaration const& varDecl = *_statement.declarations().front(); | ||||
| 		if (!varDecl.annotation().type) | ||||
| 			fatalTypeError(_statement, "Assignment necessary for type detection."); | ||||
| 		if (auto ref = dynamic_cast<ReferenceType const*>(varDecl.annotation().type.get())) | ||||
| 		if (auto ref = dynamic_cast<ReferenceType const*>(type(varDecl).get())) | ||||
| 		{ | ||||
| 			if (ref->dataStoredIn(DataLocation::Storage)) | ||||
| 			{ | ||||
| @ -610,10 +628,10 @@ bool TypeChecker::visit(VariableDeclarationStatement const& _statement) | ||||
| 
 | ||||
| 	_statement.initialValue()->accept(*this); | ||||
| 	TypePointers valueTypes; | ||||
| 	if (auto tupleType = dynamic_cast<TupleType const*>(_statement.initialValue()->annotation().type.get())) | ||||
| 	if (auto tupleType = dynamic_cast<TupleType const*>(type(*_statement.initialValue()).get())) | ||||
| 		valueTypes = tupleType->components(); | ||||
| 	else | ||||
| 		valueTypes = TypePointers{_statement.initialValue()->annotation().type}; | ||||
| 		valueTypes = TypePointers{type(*_statement.initialValue())}; | ||||
| 
 | ||||
| 	// Determine which component is assigned to which variable.
 | ||||
| 	// If numbers do not match, fill up if variables begin or end empty (not both).
 | ||||
| @ -712,7 +730,13 @@ bool TypeChecker::visit(Assignment const& _assignment) | ||||
| 	requireLValue(_assignment.leftHandSide()); | ||||
| 	TypePointer t = type(_assignment.leftHandSide()); | ||||
| 	_assignment.annotation().type = t; | ||||
| 	if (t->category() == Type::Category::Mapping) | ||||
| 	if (TupleType const* tupleType = dynamic_cast<TupleType const*>(t.get())) | ||||
| 	{ | ||||
| 		// Sequenced assignments of tuples is not valid, make the result a "void" type.
 | ||||
| 		_assignment.annotation().type = make_shared<TupleType>(); | ||||
| 		expectType(_assignment.rightHandSide(), *tupleType); | ||||
| 	} | ||||
| 	else if (t->category() == Type::Category::Mapping) | ||||
| 	{ | ||||
| 		typeError(_assignment, "Mappings cannot be assigned to."); | ||||
| 		_assignment.rightHandSide().accept(*this); | ||||
| @ -741,6 +765,51 @@ bool TypeChecker::visit(Assignment const& _assignment) | ||||
| 	return false; | ||||
| } | ||||
| 
 | ||||
| bool TypeChecker::visit(TupleExpression const& _tuple) | ||||
| { | ||||
| 	vector<ASTPointer<Expression>> const& components = _tuple.components(); | ||||
| 	TypePointers types; | ||||
| 	if (_tuple.annotation().lValueRequested) | ||||
| 	{ | ||||
| 		for (auto const& component: components) | ||||
| 			if (component) | ||||
| 			{ | ||||
| 				requireLValue(*component); | ||||
| 				types.push_back(type(*component)); | ||||
| 			} | ||||
| 			else | ||||
| 				types.push_back(TypePointer()); | ||||
| 		_tuple.annotation().type = make_shared<TupleType>(types); | ||||
| 		// If some of the components are not LValues, the error is reported above.
 | ||||
| 		_tuple.annotation().isLValue = true; | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		for (size_t i = 0; i < components.size(); ++i) | ||||
| 		{ | ||||
| 			// Outside of an lvalue-context, the only situation where a component can be empty is (x,).
 | ||||
| 			if (!components[i] && !(i == 1 && components.size() == 2)) | ||||
| 				fatalTypeError(_tuple, "Tuple component cannot be empty."); | ||||
| 			else if (components[i]) | ||||
| 			{ | ||||
| 				components[i]->accept(*this); | ||||
| 				types.push_back(type(*components[i])); | ||||
| 			} | ||||
| 			else | ||||
| 				types.push_back(TypePointer()); | ||||
| 		} | ||||
| 		if (components.size() == 1) | ||||
| 			_tuple.annotation().type = type(*components[0]); | ||||
| 		else | ||||
| 		{ | ||||
| 			if (components.size() == 2 && !components[1]) | ||||
| 				types.pop_back(); | ||||
| 			_tuple.annotation().type = make_shared<TupleType>(types); | ||||
| 		} | ||||
| 	} | ||||
| 	return false; | ||||
| } | ||||
| 
 | ||||
| bool TypeChecker::visit(UnaryOperation const& _operation) | ||||
| { | ||||
| 	// Inc, Dec, Add, Sub, Not, BitNot, Delete
 | ||||
| @ -1236,10 +1305,10 @@ void TypeChecker::expectType(Expression const& _expression, Type const& _expecte | ||||
| 
 | ||||
| void TypeChecker::requireLValue(Expression const& _expression) | ||||
| { | ||||
| 	_expression.annotation().lValueRequested = true; | ||||
| 	_expression.accept(*this); | ||||
| 	if (!_expression.annotation().isLValue) | ||||
| 		typeError(_expression, "Expression has to be an lvalue."); | ||||
| 	_expression.annotation().lValueRequested = true; | ||||
| } | ||||
| 
 | ||||
| void TypeChecker::typeError(ASTNode const& _node, string const& _description) | ||||
|  | ||||
| @ -90,6 +90,7 @@ private: | ||||
| 	virtual bool visit(VariableDeclarationStatement const& _variable) override; | ||||
| 	virtual void endVisit(ExpressionStatement const& _statement) override; | ||||
| 	virtual bool visit(Assignment const& _assignment) override; | ||||
| 	virtual bool visit(TupleExpression const& _tuple) override; | ||||
| 	virtual void endVisit(BinaryOperation const& _operation) override; | ||||
| 	virtual bool visit(UnaryOperation const& _operation) override; | ||||
| 	virtual bool visit(FunctionCall const& _functionCall) override; | ||||
|  | ||||
| @ -1242,6 +1242,38 @@ unsigned int EnumType::memberValue(ASTString const& _member) const | ||||
| 	BOOST_THROW_EXCEPTION(m_enum.createTypeError("Requested unknown enum value ." + _member)); | ||||
| } | ||||
| 
 | ||||
| bool TupleType::isImplicitlyConvertibleTo(Type const& _other) const | ||||
| { | ||||
| 	if (auto tupleType = dynamic_cast<TupleType const*>(&_other)) | ||||
| 	{ | ||||
| 		TypePointers const& targets = tupleType->components(); | ||||
| 		if (targets.empty()) | ||||
| 			return components().empty(); | ||||
| 		if (components().size() != targets.size() && !targets.front() && !targets.back()) | ||||
| 			return false; // (,a,) = (1,2,3,4) - unable to position `a` in the tuple.
 | ||||
| 		size_t minNumValues = targets.size(); | ||||
| 		if (!targets.back() || !targets.front()) | ||||
| 			--minNumValues; // wildcards can also match 0 components
 | ||||
| 		if (components().size() < minNumValues) | ||||
| 			return false; | ||||
| 		if (components().size() > targets.size() && targets.front() && targets.back()) | ||||
| 			return false; // larger source and no wildcard
 | ||||
| 		bool fillRight = !targets.back() || targets.front(); | ||||
| 		for (size_t i = 0; i < min(targets.size(), components().size()); ++i) | ||||
| 		{ | ||||
| 			auto const& s = components()[fillRight ? i : components().size() - i - 1]; | ||||
| 			auto const& t = targets[fillRight ? i : targets.size() - i - 1]; | ||||
| 			if (!s && t) | ||||
| 				return false; | ||||
| 			else if (s && t && !s->isImplicitlyConvertibleTo(*t)) | ||||
| 				return false; | ||||
| 		} | ||||
| 		return true; | ||||
| 	} | ||||
| 	else | ||||
| 		return false; | ||||
| } | ||||
| 
 | ||||
| bool TupleType::operator==(Type const& _other) const | ||||
| { | ||||
| 	if (auto tupleType = dynamic_cast<TupleType const*>(&_other)) | ||||
| @ -1252,12 +1284,12 @@ bool TupleType::operator==(Type const& _other) const | ||||
| 
 | ||||
| string TupleType::toString(bool _short) const | ||||
| { | ||||
| 	if (m_components.empty()) | ||||
| 	if (components().empty()) | ||||
| 		return "tuple()"; | ||||
| 	string str = "tuple("; | ||||
| 	for (auto const& t: m_components) | ||||
| 		str += t->toString(_short) + ", "; | ||||
| 	str.resize(str.size() - 2); | ||||
| 	for (auto const& t: components()) | ||||
| 		str += (t ? t->toString(_short) : "") + ","; | ||||
| 	str.pop_back(); | ||||
| 	return str + ")"; | ||||
| } | ||||
| 
 | ||||
| @ -1272,11 +1304,35 @@ u256 TupleType::storageSize() const | ||||
| unsigned TupleType::sizeOnStack() const | ||||
| { | ||||
| 	unsigned size = 0; | ||||
| 	for (auto const& t: m_components) | ||||
| 		size += t->sizeOnStack(); | ||||
| 	for (auto const& t: components()) | ||||
| 		size += t ? t->sizeOnStack() : 0; | ||||
| 	return size; | ||||
| } | ||||
| 
 | ||||
| TypePointer TupleType::mobileType() const | ||||
| { | ||||
| 	TypePointers mobiles; | ||||
| 	for (auto const& c: components()) | ||||
| 		mobiles.push_back(c ? c->mobileType() : TypePointer()); | ||||
| 	return make_shared<TupleType>(mobiles); | ||||
| } | ||||
| 
 | ||||
| TypePointer TupleType::closestTemporaryType(TypePointer const& _targetType) const | ||||
| { | ||||
| 	solAssert(!!_targetType, ""); | ||||
| 	TypePointers const& targetComponents = dynamic_cast<TupleType const&>(*_targetType).components(); | ||||
| 	bool fillRight = !targetComponents.empty() && (!targetComponents.back() || targetComponents.front()); | ||||
| 	TypePointers tempComponents(targetComponents.size()); | ||||
| 	for (size_t i = 0; i < min(targetComponents.size(), components().size()); ++i) | ||||
| 	{ | ||||
| 		size_t si = fillRight ? i : components().size() - i - 1; | ||||
| 		size_t ti = fillRight ? i : targetComponents.size() - i - 1; | ||||
| 		if (components()[si] && targetComponents[ti]) | ||||
| 			tempComponents[ti] = components()[si]->closestTemporaryType(targetComponents[ti]); | ||||
| 	} | ||||
| 	return make_shared<TupleType>(tempComponents); | ||||
| } | ||||
| 
 | ||||
| FunctionType::FunctionType(FunctionDefinition const& _function, bool _isInternal): | ||||
| 	m_location(_isInternal ? Location::Internal : Location::External), | ||||
| 	m_isConstant(_function.isDeclaredConst()), | ||||
| @ -1638,14 +1694,15 @@ FunctionTypePointer FunctionType::asMemberFunction(bool _inLibrary) const | ||||
| 			parameterTypes.push_back(t); | ||||
| 	} | ||||
| 
 | ||||
| 	//@todo make this more intelligent once we support destructuring assignments
 | ||||
| 	// Removes dynamic types.
 | ||||
| 	TypePointers returnParameterTypes; | ||||
| 	vector<string> returnParameterNames; | ||||
| 	if (!m_returnParameterTypes.empty() && m_returnParameterTypes.front()->calldataEncodedSize() > 0) | ||||
| 	{ | ||||
| 		returnParameterTypes.push_back(m_returnParameterTypes.front()); | ||||
| 		returnParameterNames.push_back(m_returnParameterNames.front()); | ||||
| 	} | ||||
| 	for (size_t i = 0; i < m_returnParameterTypes.size(); ++i) | ||||
| 		if (m_returnParameterTypes[i]->calldataEncodedSize() > 0) | ||||
| 		{ | ||||
| 			returnParameterTypes.push_back(m_returnParameterTypes[i]); | ||||
| 			returnParameterNames.push_back(m_returnParameterNames[i]); | ||||
| 		} | ||||
| 	return make_shared<FunctionType>( | ||||
| 		parameterTypes, | ||||
| 		returnParameterTypes, | ||||
|  | ||||
| @ -210,6 +210,13 @@ public: | ||||
| 	/// @returns true if this is a non-value type and the data of this type is stored at the
 | ||||
| 	/// given location.
 | ||||
| 	virtual bool dataStoredIn(DataLocation) const { return false; } | ||||
| 	/// @returns the type of a temporary during assignment to a variable of the given type.
 | ||||
| 	/// Specifically, returns the requested itself if it can be dynamically allocated (or is a value type)
 | ||||
| 	/// and the mobile type otherwise.
 | ||||
| 	virtual TypePointer closestTemporaryType(TypePointer const& _targetType) const | ||||
| 	{ | ||||
| 		return _targetType->dataStoredIn(DataLocation::Storage) ? mobileType() : _targetType; | ||||
| 	} | ||||
| 
 | ||||
| 	/// Returns the list of all members of this type. Default implementation: no members.
 | ||||
| 	virtual MemberList const& members() const { return EmptyMemberList; } | ||||
| @ -682,12 +689,14 @@ private: | ||||
| 
 | ||||
| /**
 | ||||
|  * Type that can hold a finite sequence of values of different types. | ||||
|  * In some cases, the components are empty pointers (when used as placeholders). | ||||
|  */ | ||||
| class TupleType: public Type | ||||
| { | ||||
| public: | ||||
| 	virtual Category category() const override { return Category::Tuple; } | ||||
| 	explicit TupleType(std::vector<TypePointer> const& _types = std::vector<TypePointer>()): m_components(_types) {} | ||||
| 	virtual bool isImplicitlyConvertibleTo(Type const& _other) const override; | ||||
| 	virtual bool operator==(Type const& _other) const override; | ||||
| 	virtual TypePointer binaryOperatorResult(Token::Value, TypePointer const&) const override { return TypePointer(); } | ||||
| 	virtual std::string toString(bool) const override; | ||||
| @ -695,6 +704,9 @@ public: | ||||
| 	virtual u256 storageSize() const override; | ||||
| 	virtual bool canLiveOutsideStorage() const override { return false; } | ||||
| 	virtual unsigned sizeOnStack() const override; | ||||
| 	virtual TypePointer mobileType() const override; | ||||
| 	/// Converts components to their temporary types and performs some wildcard matching.
 | ||||
| 	virtual TypePointer closestTemporaryType(TypePointer const& _targetType) const override; | ||||
| 
 | ||||
| 	std::vector<TypePointer> const& components() const { return m_components; } | ||||
| 
 | ||||
|  | ||||
| @ -5677,6 +5677,101 @@ BOOST_AUTO_TEST_CASE(multi_variable_declaration) | ||||
| 	BOOST_CHECK(callContractFunction("f()", encodeArgs()) == encodeArgs(true)); | ||||
| } | ||||
| 
 | ||||
| BOOST_AUTO_TEST_CASE(tuples) | ||||
| { | ||||
| 	char const* sourceCode = R"( | ||||
| 		contract C { | ||||
| 			uint[] data; | ||||
| 			function g() internal returns (uint a, uint b, uint[] storage c) { | ||||
| 				return (1, 2, data); | ||||
| 			} | ||||
| 			function h() external returns (uint a, uint b) { | ||||
| 				return (5, 6); | ||||
| 			} | ||||
| 			function f() returns (uint) { | ||||
| 				data.length = 1; | ||||
| 				data[0] = 3; | ||||
| 				uint a; uint b; | ||||
| 				(a, b) = this.h(); | ||||
| 				if (a != 5 || b != 6) return 1; | ||||
| 				uint[] storage c; | ||||
| 				(a, b, c) = g(); | ||||
| 				if (a != 1 || b != 2 || c[0] != 3) return 2; | ||||
| 				(a, b) = (b, a); | ||||
| 				if (a != 2 || b != 1) return 3; | ||||
| 				(a, , b, ) = (8, 9, 10, 11, 12); | ||||
| 				if (a != 8 || b != 10) return 4; | ||||
| 			} | ||||
| 		} | ||||
| 	)"; | ||||
| 	compileAndRun(sourceCode); | ||||
| 	BOOST_CHECK(callContractFunction("f()") == encodeArgs(u256(0))); | ||||
| } | ||||
| 
 | ||||
| BOOST_AUTO_TEST_CASE(destructuring_assignment) | ||||
| { | ||||
| 	char const* sourceCode = R"( | ||||
| 		contract C { | ||||
| 			uint x = 7; | ||||
| 			bytes data; | ||||
| 			uint[] y; | ||||
| 			uint[] arrayData; | ||||
| 			function returnsArray() returns (uint[]) { | ||||
| 				arrayData.length = 9; | ||||
| 				arrayData[2] = 5; | ||||
| 				arrayData[7] = 4; | ||||
| 				return arrayData; | ||||
| 			} | ||||
| 			function f(bytes s) returns (uint) { | ||||
| 				uint loc; | ||||
| 				uint[] memory memArray; | ||||
| 				(loc, x, y, data, arrayData[3]) = (8, 4, returnsArray(), s, 2); | ||||
| 				if (loc != 8) return 1; | ||||
| 				if (x != 4) return 2; | ||||
| 				if (y.length != 9) return 3; | ||||
| 				if (y[2] != 5) return 4; | ||||
| 				if (y[7] != 4) return 5; | ||||
| 				if (data.length != s.length) return 6; | ||||
| 				if (data[3] != s[3]) return 7; | ||||
| 				if (arrayData[3] != 2) return 8; | ||||
| 				(memArray, loc) = (arrayData, 3); | ||||
| 				if (loc != 3) return 9; | ||||
| 				if (memArray.length != arrayData.length) return 10; | ||||
| 				bytes memory memBytes; | ||||
| 				(x, memBytes, y[2], ) = (456, s, 789, 101112, 131415); | ||||
| 				if (x != 456 || memBytes.length != s.length || y[2] != 789) return 11; | ||||
| 			} | ||||
| 		} | ||||
| 	)"; | ||||
| 	compileAndRun(sourceCode); | ||||
| 	BOOST_CHECK(callContractFunction("f(bytes)", u256(0x20), u256(5), string("abcde")) == encodeArgs(u256(0))); | ||||
| } | ||||
| 
 | ||||
| BOOST_AUTO_TEST_CASE(destructuring_assignment_wildcard) | ||||
| { | ||||
| 	char const* sourceCode = R"( | ||||
| 		contract C { | ||||
| 			function f() returns (uint) { | ||||
| 				uint a; | ||||
| 				uint b; | ||||
| 				uint c; | ||||
| 				(a,) = (1,); | ||||
| 				if (a != 1) return 1; | ||||
| 				(,b) = (2,3,4); | ||||
| 				if (b != 4) return 2; | ||||
| 				(, c,) = (5,6,7); | ||||
| 				if (c != 6) return 3; | ||||
| 				(a, b,) = (11, 12, 13); | ||||
| 				if (a != 11 || b != 12) return 4; | ||||
| 				(, a, b) = (11, 12, 13); | ||||
| 				if (a != 12 || b != 13) return 5; | ||||
| 			} | ||||
| 		} | ||||
| 	)"; | ||||
| 	compileAndRun(sourceCode); | ||||
| 	BOOST_CHECK(callContractFunction("f()") == encodeArgs(u256(0))); | ||||
| } | ||||
| 
 | ||||
| BOOST_AUTO_TEST_SUITE_END() | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -109,13 +109,9 @@ ASTPointer<SourceUnit> parseAndAnalyse(string const& _source) | ||||
| 	return sourceAndError.first; | ||||
| } | ||||
| 
 | ||||
| bool success(std::string const& _source) | ||||
| bool success(string const& _source) | ||||
| { | ||||
| 	auto sourceAndError = parseAnalyseAndReturnError(_source); | ||||
| 
 | ||||
| 	if (sourceAndError.second && *sourceAndError.second == Error::Type::TypeError) | ||||
| 		return false; | ||||
| 	return true; | ||||
| 	return !parseAnalyseAndReturnError(_source).second; | ||||
| } | ||||
| 
 | ||||
| Error::Type expectError(std::string const& _source, bool _warning = false) | ||||
| @ -2460,6 +2456,33 @@ BOOST_AUTO_TEST_CASE(multi_variable_declaration_wildcards_fail_4) | ||||
| 	BOOST_CHECK(expectError(text) == Error::Type::TypeError); | ||||
| } | ||||
| 
 | ||||
| BOOST_AUTO_TEST_CASE(tuples) | ||||
| { | ||||
| 	char const* text = R"( | ||||
| 		contract C { | ||||
| 			function f() { | ||||
| 				uint a = (1); | ||||
| 				var (b,) = (1,); | ||||
| 				var (c,d) = (1, 2 + a); | ||||
| 				var (e,) = (1, 2, b); | ||||
| 			} | ||||
| 		} | ||||
| 	)"; | ||||
| 	BOOST_CHECK(success(text)); | ||||
| } | ||||
| 
 | ||||
| BOOST_AUTO_TEST_CASE(tuples_empty_components) | ||||
| { | ||||
| 	char const* text = R"( | ||||
| 		contract C { | ||||
| 			function f() { | ||||
| 				(1,,2); | ||||
| 			} | ||||
| 		} | ||||
| 	)"; | ||||
| 	BOOST_CHECK(expectError(text) == Error::Type::TypeError); | ||||
| } | ||||
| 
 | ||||
| BOOST_AUTO_TEST_CASE(multi_variable_declaration_wildcards_fail_5) | ||||
| { | ||||
| 	char const* text = R"( | ||||
|  | ||||
| @ -983,7 +983,7 @@ BOOST_AUTO_TEST_CASE(local_const_variable) | ||||
| BOOST_AUTO_TEST_CASE(multi_variable_declaration) | ||||
| { | ||||
| 	char const* text = R"( | ||||
| 		library Lib { | ||||
| 		contract C { | ||||
| 			function f() { | ||||
| 				var (a,b,c) = g(); | ||||
| 				var (d) = 2; | ||||
| @ -1000,6 +1000,21 @@ BOOST_AUTO_TEST_CASE(multi_variable_declaration) | ||||
| 	BOOST_CHECK(successParse(text)); | ||||
| } | ||||
| 
 | ||||
| BOOST_AUTO_TEST_CASE(tuples) | ||||
| { | ||||
| 	char const* text = R"( | ||||
| 		contract C { | ||||
| 			function f() { | ||||
| 				uint a = (1); | ||||
| 				var (b,) = (1,); | ||||
| 				var (c,d) = (1, 2 + a); | ||||
| 				var (e,) = (1, 2, b); | ||||
| 			} | ||||
| 		} | ||||
| 	)"; | ||||
| 	BOOST_CHECK(successParse(text)); | ||||
| } | ||||
| 
 | ||||
| BOOST_AUTO_TEST_SUITE_END() | ||||
| 
 | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user