Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support user-defined operators in SMTChecker #14534

Merged
merged 4 commits into from
Oct 22, 2023

Conversation

pgebal
Copy link
Collaborator

@pgebal pgebal commented Sep 5, 2023

Closes #13893.

User-defined operators are handled like function calls.

@pgebal pgebal marked this pull request as draft September 5, 2023 09:22
@pgebal pgebal force-pushed the pgebal/operators branch 3 times, most recently from 3a5a4df to bb37a3e Compare September 8, 2023 08:39
@pgebal pgebal changed the title Pgebal/operators Support user-defined operators in SMT checker Sep 8, 2023
@pgebal pgebal changed the title Support user-defined operators in SMT checker Support user-defined operators in SMTChecker Sep 8, 2023
}

contract C {
function f(U x, U y) public pure returns (U) {
return x / y; // FIXME: should detect div by zero
return x / y; // FIXME: are we ok that div by zero not reported here?
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a potential problem that errors are not reported where the operator is used, but in the implementation.
In this example division by zero is reported in line 5 but not here.
I could live with that, but maybe we want the error to be reported here as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it makes perfect sense to report it in the implementation. I guess that's consistent with what would happen for a div(x, y) function that does division. So I think this is fine and probably even expected.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. The implementation of the user-defined operator can be arbitrary code, so it makes sense to pinpoint the exact problem in the operator implementation.

}

function testArithmetic(I16 x, I16 y) public pure {
assert(x + y == add(x, y)); // FIXME: should hold
assert(x + y == add(x, y)); // should hold
Copy link
Collaborator Author

@pgebal pgebal Sep 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHC reports Assertion violation *might* happen here.
I don't know why. Maybe it's because there are possible overlows/underflows/division by zero in implementation?

It affects only integer arithmetic operators. Bitwise and boolean are ok.

When I don't use operators and just apply appropriate function calls on both sides of the relation I also get those warnings.

edit:
On current develop branch, if I turn the assert statement into for example:
assert(eq(add(x, y), add(x, y))) it also yields Assertion violation *might* happen here. warnings.

Copy link
Collaborator Author

@pgebal pgebal Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blishko Do you know what this^ might happen?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*might* happen just means that the solver was not able to solve the query within the given timeout/resource limit.
At least I think it is only in this case. This does not mean that there is necessarily something wrong here, just that the solver is not able to deal with the query efficiently. We can try to solve it with increased timeout, see if that helps.

@ethereum ethereum deleted a comment Sep 21, 2023
@pgebal pgebal force-pushed the pgebal/operators branch 2 times, most recently from 439c4e1 to 3a8b65c Compare September 26, 2023 09:00
@pgebal pgebal marked this pull request as ready for review September 26, 2023 09:00
@pgebal pgebal requested a review from leonardoalt September 28, 2023 17:13
@pgebal pgebal force-pushed the pgebal/operators branch 3 times, most recently from 772ecc0 to b68f8f8 Compare September 29, 2023 16:14
Copy link
Contributor

@blishko blishko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good! I agree that treating the user-defined operators as function calls is the right approach. I just have a few questions.

libsolidity/formal/SMTEncoder.h Outdated Show resolved Hide resolved
libsolidity/formal/CHC.cpp Outdated Show resolved Hide resolved
libsolidity/formal/CHC.h Show resolved Hide resolved
libsolidity/formal/CHC.cpp Outdated Show resolved Hide resolved
libsolidity/formal/CHC.cpp Outdated Show resolved Hide resolved
Copy link
Contributor

@blishko blishko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some more comments based on my recent reflections.

void BMC::inlineFunctionCall(
FunctionDefinition const* _funDef,
Expression const& _callStackExpr,
Expression const* _calledExpr,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks to me like we are not consistent about what _calledExpr represents.
In case of Operations, you pass the pointer to the whole operation expression, e.g. a + b.
But in case of a function call, e.g., f(a,b), this is only the pointer to the expression representing the name of the function, i.e, f.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea there is a mix up here:

  • pushCallStack and defineExpr inside createReturnedExpression should be called on the FunctionCall object for the normal function call case, and indeed for the expression in case if user-defined operator call (e.g. a + b).
  • _funType->hasBoundFirstArgument() can only be true for normal function calls (I assume, we should check in solidity-dev), so in symbolicArguments we do need the FunctionCall::expression() object (of type Expression) for the function call case, but not for the user-defined case

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please correct me if I'm wrong, but I think now the arguments provided to BMC::inlineFuncitonCall are correct.

Comment on lines 742 to 746
Expression const* calledExpr = &_funCall.expression();
auto funType = dynamic_cast<FunctionType const*>(calledExpr->annotation().type);
std::vector<ASTPointer<Expression const>> arguments = _funCall.sortedArguments();

inlineFunctionCall(funDef, _funCall, calledExpr, funType, arguments);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we extract only the name of the function to calledExpr from the whole function call expression, see my comment above about difference in what calledExpr represents.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I agree, the callstack object needs to be the one of type FunctionCall* in case of an actual function call

Copy link
Collaborator Author

@pgebal pgebal Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leonardoalt, to make sure, the current code is ok in that regard?
Maybe I should leave a clarifying comment in the code?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leonardoalt , I am still confused about "callstack object". This should be the expression that represents the function call in the caller?

For me the confusing bit here is the calledExpr. In the case of an actual function call, this will be the expression representing the name of the function (because it is FunctionCall::expression).
Why would we refer to the expressions of the function name as calledExpr? It is not the expression that represents the function call, only the name of the function.

Comment on lines 3151 to 3154
if (_funType->hasBoundFirstArgument())
{
calledExpr = innermostTuple(*calledExpr);
auto const& attachedFunction = dynamic_cast<MemberAccess const*>(calledExpr);
_calledExpr = innermostTuple(*_calledExpr);
auto const& attachedFunction = dynamic_cast<MemberAccess const*>(_calledExpr);
solAssert(attachedFunction, "");
args.push_back(expr(attachedFunction->expression(), functionParams.front()->type()));
firstParam = 1;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more I am looking at this, the more I would like to refactor this method somehow to split the treatment of the extra argument in the special case of MemberAccess and the conversion of actual arguments to SMT expressions.

One way that comes to my mind is to check for this optional extra argument beforehand and pass here just std::optional<smtutil::Expression>. Then instead of _funDef, _calledExpr, _funType, this method would only need the formal parameters (what we now get from _funDef->parameters()).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

libsolidity/ast/AST.h Outdated Show resolved Hide resolved
libsolidity/ast/AST.h Outdated Show resolved Hide resolved
Comment on lines 742 to 746
Expression const* calledExpr = &_funCall.expression();
auto funType = dynamic_cast<FunctionType const*>(calledExpr->annotation().type);
std::vector<ASTPointer<Expression const>> arguments = _funCall.sortedArguments();

inlineFunctionCall(funDef, _funCall, calledExpr, funType, arguments);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I agree, the callstack object needs to be the one of type FunctionCall* in case of an actual function call

void BMC::inlineFunctionCall(
FunctionDefinition const* _funDef,
Expression const& _callStackExpr,
Expression const* _calledExpr,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea there is a mix up here:

  • pushCallStack and defineExpr inside createReturnedExpression should be called on the FunctionCall object for the normal function call case, and indeed for the expression in case if user-defined operator call (e.g. a + b).
  • _funType->hasBoundFirstArgument() can only be true for normal function calls (I assume, we should check in solidity-dev), so in symbolicArguments we do need the FunctionCall::expression() object (of type Expression) for the function call case, but not for the user-defined case

@pgebal pgebal force-pushed the pgebal/operators branch 3 times, most recently from b7e11f2 to 71eb81f Compare October 12, 2023 08:36
@pgebal pgebal requested review from blishko and leonardoalt October 12, 2023 08:37
blishko
blishko previously approved these changes Oct 12, 2023
Copy link
Contributor

@blishko blishko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good from my side!

unsigned firstParam = 0;
if (funType->hasBoundFirstArgument())
if (_calledExpr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks a bit misleading. It must run only for bound calls, but it's called _calledExpr. Are we sure it only runs in such case? Looking into the caller code I think this is the case, but we should confirm. In any case, I think this variable should be renamed to show more explicitly that it's about bound function calls.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I corrected that by renaming it to boundArgumentCall.
I've checked it in the code, that this is an optional with value only for functions in which funType->hasBoundFirstArgument() is true.

m_callGraph[m_currentContract].insert(funDef);
}

Expression const* calledExpr = &_funCall.expression();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calledExpr here has the meaning that I imagined at first during the previous review, but in BMC it means bound functions. We should clarify this

if (!usesStaticCall(_funCall))

std::optional<Expression const*> calledExpr =
_funType->hasBoundFirstArgument() ? std::make_optional(_calledExpr) : std::nullopt;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here it has the same behavior as BMC

Copy link
Member

@leonardoalt leonardoalt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, please squash

Copy link
Contributor

@blishko blishko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@leonardoalt leonardoalt merged commit b187d06 into ethereum:develop Oct 22, 2023
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for operators in SMTChecker
4 participants