Skip to content

Take the state at a join as the disjunction of the states behind it - #224

Draft
coord-e wants to merge 1 commit into
claude/call-type-from-calleefrom
claude/reduce-predicate-variables-1aeoll
Draft

Take the state at a join as the disjunction of the states behind it#224
coord-e wants to merge 1 commit into
claude/call-type-from-calleefrom
claude/reduce-predicate-variables-1aeoll

Conversation

@coord-e

@coord-e coord-e commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Last of three, on top of #227 (which is on top of #226). Each removes one predicate variable that stood for a fact the constraints already state elsewhere.

A block reached by more than one edge took a template inferred from a clause per incoming edge. The states its predecessors leave say exactly what it is entered in, so their disjunction is its precondition, and only a loop header still has one to infer: the state carried by its back edge is not yet known when it is analyzed. needs_own_precondition becomes is_loop_header accordingly.

A disjunction is no conjunct of a Horn clause body, so this holds only as long as no predicate variable appears in the states. Where one does, the disjunction is named by a predicate variable of its own, bounded from below by each state — which is what the block had all along. Body::into_formula decides which of the two applies.

The states are collected as the predecessors are analyzed and installed once they all have been, so a block that inherits its precondition holds the states until then rather than a flag.

Effect

Over tests/ui/pass (160 files):

predicate variables constraints
main 1691
after #226 1293 1088 KiB
after #227 734 917 KiB
after this 676 832 KiB

The path explosion this invites did not show up: the constraints came out 9% smaller, since naming a state costs an argument list at every use, which the disjunction rarely exceeds. Worst case loop_invariant_multi.rs at 1.23x (2.4 KiB to 2.9 KiB), best result_mut.rs at 0.35x (59 KiB to 21 KiB).

With all three, a predicate variable is generated only where something is genuinely unknown: the type of a function without an annotation, and a loop header without an invariant!. A fully annotated program with no loop generates constraints with none at all — an annotated fn abs(x: i64) -> i64 { if x >= 0 { x } else { -x } } comes out as a plain SMT problem and verifies.

Notes for review

  • Nothing pins the counts above. The generated constraints changing shape is invisible to the ui tests: while writing this I had Refinement::disjunction demand a predicate-variable-free form even for a single state, which quietly put a predicate variable back on every block that inherits, and all 316 tests still passed. A unit test over Refinement::disjunction would cover the case that broke; happy to add one.
  • is_loop_header reads a back edge off the dominator tree, which answers the question asked (is some predecessor analyzed after this block?) only for a reducible CFG. An irreducible one does not go unnoticed: the state arrives after the precondition is installed, and push_basic_block_precondition panics.
  • Hoisting the existential variables of every disjunct in front of the disjunction is valid as long as every sort is inhabited.

316 tests pass.

A block reached by more than one edge took a predicate variable for its
precondition, inferred from a clause per incoming edge. The states its
predecessors leave say exactly what it is entered in, so their
disjunction is its precondition, and only a loop header still has one to
infer: the state carried by its back edge is not yet known when it is
analyzed.

A disjunction is no conjunct of a Horn clause body, so this holds only as
long as no predicate variable appears in the states. Where one does, the
disjunction is named by a predicate variable of its own, bounded from
below by each state, which is what the block had all along.

`needs_own_precondition` becomes `is_loop_header` accordingly. The states
are collected as the predecessors are analyzed and installed once they
all have been, so a block that inherits its precondition now holds the
states until then rather than a flag.

Over the pass tests this drops the predicate variables from 734 to 676
and the constraints from 917 KiB to 832 KiB: naming a state costs an
argument list at every use, which the disjunction rarely exceeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7MyQbvfkfNy1h7yeN553N
@coord-e
coord-e force-pushed the claude/reduce-predicate-variables-1aeoll branch from 2e4e840 to 8a03fd7 Compare August 16, 2026 05:48
@coord-e coord-e changed the title Generate predicate variables only where something is left to infer Take the state at a join as the disjunction of the states behind it Aug 16, 2026
@coord-e
coord-e changed the base branch from main to claude/call-type-from-callee August 16, 2026 05:49
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