Skip to content

Passes for CHC-based invariant inference - #168

Merged
aaronbembenek merged 44 commits into
UQ-PAC:mainfrom
aaronbembenek:chc-inv-infer
Jun 19, 2026
Merged

Passes for CHC-based invariant inference#168
aaronbembenek merged 44 commits into
UQ-PAC:mainfrom
aaronbembenek:chc-inv-infer

Conversation

@aaronbembenek

@aaronbembenek aaronbembenek commented May 28, 2026

Copy link
Copy Markdown
Collaborator

This PR adds passes for inferring program invariants (loop invariants, and pre/postconditions) in the Basil IL using CHC solving. In short, the passes encode the program's IL as CHCs, and then uses a CHC solver to find a model; the invariants are extracted from the CHC model and then added to the IL as assertions.

The two invariant-inference passes are:

  • chc-infer-invariants: this pass is all or nothing - it finds invariants strong enough to discharge all obligations, or nothing
  • chc-infer-invariants-per-query: this pass infers invariants on a per-query basis (e.g., per assertion/pre/postcondition), which means that it can find invariants that are strong enough to discharge some, but not all obligations

Right now, we use Z3/Spacer to do the CHC solving; it should be possible to add support for other CHC solvers, like Eldarica, in the future.

The encoding of the IL into CHCs is based on the encoding used in this paper: Bit-Vector CHC Solving for Binary Analysis and Binary Analysis for Bit-Vector CHC Solving. However, we have augmented the encoding to support pre/postcondition specs for procedures.

  • If a procedure has a precondition, a query is generated checking it at all call sites
  • If a procedure has a postcondition, a query is generated checking it at the end of that procedure
  • The encoding function takes in a predicate use_specs over procedures; if use_specs p is true for some procedure p, then the pre/postconditions for p are inlined at call sites of p, instead of generating CHCs encoding that control flow goes to the actual body of p
  • By default, use_specs p returns true iff the procedure p has non-trivial specifications

This PR also adds a load-store-reduction pass, which uses some of the pre-processing infrastructure from the Boogie backend to simplify the IL; it is required to run this pass before the CHC invariant inference passes. In particular, we require this sequence of passes to be run first: full-ssa → load-store-reduction → lambda-lifting.

@katrinafyi
katrinafyi removed their request for review May 28, 2026 03:25

@agle agle left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The code generally looks fine but the load/store encoding seems to not work yet. For the final version it would be good to achieve parity with the python implementation up to the loopy benchmarks if its possible. We can set the test up like here https://github.com/UQ-PAC/bincaml-test/tree/main/gtirb, happy to help with that if needed.

full-ssa → load-store-reduction → lambda-lifting this sequence requirement can also be expressed now with the new pass dependency system.

Comment thread lib/transforms/chc_infer.ml Outdated
Comment thread lib/transforms/chc_infer.ml Outdated
(precondition queries at the call site, postcondition queries on the body)
is performed independently of [use_spec] whenever the callee has a
[requires] or [ensures]. *)
let encode_block ~(use_spec : Program.proc -> bool) (prog : Program.t)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Most of the inner functions in this function should be made top-level so its clear they only depend on their in-parameters rather than any prior local definitions. In particular translation of regular statements, calls, and blocks should be different top-level functions.

Comment thread lib/transforms/chc_infer.ml Outdated
Comment thread test/cram/load_store_reduction.t Outdated
Comment thread lib/transforms/chc_infer.ml Outdated
Comment thread lib/transforms/chc_infer.ml Outdated
Comment thread lib/transforms/chc_infer.ml Outdated
@aaronbembenek
aaronbembenek force-pushed the chc-inv-infer branch 2 times, most recently from fdcdb3e to 12a5096 Compare June 10, 2026 06:35
@aaronbembenek

Copy link
Copy Markdown
Collaborator Author

full-ssa → load-store-reduction → lambda-lifting this sequence requirement can also be expressed now with the new pass dependency system.

This has been simplified as load-store reduction and lambda-lifting now happen internally in the CHC encoding process. I have added SSA as a dependency. Technically, the CHC pass works fine without SSA. However, I think it still makes sense to include it as a dependency, so that people run SSA before it. If SSA is run after it, the loop invariants can get messed up: the assertions (encoding the invariants) are no longer the first statement in the block, which seems to have the effect of Boogie no longer accepting them.

@aaronbembenek

Copy link
Copy Markdown
Collaborator Author

For the final version it would be good to achieve parity with the python implementation up to the loopy benchmarks if its possible. We can set the test up like here https://github.com/UQ-PAC/bincaml-test/tree/main/gtirb, happy to help with that if needed.

Loopy tests added here: UQ-PAC/bincaml-test#1

The CHC pass completes on 460/468 benchmarks. For 3 benchmarks, Z3 crashes; for the other 5, bincaml SSA crashes (I can open an issue).

CHC solver results (timeout of 10s):

  • 327 sat
  • 23 unsat
  • 110 unknown

Boogie verifies 316 benchmarks (annotated with the inferred invariants).

@aaronbembenek
aaronbembenek requested a review from agle June 12, 2026 05:20
| _ -> BasilExpr.fix node)
| None -> BasilExpr.fix node)
| _ -> BasilExpr.fix node)
| _ -> BasilExpr.fix node)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This should be much simpler, with BasilExpr.unfix3 and BasilExpr.rewrite_down rather than cata; it should have one match on multiple levels of the ast rather than 4 levels of nested match. It probably makes mores sense using downard recursion rather than upward.

Comment thread lib/lang/expr_smt.ml Outdated
Comment thread lib/lang/expr_smt.ml Outdated
Comment thread lib/transforms/chc_infer.ml Outdated
Comment thread lib/transforms/chc_solve.ml Outdated
Comment thread lib/transforms/chc_solve.ml Outdated
in
list [ atom "assert"; forall_ binders body ]

type solve_result = Sat | Unsat | Unknown

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could put the model in the SAT case rather than returning a tuple

Comment thread lib/transforms/chc_solve.ml Outdated
@aaronbembenek
aaronbembenek merged commit 5e6368e into UQ-PAC:main Jun 19, 2026
9 checks passed
@aaronbembenek
aaronbembenek deleted the chc-inv-infer branch June 19, 2026 06:16
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.

2 participants