Skip to content

Commit ba94bdc

Browse files
authored
create solver_output in case of errors too (#570)
1 parent 7516619 commit ba94bdc

File tree

3 files changed

+57
-43
lines changed

3 files changed

+57
-43
lines changed

src/halmos/__main__.py

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,16 @@ def run_message(
777777
reset(solver)
778778

779779

780+
def is_benign_solving_error(e: Exception) -> bool:
781+
match e:
782+
case ShutdownError():
783+
return True
784+
case OSError(errno=9): # BAD_FILE_DESCRIPTOR
785+
return True
786+
case _:
787+
return False
788+
789+
780790
@dataclass(frozen=True)
781791
class CounterexampleHandler:
782792
"""Handles potential assertion violations and generates counterexamples."""
@@ -856,13 +866,35 @@ def handle_assertion_violation(
856866
partial(
857867
self._solve_end_to_end_callback,
858868
ex=ex,
869+
path_ctx=path_ctx,
859870
description=description,
860871
)
861872
)
862873
self.submitted_futures.append(solve_future)
863874

875+
def _get_solver_output(self, future: Future, path_ctx: PathContext) -> SolverOutput:
876+
path_id, query_file = path_ctx.path_id, str(path_ctx.dump_file)
877+
878+
if self.ctx.solving_ctx.executor.is_shutdown():
879+
# if the thread pool is in the process of shutting down,
880+
# we want to stop processing remaining models/timeouts/errors, etc.
881+
e = "executor has been shutdown"
882+
return SolverOutput.from_error(e, path_id=path_id, query_file=query_file)
883+
884+
if e := future.exception():
885+
if not is_benign_solving_error(e):
886+
error(f"encountered exception during assertion solving: {e!r}")
887+
return SolverOutput.from_error(e, path_id=path_id, query_file=query_file)
888+
889+
try:
890+
return future.result()
891+
except Exception as e:
892+
if not is_benign_solving_error(e):
893+
error(f"encountered exception during assertion solving: {e!r}")
894+
return SolverOutput.from_error(e, path_id=path_id, query_file=query_file)
895+
864896
def _solve_end_to_end_callback(
865-
self, future: Future, ex: Exec, description: str
897+
self, future: Future, ex: Exec, path_ctx: PathContext, description: str
866898
) -> None:
867899
"""
868900
Callback function for handling solver results.
@@ -878,52 +910,19 @@ def _solve_end_to_end_callback(
878910
ctx = self.ctx
879911
args = ctx.args
880912

881-
if ctx.solving_ctx.executor.is_shutdown():
882-
# if the thread pool is in the process of shutting down,
883-
# we want to stop processing remaining models/timeouts/errors, etc.
884-
return
885-
886-
if e := future.exception():
887-
if isinstance(e, ShutdownError):
888-
return
889-
890-
# we close file descriptors forcibly to avoid this issue:
891-
# https://github.com/a16z/halmos/pull/527
892-
if isinstance(e, OSError) and e.errno == BAD_FILE_DESCRIPTOR:
893-
return
894-
895-
error(f"encountered exception during assertion solving: {e!r}")
896-
return
897-
898-
#
899-
# we are done solving, process and triage the result
900-
#
901-
902-
try:
903-
solver_output: SolverOutput = future.result()
904-
except OSError as e:
905-
if e.errno == BAD_FILE_DESCRIPTOR:
906-
return
907-
908-
# re-raise other OSErrors
909-
raise
910-
913+
solver_output: SolverOutput = self._get_solver_output(future, path_ctx)
914+
ctx.solver_outputs.append(solver_output)
911915
result, model = solver_output.result, solver_output.model
912916
path_id = solver_output.path_id
913917

914-
# keep track of the solver outputs, so that we can display PASS/FAIL/TIMEOUT/ERROR later
915-
ctx.solver_outputs.append(solver_output)
916-
917918
if result == unsat:
918919
if solver_output.unsat_core:
919920
ctx.append_unsat_core(solver_output.unsat_core)
920921
return
921922

922923
if result == "err":
924+
error(f"{solver_output.error=} ({solver_output.returncode=})")
923925
self._save_failed_query(path_id, solver_output, "error")
924-
error(
925-
f"solver error: {solver_output.error} (returncode={solver_output.returncode})"
926-
)
927926
return
928927

929928
# handle "unknown" (timeout) result
@@ -980,9 +979,9 @@ def _save_failed_query(
980979
ctx = self.ctx
981980
args = ctx.args
982981

982+
query_file = solver_output.query_file
983983
debug_dir = f"{dirname(ctx.solving_ctx.dump_dir)}-{failure_type}"
984984
os.makedirs(debug_dir, exist_ok=True)
985-
query_file = solver_output.query_file
986985
debug_query_file = os.path.join(debug_dir, os.path.basename(query_file))
987986
try:
988987
shutil.copy2(query_file, debug_query_file)

src/halmos/solve.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,21 @@ def from_result(
356356
"err", returncode, path_id, query_file, error=stderr
357357
)
358358

359+
@staticmethod
360+
def from_error(
361+
error: Exception | str,
362+
path_id: int,
363+
query_file: str,
364+
returncode: int = -1,
365+
) -> "SolverOutput":
366+
return SolverOutput(
367+
result="err",
368+
returncode=returncode,
369+
path_id=path_id,
370+
query_file=query_file,
371+
error=str(error),
372+
)
373+
359374

360375
def parse_const_value(value: str) -> int:
361376
match value[:2]:

tests/test_config.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -356,14 +356,14 @@ def test_solver_resolution_precedence(config):
356356
# command line overrides
357357
#########################################################
358358

359-
cli_config = config.with_overrides(ConfigSource.command_line, solver="cvc5")
360-
assert cli_config.solver == "cvc5"
359+
cli_config = config.with_overrides(ConfigSource.command_line, solver="z3")
360+
assert cli_config.solver == "z3"
361361

362362
# the solver command is inherited from the default config
363363
assert cli_config.solver_command == config.solver_command
364364

365365
# but the actual command is derived from the solver option (at a higher precedence)
366-
assert "cvc5" in " ".join(cli_config.resolved_solver_command)
366+
assert "z3" in " ".join(cli_config.resolved_solver_command)
367367

368368
#########################################################
369369
# contract annotation overrides
@@ -375,7 +375,7 @@ def test_solver_resolution_precedence(config):
375375
)
376376
# the solver option is inherited from the command line config
377377
assert contract_config.value_with_source("solver") == (
378-
"cvc5",
378+
"z3",
379379
ConfigSource.command_line,
380380
)
381381

@@ -384,7 +384,7 @@ def test_solver_resolution_precedence(config):
384384

385385
# the resolved solver command is derived from the solver option (command line)
386386
# because command line has higher precedence than contract annotation
387-
assert "cvc5" in " ".join(contract_config.resolved_solver_command)
387+
assert "z3" in " ".join(contract_config.resolved_solver_command)
388388

389389

390390
def test_command_line_precedence_over_annotations(config):

0 commit comments

Comments
 (0)