User-defined operators: Analysis

This commit is contained in:
wechman 2022-07-06 09:17:59 +02:00 committed by Kamil Śliwak
parent 9445483d60
commit 5b03c13f90
10 changed files with 411 additions and 47 deletions

View File

@ -64,17 +64,53 @@ bool ControlFlowBuilder::visit(BinaryOperation const& _operation)
case Token::And:
{
visitNode(_operation);
solAssert(*_operation.annotation().userDefinedFunction == nullptr);
appendControlFlow(_operation.leftExpression());
auto nodes = splitFlow<2>();
nodes[0] = createFlow(nodes[0], _operation.rightExpression());
mergeFlow(nodes, nodes[1]);
return false;
}
default:
return ASTConstVisitor::visit(_operation);
{
if (*_operation.annotation().userDefinedFunction != nullptr)
{
visitNode(_operation);
_operation.leftExpression().accept(*this);
_operation.rightExpression().accept(*this);
m_currentNode->functionDefinition = *_operation.annotation().userDefinedFunction;
auto nextNode = newLabel();
connect(m_currentNode, nextNode);
m_currentNode = nextNode;
return false;
}
}
}
return ASTConstVisitor::visit(_operation);
}
bool ControlFlowBuilder::visit(UnaryOperation const& _operation)
{
solAssert(!!m_currentNode);
if (*_operation.annotation().userDefinedFunction != nullptr)
{
visitNode(_operation);
_operation.subExpression().accept(*this);
m_currentNode->functionDefinition = *_operation.annotation().userDefinedFunction;
auto nextNode = newLabel();
connect(m_currentNode, nextNode);
m_currentNode = nextNode;
return false;
}
return ASTConstVisitor::visit(_operation);
}
bool ControlFlowBuilder::visit(Conditional const& _conditional)

View File

@ -50,6 +50,7 @@ private:
// Visits for constructing the control flow.
bool visit(BinaryOperation const& _operation) override;
bool visit(UnaryOperation const& _operation) override;
bool visit(Conditional const& _conditional) override;
bool visit(TryStatement const& _tryStatement) override;
bool visit(IfStatement const& _ifStatement) override;

View File

