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

fix --early-exit #437

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open

fix --early-exit #437

wants to merge 42 commits into from

Conversation

0xkarmacoma
Copy link
Collaborator

fixes #243

high-level notes:

  • the thread pool we were using during the assertion solving process did not allow us to interrupt work that was already started (thread_pool.shutdown(wait=False) was not what we needed it to be)
  • in order to be interruptible, we now run every solver instance as an external process (an actual subprocess.Popen object, wrapped for convenience in PopenFuture and scheduled with PopenExecutor)
  • when we call PopenExecutor.shutdown(wait=False), it does terminate/kill every process that has been submitted to that executor and is still running
  • an executor is scoped to a test function, so that a successful counterexample found for a path in a test function cancels the other solver instances for that test, but not for other tests that may be running concurrently
  • in order to support refinement, we still submit solve_end_to_end function calls to a thread pool, but that functions submits solve_low_level calls to the executor and blocks on the result. solve_low_level can be interrupted if the underlying process returns, times out, gets killed, etc -- this is the key architectural change
  • as a result, we no longer invoke the built-in z3 solve and every solver invocation is now an external solver invocation (hence --solver-command defaults to z3) and we need to parse the model values from the smtlib output. This means we no longer print Counterexample: see xyz.smt2

Additionally, to make the transition to this model easier I introduced a hierarchy of context objects:

  • ContractContext
  • FunctionContext
  • PathContext
  • each context has the associated args (HalmosConfig) with the relevant scoped overrides and intermediary values needed

@0xkarmacoma
Copy link
Collaborator Author

Things I checked:

  • works as expected without an explicit --solver-command (invokes an external z3 process)
  • works when passing an explicit --solver-command (caveat: bitwuzla and cvc require --produce-models, yices requires --smt2-model-format)
  • refinement works
  • timeouts work
  • we try to avoid leaving orphaned solvers behind, especially during timeouts
  • --early-exit really does print the first counterexample and cancels the other processes

Things I still need to check:

  • Windows support (I think --solver-command was never supported on Windows, but we now always require it)
  • ctrl-c behavior is likely wonky (it always was, but it should be fixable now, maybe as an additional improvement later)

@@ -0,0 +1,174 @@
import json
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no changes here, just moving build-related functionality out of __main__.py and into its own module

@@ -0,0 +1,160 @@
import io
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no changes here, just moving trace-related functionality out of __main__.py and into its own module

@@ -0,0 +1,217 @@
import concurrent.futures
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

new module that provides an executor/futures abstraction layer built on top of subprocess.Popen

return SolverOutput.from_result(stdout, stderr, returncode, path_ctx)


def solve_end_to_end(ctx: PathContext) -> None:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

used to be gen_model_from_sexpr

@dataclass(frozen=True)
class PathContext:
# id of this path
path_id: int
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

roughly corresponds to the old iteration idx


refined_ctx = ctx.refine()

if refined_ctx.query.smtlib != query.smtlib:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

related: #279

@@ -1411,6 +817,8 @@ def _main(_args=None) -> MainResult:
#

def on_exit(exitcode: int) -> MainResult:
ExecutorRegistry().shutdown_all()
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't understand what's going on, this sounds like a safe thing to do (shut down external processes) but when I do this I can somewhat consistently get the following crash when triggering a ctrl-c at the right time:

halmos --root ~/projects/halmos-sandbox --function test_manyCexes --solver-command "yices-smt2 --smt2-model-format" --early-exit                                                         ✘ INT took 4s
[⠢] Compiling...
No files changed, compilation skipped

Running 1 tests for test/60_manyCexes.t.sol:Test60
setup: 0.01s (decode: 0.01s)
Generating SMT queries in /tmp/test_manyCexes-2459149073354744aa4b653b5fb94c9f
^C
Shutting down all executors
ASSERTION VIOLATION
File: ../src/ast/ast.cpp
Line: 406
UNEXPECTED CODE WAS REACHED.
Z3 4.12.6.0
Please file an issue with this message and more detail about how you encountered it at https://github.com/Z3Prover/z3/issues/new

