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

perf: early exit for fail() #322

Merged
merged 8 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions src/halmos/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,46 @@
"""


class InfeasiblePath(Exception):
class PathEndingException(Exception):
"""
Base class for any exception that should stop the current path exploration.

Stopping path exploration means stopping not only the current EVM context but also its parent contexts if any.
"""

pass


class HalmosException(Exception):
class HalmosException(PathEndingException):
"""
Base class for unexpected internal errors happening during a test run.
Inherits from RunEndingException because it should stop further path exploration.
"""

pass


class NotConcreteError(HalmosException):
pass


class InfeasiblePath(PathEndingException):
"""
Raise when the current path condition turns out to be infeasible.
"""

pass


class FailCheatcode(PathEndingException):
"""
Raised when invoking DSTest's fail() pseudo-cheatcode.
Inherits from RunEndingException because it should stop further path exploration.
"""

pass


class EvmException(Exception):
"""
Base class for all EVM exceptions.
Expand Down Expand Up @@ -136,14 +164,6 @@ class InvalidContractPrefix(ExceptionalHalt):
pass


class FailCheatcode(ExceptionalHalt):
"""
Raised when invoking hevm's vm.fail() cheatcode
"""

pass


class AddressCollision(ExceptionalHalt):
"""
Raised when trying to deploy into a non-empty address
Expand Down
6 changes: 6 additions & 0 deletions src/halmos/sevm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2544,6 +2544,12 @@ def finalize(ex: Exec):
yield from finalize(ex)
continue

except FailCheatcode as err:
# return data shouldn't be None, as it is considered being stuck
ex.halt(data=ByteVec(), error=err)
yield ex # early exit; do not call finalize()
continue

def mk_exec(
self,
#
Expand Down
24 changes: 23 additions & 1 deletion tests/expected/all.json
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@
{
"name": "check_call1_fail(uint256)",
"exitcode": 1,
"num_models": 2,
"num_models": 1,
"models": null,
"num_paths": null,
"time": null,
Expand Down Expand Up @@ -766,6 +766,28 @@
"num_bounded_loops": null
}
],
"test/Foundry.t.sol:DeepFailer": [
{
"name": "check_fail_cheatcode()",
"exitcode": 1,
"num_models": 1,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
}
],
"test/Foundry.t.sol:EarlyFailTest": [
{
"name": "check_early_fail_cheatcode(uint256)",
"exitcode": 1,
"num_models": 1,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
}
],
"test/Foundry.t.sol:FoundryTest": [
{
"name": "check_assume(uint256)",
Expand Down
20 changes: 19 additions & 1 deletion tests/regression/test/Foundry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,29 @@ contract DeepFailer is Test {
}
}

function test_fail_cheatcode() public {
function check_fail_cheatcode() public {
DeepFailer(address(this)).do_test(0);
}
}

contract EarlyFailTest is Test {
function do_fail() external {
fail();
}

function check_early_fail_cheatcode(uint x) public {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nice test 👍

// we want `fail()` to happen in a nested context,
// to test that it ends not just the current context but the whole run
address(this).call(abi.encodeWithSelector(this.do_fail.selector, ""));
daejunpark marked this conversation as resolved.
Show resolved Hide resolved

// this shouldn't be reached due to the early fail() semantics.
// if this assertion is executed, two counterexamples will be generated:
// - counterexample caused by fail(): x > 0
// - counterexample caused by assert(x > 0): x == 0
assert(x > 0);
}
}

contract FoundryTest is Test {
/* TODO: support checkFail prefix
function checkFail() public {
Expand Down
Loading