@ -204,6 +204,20 @@ bool FunctionCallGraphBuilder::visit(MemberAccess const& _memberAccess)
return true;
}
bool FunctionCallGraphBuilder::visit(BinaryOperation const& _binaryOperation)
{
if (*_binaryOperation.annotation().userDefinedFunction != nullptr)
functionReferenced(**_binaryOperation.annotation().userDefinedFunction, true /* called directly */);
return true;
}
bool FunctionCallGraphBuilder::visit(UnaryOperation const& _unaryOperation)
{
if (*_unaryOperation.annotation().userDefinedFunction != nullptr)
functionReferenced(**_unaryOperation.annotation().userDefinedFunction, true /* called directly */);
return true;
}
bool FunctionCallGraphBuilder::visit(ModifierInvocation const& _modifierInvocation)
{
if (auto const* modifier = dynamic_cast<ModifierDefinition const*>(_modifierInvocation.name().annotation().referencedDeclaration))

View File

@ -72,6 +72,8 @@ private:
bool visit(EmitStatement const& _emitStatement) override;
bool visit(Identifier const& _identifier) override;
bool visit(MemberAccess const& _memberAccess) override;
bool visit(BinaryOperation const& _binaryOperation) override;
bool visit(UnaryOperation const& _unaryOperation) override;
bool visit(ModifierInvocation const& _modifierInvocation) override;
bool visit(NewExpression const& _newExpression) override;

View File

@ -411,6 +411,12 @@ void SyntaxChecker::endVisit(ContractDefinition const&)
bool SyntaxChecker::visit(UsingForDirective const& _usingFor)
{
if (!_usingFor.usesBraces())
solAssert(
_usingFor.functionsAndOperators().size() == 1 &&
!get<1>(_usingFor.functionsAndOperators().front())
);
if (!m_currentContractKind && !_usingFor.typeName())
m_errorReporter.syntaxError(
8118_error,

View File

@ -24,6 +24,7 @@
#include <libsolidity/analysis/TypeChecker.h>
#include <libsolidity/ast/AST.h>
#include <libsolidity/ast/ASTUtils.h>
#include <libsolidity/ast/UserDefinableOperators.h>
#include <libsolidity/ast/TypeProvider.h>
#include <libyul/AsmAnalysis.h>
@ -1606,7 +1607,7 @@ bool TypeChecker::visit(Assignment const& _assignment)
7366_error,
_assignment.location(),
"Operator " +
string(TokenTraits::toString(_assignment.assignmentOperator())) +
string(TokenTraits::friendlyName(_assignment.assignmentOperator())) +
" not compatible with types " +
t->humanReadableName() +
" and " +
@ -1729,16 +1730,45 @@ bool TypeChecker::visit(UnaryOperation const& _operation)
requireLValue(_operation.subExpression(), false);
else
_operation.subExpression().accept(*this);
Type const* subExprType = type(_operation.subExpression());
TypeResult result = type(_operation.subExpression())->unaryOperatorResult(op);
if (!result)
Type const* operandType = type(_operation.subExpression());
// Check if the operator is built-in or user-defined.
TypeResult builtinResult = operandType->unaryOperatorResult(op);
set<FunctionDefinition const*, ASTNode::CompareByID> matchingDefinitions = operandType->operatorDefinitions(
op,
*currentDefinitionScope(),
true // _unary
);
// Operator can't be both user-defined and built-in at the same time.
solAssert(!builtinResult || matchingDefinitions.empty());
// By default use the type we'd expect from correct code. This way we can continue analysis
// of other expressions in a sensible way in case of a non-fatal error.
Type const* resultType = operandType;
FunctionDefinition const* operatorDefinition = nullptr;
if (builtinResult)
resultType = builtinResult;
else if (!matchingDefinitions.empty())
{
// This is checked along with `using for` directive but the error is not fatal.
if (matchingDefinitions.size() != 1)
solAssert(m_errorReporter.hasErrors());
operatorDefinition = *matchingDefinitions.begin();
}
else
{
string description = fmt::format(
"Built-in unary operator {} cannot be applied to type {}.{}",
TokenTraits::toString(op),
subExprType->humanReadableName(),
!result.message().empty() ? " " + result.message() : ""
"Built-in unary operator {} cannot be applied to type {}.",
TokenTraits::friendlyName(op),
operandType->humanReadableName()
);
if (!builtinResult.message().empty())
description += " " + builtinResult.message();
if (operandType->typeDefinition() && util::contains(userDefinableOperators, op))
description += " No matching user-defined operator found.";
if (modifying)
// Cannot just report the error, ignore the unary operator, and continue,
@ -1746,14 +1776,21 @@ bool TypeChecker::visit(UnaryOperation const& _operation)
m_errorReporter.fatalTypeError(9767_error, _operation.location(), description);
else
m_errorReporter.typeError(4907_error, _operation.location(), description);
_operation.annotation().type = subExprType;
}
else
_operation.annotation().type = result.get();
_operation.annotation().userDefinedFunction = operatorDefinition;
TypePointers const& returnParameterTypes = _operation.userDefinedFunctionType()->returnParameterTypes();
if (operatorDefinition && !returnParameterTypes.empty())
// Use the actual result type from operator definition. Ignore all values but the
// first one - in valid code there will be only one anyway.
resultType = returnParameterTypes[0];
_operation.annotation().type = resultType;
_operation.annotation().isConstant = false;
_operation.annotation().isPure =
!modifying &&
*_operation.subExpression().annotation().isPure;
*_operation.subExpression().annotation().isPure &&
(!_operation.userDefinedFunctionType() || _operation.userDefinedFunctionType()->isPure());
_operation.annotation().isLValue = false;
return false;
@ -1763,31 +1800,95 @@ void TypeChecker::endVisit(BinaryOperation const& _operation)
{
Type const* leftType = type(_operation.leftExpression());
Type const* rightType = type(_operation.rightExpression());
TypeResult result = leftType->binaryOperatorResult(_operation.getOperator(), rightType);
Type const* commonType = result.get();
if (!commonType)
// Check if the operator is built-in or user-defined.
TypeResult builtinResult = leftType->binaryOperatorResult(_operation.getOperator(), rightType);
set<FunctionDefinition const*, ASTNode::CompareByID> matchingDefinitions = leftType->operatorDefinitions(
_operation.getOperator(),
*currentDefinitionScope(),
false // _unary
);
// Operator can't be both user-defined and built-in at the same time.
solAssert(!builtinResult || matchingDefinitions.empty());
Type const* commonType = nullptr;
FunctionDefinition const* operatorDefinition = nullptr;
if (builtinResult)
commonType = builtinResult.get();
else if (!matchingDefinitions.empty())
{
m_errorReporter.typeError(
2271_error,
_operation.location(),
"Built-in binary operator " +
string(TokenTraits::toString(_operation.getOperator())) +
" cannot be applied to types " +
leftType->humanReadableName() +
" and " +
rightType->humanReadableName() + "." +
(!result.message().empty() ? " " + result.message() : "")
);
// This is checked along with `using for` directive but the error is not fatal.
if (matchingDefinitions.size() != 1)
solAssert(m_errorReporter.hasErrors());
operatorDefinition = *matchingDefinitions.begin();
// Set common type to the type used in the `using for` directive.
commonType = leftType;
}
else
{
string description = fmt::format(
"Built-in binary operator {} cannot be applied to types {} and {}.",
TokenTraits::friendlyName(_operation.getOperator()),
leftType->humanReadableName(),
rightType->humanReadableName()
);
if (!builtinResult.message().empty())
description += " " + builtinResult.message();
if (leftType->typeDefinition() && util::contains(userDefinableOperators, _operation.getOperator()))
description += " No matching user-defined operator found.";
m_errorReporter.typeError(2271_error, _operation.location(), description);
// Set common type to something we'd expect from correct code just so that we can continue analysis.
commonType = leftType;
}
_operation.annotation().commonType = commonType;
_operation.annotation().type =
_operation.annotation().userDefinedFunction = operatorDefinition;
FunctionType const* userDefinedFunctionType = _operation.userDefinedFunctionType();
// By default use the type we'd expect from correct code. This way we can continue analysis
// of other expressions in a sensible way in case of a non-fatal error.
Type const* resultType =
TokenTraits::isCompareOp(_operation.getOperator()) ?
TypeProvider::boolean() :
commonType;
if (operatorDefinition)
{
TypePointers const& parameterTypes = userDefinedFunctionType->parameterTypes();
TypePointers const& returnParameterTypes = userDefinedFunctionType->returnParameterTypes();
// operatorDefinitions() filters out definitions with non-matching first argument.
solAssert(parameterTypes.size() == 2);
solAssert(parameterTypes[0] && *leftType == *parameterTypes[0]);
if (*rightType != *parameterTypes[0])
m_errorReporter.typeError(
5653_error,
_operation.location(),
fmt::format(
"The type of the second operand of this user-defined binary operator {} "
"does not match the type of the first operand, which is {}.",
TokenTraits::friendlyName(_operation.getOperator()),
parameterTypes[0]->humanReadableName()
)
);
if (!returnParameterTypes.empty())
// Use the actual result type from operator definition. Ignore all values but the
// first one - in valid code there will be only one anyway.
resultType = returnParameterTypes[0];
}
_operation.annotation().type = resultType;
_operation.annotation().isPure =
*_operation.leftExpression().annotation().isPure &&
*_operation.rightExpression().annotation().isPure;
*_operation.rightExpression().annotation().isPure &&
(!userDefinedFunctionType || userDefinedFunctionType->isPure());
_operation.annotation().isLValue = false;
_operation.annotation().isConstant = false;
@ -1814,14 +1915,14 @@ void TypeChecker::endVisit(BinaryOperation const& _operation)
m_errorReporter.warning(
3149_error,
_operation.location(),
"The result type of the " +
operation +
" operation is equal to the type of the first operand (" +
commonType->humanReadableName() +
") ignoring the (larger) type of the second operand (" +
rightType->humanReadableName() +
") which might be unexpected. Silence this warning by either converting "
"the first or the second operand to the type of the other."
fmt::format(
"The result type of the {} operation is equal to the type of the first operand ({}) "
"ignoring the (larger) type of the second operand ({}) which might be unexpected. "
"Silence this warning by either converting the first or the second operand to the type of the other.",
operation,
commonType->humanReadableName(),
rightType->humanReadableName()
)
);
}
}
@ -3820,7 +3921,7 @@ void TypeChecker::endVisit(UsingForDirective const& _usingFor)
);
solAssert(normalizedType);
for (ASTPointer<IdentifierPath> const& path: _usingFor.functionsOrLibrary())
for (auto const& [path, operator_]: _usingFor.functionsAndOperators())
{
solAssert(path->annotation().referencedDeclaration);
FunctionDefinition const& functionDefinition =
@ -3839,8 +3940,8 @@ void TypeChecker::endVisit(UsingForDirective const& _usingFor)
4731_error,
path->location(),
SecondarySourceLocation().append(
"Function defined here:",
functionDefinition.location()
"Function defined here:",
functionDefinition.location()
),
fmt::format(
"The function \"{}\" does not have any parameters, and therefore cannot be attached to the type \"{}\".",
@ -3859,8 +3960,8 @@ void TypeChecker::endVisit(UsingForDirective const& _usingFor)
6772_error,
path->location(),
SecondarySourceLocation().append(
"Function defined here:",
functionDefinition.location()
"Function defined here:",
functionDefinition.location()
),
fmt::format(
"Function \"{}\" is private and therefore cannot be attached"
@ -3875,13 +3976,13 @@ void TypeChecker::endVisit(UsingForDirective const& _usingFor)
BoolResult result = normalizedType->isImplicitlyConvertibleTo(
*TypeProvider::withLocationIfReference(DataLocation::Storage, functionTypeWithBoundFirstArgument->selfType())
);
if (!result)
if (!result && !operator_)
m_errorReporter.typeError(
3100_error,
path->location(),
SecondarySourceLocation().append(
"Function defined here:",
functionDefinition.location()
"Function defined here:",
functionDefinition.location()
),
fmt::format(
"The function \"{}\" cannot be attached to the type \"{}\" because the type cannot "
@ -3892,6 +3993,144 @@ void TypeChecker::endVisit(UsingForDirective const& _usingFor)
result.message().empty() ? "." : ": " + result.message()
)
);
else if (operator_.has_value())
{
if (!_usingFor.global())
m_errorReporter.typeError(
3320_error,
path->location(),
"Operators can only be defined in a global 'using for' directive."
);
if (
functionType->stateMutability() != StateMutability::Pure ||
!functionDefinition.isFree()
)
m_errorReporter.typeError(
7775_error,
path->location(),
SecondarySourceLocation().append(
"Function defined as non-pure here:",
functionDefinition.location()
),
"Only pure free functions can be used to define operators."
);
solAssert(!functionType->hasBoundFirstArgument());
TypePointers const& parameterTypes = functionType->parameterTypes();
size_t const parameterCount = parameterTypes.size();
if (usingForType->category() != Type::Category::UserDefinedValueType)
{
m_errorReporter.typeError(
5332_error,
path->location(),
"Operators can only be implemented for user-defined value types."
);
continue;
}
solAssert(usingForType->typeDefinition());
bool identicalFirstTwoParameters = (parameterCount < 2 || *parameterTypes.at(0) == *parameterTypes.at(1));
bool isUnaryOnlyOperator = (!TokenTraits::isBinaryOp(operator_.value()) && TokenTraits::isUnaryOp(operator_.value()));
bool isBinaryOnlyOperator =
(TokenTraits::isBinaryOp(operator_.value()) && !TokenTraits::isUnaryOp(operator_.value())) ||
operator_.value() == Token::Add;
bool firstParameterMatchesUsingFor = parameterCount == 0 || *usingForType == *parameterTypes.front();
optional<string> wrongParametersMessage;
if (isBinaryOnlyOperator && (parameterCount != 2 || !identicalFirstTwoParameters))
wrongParametersMessage = fmt::format("two parameters of type {} and the same data location", usingForType->canonicalName());
else if (isUnaryOnlyOperator && (parameterCount != 1 || !firstParameterMatchesUsingFor))
wrongParametersMessage = fmt::format("exactly one parameter of type {}", usingForType->canonicalName());
else if (parameterCount >= 3 || !firstParameterMatchesUsingFor || !identicalFirstTwoParameters)
wrongParametersMessage = fmt::format("one or two parameters of type {} and the same data location", usingForType->canonicalName());
if (wrongParametersMessage.has_value())
m_errorReporter.typeError(
1884_error,
functionDefinition.parameterList().location(),
SecondarySourceLocation().append(
"Function was used to implement an operator here:",
path->location()
),
fmt::format(
"Wrong parameters in operator definition. "
"The function \"{}\" needs to have {} to be used for the operator {}.",
joinHumanReadable(path->path(), "."),
wrongParametersMessage.value(),
TokenTraits::friendlyName(operator_.value())
)
);
// This case is separately validated for all attached functions and is a fatal error
solAssert(parameterCount != 0);
TypePointers const& returnParameterTypes = functionType->returnParameterTypes();
size_t const returnParameterCount = returnParameterTypes.size();
optional<string> wrongReturnParametersMessage;
if (!TokenTraits::isCompareOp(operator_.value()) && operator_.value() != Token::Not)
{
if (returnParameterCount != 1 || *usingForType != *returnParameterTypes.front())
wrongReturnParametersMessage = "exactly one value of type " + usingForType->canonicalName();
else if (*returnParameterTypes.front() != *parameterTypes.front())
wrongReturnParametersMessage = "a value of the same type and data location as its parameters";
}
else if (returnParameterCount != 1 || *returnParameterTypes.front() != *TypeProvider::boolean())
wrongReturnParametersMessage = "exactly one value of type bool";
solAssert(functionDefinition.returnParameterList());
if (wrongReturnParametersMessage.has_value())
m_errorReporter.typeError(
7743_error,
functionDefinition.returnParameterList()->location(),
SecondarySourceLocation().append(
"Function was used to implement an operator here:",
path->location()
),
fmt::format(
"Wrong return parameters in operator definition. "
"The function \"{}\" needs to return {} to be used for the operator {}.",
joinHumanReadable(path->path(), "."),
wrongReturnParametersMessage.value(),
TokenTraits::friendlyName(operator_.value())
)
);
if (parameterCount != 1 && parameterCount != 2)
solAssert(m_errorReporter.hasErrors());
else
{
// TODO: This is pretty inefficient. For every operator binding we find, we're
// traversing all bindings in all `using for` directives in the current scope.
set<FunctionDefinition const*, ASTNode::CompareByID> matchingDefinitions = usingForType->operatorDefinitions(
operator_.value(),
*currentDefinitionScope(),
parameterCount == 1 // _unary
);
if (matchingDefinitions.size() >= 2)
{
// TODO: We should point at other places that bind the operator rather than at
// the definitions they bind.
SecondarySourceLocation secondaryLocation;
for (FunctionDefinition const* definition: matchingDefinitions)
if (functionDefinition != *definition)
secondaryLocation.append("Conflicting definition:", definition->location());
m_errorReporter.typeError(
4705_error,
path->location(),
secondaryLocation,
fmt::format(
"User-defined {} operator {} has more than one definition matching the operand type visible in the current scope.",
parameterCount == 1 ? "unary" : "binary",
TokenTraits::friendlyName(operator_.value())
)
);
}
}
}
}
}

View File

@ -331,6 +331,18 @@ void ViewPureChecker::reportFunctionCallMutability(StateMutability _mutability,
reportMutability(_mutability, _location);
}
void ViewPureChecker::endVisit(BinaryOperation const& _binaryOperation)
{
if (*_binaryOperation.annotation().userDefinedFunction != nullptr)
reportFunctionCallMutability((*_binaryOperation.annotation().userDefinedFunction)->stateMutability(), _binaryOperation.location());
}
void ViewPureChecker::endVisit(UnaryOperation const& _unaryOperation)
{
if (*_unaryOperation.annotation().userDefinedFunction != nullptr)
reportFunctionCallMutability((*_unaryOperation.annotation().userDefinedFunction)->stateMutability(), _unaryOperation.location());
}
void ViewPureChecker::endVisit(FunctionCall const& _functionCall)
{
if (*_functionCall.annotation().kind != FunctionCallKind::FunctionCall)

View File

@ -54,6 +54,8 @@ private:
bool visit(FunctionDefinition const& _funDef) override;
void endVisit(FunctionDefinition const& _funDef) override;
void endVisit(BinaryOperation const& _binaryOperation) override;
void endVisit(UnaryOperation const& _unaryOperation) override;
bool visit(ModifierDefinition const& _modifierDef) override;
void endVisit(ModifierDefinition const& _modifierDef) override;
void endVisit(Identifier const& _identifier) override;

View File

@ -383,6 +383,38 @@ vector<UsingForDirective const*> usingForDirectivesForType(Type const& _type, AS
}
set<FunctionDefinition const*, ASTNode::CompareByID> Type::operatorDefinitions(
Token _token,
ASTNode const& _scope,
bool _unary
) const
{
if (!typeDefinition())
return {};
set<FunctionDefinition const*, ASTNode::CompareByID> matchingDefinitions;
for (UsingForDirective const* directive: usingForDirectivesForType(*this, _scope))
for (auto const& [identifierPath, operator_]: directive->functionsAndOperators())
{
if (operator_ != _token)
continue;
auto const& functionDefinition = dynamic_cast<FunctionDefinition const&>(
*identifierPath->annotation().referencedDeclaration
);
auto const* functionType = dynamic_cast<FunctionType const*>(
functionDefinition.libraryFunction() ? functionDefinition.typeViaContractName() : functionDefinition.type()
);
solAssert(functionType && !functionType->parameterTypes().empty());
size_t parameterCount = functionDefinition.parameterList().parameters().size();
if (*this == *functionType->parameterTypes().front() && (_unary ? parameterCount == 1 : parameterCount == 2))
matchingDefinitions.insert(&functionDefinition);
}
return matchingDefinitions;
}
MemberList::MemberMap Type::attachedFunctions(Type const& _type, ASTNode const& _scope)
{
MemberList::MemberMap members;
@ -405,8 +437,13 @@ MemberList::MemberMap Type::attachedFunctions(Type const& _type, ASTNode const&
};
for (UsingForDirective const* ufd: usingForDirectivesForType(_type, _scope))
for (auto const& identifierPath: ufd->functionsOrLibrary())
for (auto const& [identifierPath, operator_]: ufd->functionsAndOperators())
{
if (operator_.has_value())
// Functions used to define operators are not automatically attached to the type.
// I.e. `using {f, f as +} for T` allows `T x; x.f()` but `using {f as +} for T` does not.
continue;
solAssert(identifierPath);
Declaration const* declaration = identifierPath->annotation().referencedDeclaration;
solAssert(declaration);

View File

@ -377,6 +377,21 @@ public:
/// Clears all internally cached values (if any).
virtual void clearCache() const;
/// Scans all "using for" directives in the @a _scope for functions implementing
/// the operator represented by @a _token. Returns the set of all definitions where the type
/// of the first argument matches this type object.
///
/// @note: If the AST has passed analysis without errors,
/// the function will find at most one definition for an operator.
///
/// @param _unary If true, only definitions that accept exactly one argument are included.
/// Otherwise only definitions that accept exactly two arguments.
std::set<FunctionDefinition const*, ASTCompareByID<ASTNode>> operatorDefinitions(
Token _token,
ASTNode const& _scope,
bool _unary
) const;
private:
/// @returns a member list containing all members added to this type by `using for` directives.
static MemberList::MemberMap attachedFunctions(Type const& _type, ASTNode const& _scope);