Generate predicate variables only where something is left to infer - #224
Draft
coord-e wants to merge 3 commits into
Draft
Generate predicate variables only where something is left to infer#224coord-e wants to merge 3 commits into
coord-e wants to merge 3 commits into
Conversation
A call bound its result to a template, and the return type of the callee was related to it by subtyping, leaving a predicate variable to be inferred for what the callee already states. Instantiate the return type of the callee at the arguments and bind that instead. Adapting the arguments to the parameter list of the callee now works on `PlaceType`s, which lets a `rust-call` tuple be untupled by projection rather than by rewriting refinements, and keeps the arguments as terms so that no variable stands between a call and its result. This predicate variable also served as a cut point for the solver. Until the entry block gives up its own in the commit that follows, tests/ui/pass/mut_recursive.rs takes z3 well past its timeout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P7MyQbvfkfNy1h7yeN553N
The entry block took a template for its precondition, which
`assert_entry` then bounded from below by the precondition of the
function. Install the precondition of the function directly, equating
each parameter with the value it has on entry, which is what the
predicate variable stood for.
The parameter types come from the signature as well, so that refinements
written in it reach the block: a refinement nested in a type
(`Vec<{ v: i32 | v > 0 }>`) or the contract of a function-typed
parameter has nowhere else to come from once `assert_entry` is gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7MyQbvfkfNy1h7yeN553N
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three of the predicate variables we generate stood for facts the constraints already state elsewhere, leaving a solver to infer what is written down. Each commit removes one.
ca7d6c0Take the type of a call from the calleeA call bound its result to a template and related the return type of the callee to it by subtyping. The callee already says everything known about its result, so its return type is instantiated at the arguments and bound directly.
Adapting the arguments to the parameter list of the callee now works on
PlaceTypes, which lets arust-calltuple be untupled by projection instead of by rewriting refinements, and keeps the arguments as terms so no variable stands between a call and its result.9429b01Take the precondition of the entry block from the signatureThe entry block took a template that
assert_entrythen bounded from below by the precondition of the function. It is installed directly instead, equating each parameter with the value it has on entry. The parameter types come from the signature too, so that refinements written there — one nested in a type (Vec<{ v: i32 | v > 0 }>), or the contract of a function-typed parameter — still reach the block onceassert_entryis gone.2e4e840Take the state at a join as the disjunction of the states behind itA 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.
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.
Effect
Over
tests/ui/pass(160 files):mainThe path explosion the third commit 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. The worst case was
loop_invariant_multi.rsat 1.23x (2.4 KiB to 2.9 KiB) and the bestresult_mut.rsat 0.35x (59 KiB to 21 KiB).What is left generates a predicate variable 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 now generates constraints with none at all — an annotatedfn abs(x: i64) -> i64 { if x >= 0 { x } else { -x } }comes out as a plain SMT problem and verifies.Notes for review
ca7d6c0failstests/ui/pass/mut_recursive.rs. The predicate variable it removes also served as a cut point, and z3 needs far longer than its timeout on that test until the entry block gives up its own in9429b01. Every other commit is green, and so is the head of the branch (316 passing). Worth knowing before bisecting.adapt_args_to_abiinca7d6c0: therust-callexpansion both moved out ofrelate_fn_param_sub_types_with_builderand was rewritten, so git shows it as a delete plus an add.9429b01and are worth a second pair of eyes: the equation between each parameter and itsOuterFnParamcopy (the least solution of the removed predicate variable), andinstall_signature_types, without which a nested refinement or a function-typed parameter's contract would be lost — silently, and unsoundly for the latter.Refinement::disjunctiondemand 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 overRefinement::disjunctionwould cover the case that broke; happy to add one.Generated by Claude Code