(note that the interrupted assertion solver is yices, not z3 -- I don't understand why z3 is involved in this)

@0xkarmacoma
Copy link
Collaborator Author

TODO: fix the case where:

  • path exploration is running long
  • a counterexample is found early on
  • the solver executor is shut down, but path exploration continues
  • path exploration yield new potential paths to solve
  • we submit these new paths to the executor, but this causes an exception
ERROR    encountered exception during assertion solving: RuntimeError('Cannot submit to a shutdown executor.')                                                                                                                                                                                                               
ERROR    exception calling callback for <Future at 0x112aee4e0 state=finished raised RuntimeError>                                                                                                                                                                                                                           
         Traceback (most recent call last):                                                                                                                                                                                                                                                                                  
           File "/Users/karma/.pyenv/versions/3.12.1/lib/python3.12/concurrent/futures/_base.py", line 340, in _invoke_callbacks                                                                                                                                                                                             
             callback(self)                                                                                                                                                                                                                                                                                                  
           File "/Users/karma/projects/halmos/src/halmos/__main__.py", line 472, in solve_end_to_end_callback                                                                                                                                                                                                                
             solver_output = future.result()                                                                                                                                                                                                                                                                                 
                             ^^^^^^^^^^^^^^^                                                                                                                                                                                                                                                                                 
           File "/Users/karma/.pyenv/versions/3.12.1/lib/python3.12/concurrent/futures/_base.py", line 449, in result                                                                                                                                                                                                        
             return self.__get_result()                                                                                                                                                                                                                                                                                      
                    ^^^^^^^^^^^^^^^^^^^                                                                                                                                                                                                                                                                                      
           File "/Users/karma/.pyenv/versions/3.12.1/lib/python3.12/concurrent/futures/_base.py", line 401, in __get_result                                                                                                                                                                                                  
             raise self._exception                                                                                                                                                                                                                                                                                           
           File "/Users/karma/.pyenv/versions/3.12.1/lib/python3.12/concurrent/futures/thread.py", line 58, in run                                                                                                                                                                                                           
             result = self.fn(*self.args, **self.kwargs)                                                                                                                                                                                                                                                                     
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                                                                                                                                                                                                                                     
           File "/Users/karma/projects/halmos/src/halmos/solve.py", line 438, in solve_end_to_end                                                                                                                                                                                                                            
             solver_output = solve_low_level(ctx)                                                                                                                                                                                                                                                                            
                             ^^^^^^^^^^^^^^^^^^^^                                                                                                                                                                                                                                                                            
           File "/Users/karma/projects/halmos/src/halmos/solve.py", line 399, in solve_low_level                                                                                                                                                                                                                             
             path_ctx.solving_ctx.executor.submit(future)                                                                                                                                                                                                                                                                    
           File "/Users/karma/projects/halmos/src/halmos/processes.py", line 161, in submit                                                                                                                                                                                                                                  
             raise RuntimeError("Cannot submit to a shutdown executor.")                                                                                                                                                                                                                                                     
         RuntimeError: Cannot submit to a shutdown executor. 

We need some way to also interrupt path exploration when early exit is triggered.

@0xkarmacoma
Copy link
Collaborator Author

0xkarmacoma commented Jan 23, 2025

TODO: fix the case where:

* path exploration is running long

* a counterexample is found early on

* the solver executor is shut down, but path exploration continues

* path exploration yield new potential paths to solve

* we submit these new paths to the executor, but this causes an exception
ERROR    encountered exception during assertion solving: RuntimeError('Cannot submit to a shutdown executor.')                                                                                                                                                                                                               
ERROR    exception calling callback for <Future at 0x112aee4e0 state=finished raised RuntimeError>                                                                                                                                                                                                                           
         Traceback (most recent call last):                                                                                                                                                                                                                                                                                  
           File "/Users/karma/.pyenv/versions/3.12.1/lib/python3.12/concurrent/futures/_base.py", line 340, in _invoke_callbacks                                                                                                                                                                                             
             callback(self)                                                                                                                                                                                                                                                                                                  
           File "/Users/karma/projects/halmos/src/halmos/__main__.py", line 472, in solve_end_to_end_callback                                                                                                                                                                                                                
             solver_output = future.result()                                                                                                                                                                                                                                                                                 
                             ^^^^^^^^^^^^^^^                                                                                                                                                                                                                                                                                 
           File "/Users/karma/.pyenv/versions/3.12.1/lib/python3.12/concurrent/futures/_base.py", line 449, in result                                                                                                                                                                                                        
             return self.__get_result()                                                                                                                                                                                                                                                                                      
                    ^^^^^^^^^^^^^^^^^^^                                                                                                                                                                                                                                                                                      
           File "/Users/karma/.pyenv/versions/3.12.1/lib/python3.12/concurrent/futures/_base.py", line 401, in __get_result                                                                                                                                                                                                  
             raise self._exception                                                                                                                                                                                                                                                                                           
           File "/Users/karma/.pyenv/versions/3.12.1/lib/python3.12/concurrent/futures/thread.py", line 58, in run                                                                                                                                                                                                           
             result = self.fn(*self.args, **self.kwargs)                                                                                                                                                                                                                                                                     
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                                                                                                                                                                                                                                     
           File "/Users/karma/projects/halmos/src/halmos/solve.py", line 438, in solve_end_to_end                                                                                                                                                                                                                            
             solver_output = solve_low_level(ctx)                                                                                                                                                                                                                                                                            
                             ^^^^^^^^^^^^^^^^^^^^                                                                                                                                                                                                                                                                            
           File "/Users/karma/projects/halmos/src/halmos/solve.py", line 399, in solve_low_level                                                                                                                                                                                                                             
             path_ctx.solving_ctx.executor.submit(future)                                                                                                                                                                                                                                                                    
           File "/Users/karma/projects/halmos/src/halmos/processes.py", line 161, in submit                                                                                                                                                                                                                                  
             raise RuntimeError("Cannot submit to a shutdown executor.")                                                                                                                                                                                                                                                     
         RuntimeError: Cannot submit to a shutdown executor. 

We need some way to also interrupt path exploration when early exit is triggered.

✅ in 1fc8649

Copy link
Collaborator

@daejunpark daejunpark left a comment

Choose a reason for hiding this comment

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

overall looks good to me. thanks for this huge effort!

i focused on checking refactoring preserves existing behaviors, and left some comments.

i didn't review the new process module in detail.

render_trace(setup_ex.context)

else:
setup_exs_no_error.append(setup_ex)
Copy link
Collaborator

Choose a reason for hiding this comment

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

this refactoring is incorrect. the smt query needs to be generated at this point. the solver object is shared across paths, and solver.to_smt2() will return a different query if it is called after a different path is explored.

alternatively, we could consider modifying path.to_smt2() to not rely on the shared solver. the idea is to create a temporary new solver with a separate z3 context, add all path constraints to the new solver, and generate a smt query from it. this indeeds already happens when --cache-solver is enabled. so this approach would improve the code maintainability avoiding this potential mistake in the future. but, a downside is the additional performance overhead of creating a new solver and translating all constraints to the new context. wdyt?

path_ctx = PathContext(
args=args,
path_id=path_id,
query=ex.path.to_smt2(args),
Copy link
Collaborator

@daejunpark daejunpark Jan 24, 2025

Choose a reason for hiding this comment

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

as mentioned above, calling path.to_smt2() here will generate an incorrect query unless it's the last path.

the ci failure might be related to this:
https://github.com/a16z/halmos/actions/runs/12958246417/job/36148232775#step:9:27

src/halmos/__main__.py Outdated Show resolved Hide resolved
src/halmos/__main__.py Outdated Show resolved Hide resolved
libs: dict

# TODO: check if this is really a contract-level variable
build_out_map: dict
Copy link
Collaborator

Choose a reason for hiding this comment

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

fyi, build_out_map is shared across all contracts compiled using the same compiler version. so in principle, we could consider having another context, say CompileUnitContext, and put build_out_map there, but it might be overkill unless there are other compiler specific items. (the current approach seems reasonable to me.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

agree, I will leave a comment

def __str__(self) -> str:
# expected to be a filename
if isinstance(self.model, str):
return f"see {self.model}"
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we still need this? we now parse the solver output, and no longer show the filename, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

that's right! forgot about this


if refined_ctx.query.smtlib != query.smtlib:
# recurse with the refined query
return solve_end_to_end(refined_ctx)
Copy link
Collaborator

Choose a reason for hiding this comment

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

why solve_end_to_end instead of solve_low_level? note that check_unsat_cores cannot return true for the refined query, because it relies on only constraint ids, which don't change after refinement.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ah good catch, I wasn't sure so solve_end_to_end seemed more general. Can switch it to solve_low_level if you think that works

Copy link
Collaborator

@daejunpark daejunpark Jan 27, 2025

Choose a reason for hiding this comment

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

yes, solve_low_level() should work here: resulting in the same output, and avoiding the unnecessary unsat_core loop.

src/halmos/solve.py Show resolved Hide resolved
src/halmos/processes.py Show resolved Hide resolved
src/halmos/__main__.py Show resolved Hide resolved
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.

--early-exit doesn't work as expected
2 participants