Keep the removed parameter's refinement in remove_param - #204
Merged
Conversation
coord-e
force-pushed
the
claude/issue-197-investigation-db8cnb
branch
from
August 6, 2026 10:58
9c5d024 to
f4accb0
Compare
A function type keeps its precondition on the last parameter, and `remove_param` dropped the refinement of the parameter it removed. `drop_bb_zst_params` removes the entry block parameters whose local is not a function argument, so whenever the last local of the entry block is a ZST temporary (a non-capturing closure, a unit struct, ...) the precondition was lost with it. The predicate variable then occurred only in clause bodies, so the solver satisfied the system by reading it as `false`, and every obligation after that point was discharged vacuously: a program that always panics verified as safe. Conjoin the refinement to the last remaining parameter instead, so that removal weakens neither the precondition nor the grounding clause built by `assert_entry`. Fixes #197 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GRJRPDgwPPxHcgaJqiMftB
coord-e
force-pushed
the
claude/issue-197-investigation-db8cnb
branch
from
August 6, 2026 11:03
f4accb0 to
249c259
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes an unsoundness where removing a basic-block entry parameter could accidentally drop the function precondition refinement (stored on the last parameter), leaving the precondition predicate unconstrained and causing panicking programs to verify as safe.
Changes:
- Update
FunctionType::remove_paramto preserve the removed parameter’s refinement by conjoining it onto the last remaining parameter. - Add UI regression tests that exercise non-capturing closure calls inside a
#[thrust::callable]function with a parameter (both pass and fail variants).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/rty.rs |
Preserves refinements when removing a parameter by merging the removed refinement into the last remaining parameter. |
tests/ui/pass/closure_no_capture_fn_param.rs |
Adds a passing regression test for non-capturing closure calls in a parameterized callable function. |
tests/ui/fail/closure_no_capture_fn_param.rs |
Adds a failing regression test to ensure false assertions remain rejected after such a closure call. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
Fixes #197.
Root cause
A function type keeps its precondition on the last parameter (
BasicBlockType::set_precondition,truncate_outer_fn_params), butremove_paramdropped the refinement of the parameter it removed.The entry basic block type orders its local parameters by
Localindex, so they run_0(RETURN_PLACE), then the arguments, then any temporary that liveness reports as live at entry — which happens for ZST locals such as a non-capturing closure or a unit struct.drop_bb_zst_paramsremoves every parameter whose local is not a function argument, so when the last local is such a temporary, the precondition went with it andassert_entrynever emitted a clause with that predicate variable in a head. The predicate then occurred only in clause bodies, so the solver satisfied the system by reading it as the empty relation, and every obligation after that point was discharged vacuously — a program that always panics verified assafe.Only the
arg_count == 0case survived, becausedrop_bb_zst_paramsre-introduces a synthetic unit parameter carrying the refinement of the last dropped parameter.Before, for
fn check(v: i32) { let f = |a: i32| a; f(v); assert!(1 == 2); }:p4is gone, and the emitted CHC system contains no clause withp4in a head.Trigger
The trigger is broader than the issue describes: any function with at least one argument whose entry block has a live non-argument ZST local. Neither an argument at the call site nor a closure at all is required —
fn check(v: i32) { let z = Z; take(&z); assert!(1 == 2); }for a unit structZverifies assafetoo.Change
remove_paramnow conjoins the removed parameter's refinement to the last remaining parameter, rebinding the reference to that parameter as the value variable. Removal no longer weakens the precondition, and callers do not have to know where a function type keeps it.Testing
tests/ui/{pass,fail}/closure_no_capture_fn_param.rsadded. Thefailone is rejected without this change and passes with it.Unsatnow;assert!(f(v) == v)still verifies assafe.cargo test,cargo fmt --check,cargo clippy -- -D warnings. The test run has 32 pre-existing failures in this environment because PCSat (tests/thrust-pcsat-wrapper) is unavailable; the failing set is identical with and without this change.