Specification refinement#133
Conversation
9d38699 to
5d792c1
Compare
|
We currently translate each specification item in a #[refine_spec(where T: Bar, [requires(v > 0), requires(v < 10)])]is translated to fn spec_item_pre_0<T: Bar>(v: i32) -> bool { v > 0 }
fn spec_item_pre_1<T: Bar>(v: i32) -> bool { v < 10 }This is problematic in the sense that we get two "different" specification refinements despite the fact that they use the same #[refine_spec(where T: Bar, [requires(v > 0 && v < 10)])]We could alternatively try to group the specification items, and then assert that there is only one refinement group. What do you think? Additionally, it would be now legal to write such a refinement: #[refine_spec(where T: Foo, [requires(v > 0)])]
#[refine_spec(where T: Bar, [ensures(result == 42)])]As there is at most one refinement for pre/post conditions, this is deemed legal. Personally, I don't see an issue with this, but I'd like to know if you think this should be allowed or not. |
Where do we reject this? We can make the spec rewriter produce multiple spec items with, e.g., an extra attribute (containing another UUID I guess) attached to each. When identifying the specs after typing, we would group by this new attribute. (I prefer this to grouping by the where clause, in particular I worry about syntactically different clauses that actually have the same constraints.)
I would say this is not consistent with the rules we are trying to enforce, but I am also not completely opposed to lifting the restriction. However, we need to enforce behavioural subtyping correctly. For example, suppose a method had:
For this method, we should enforce that B is a b.s. of A, C is a b.s. of A, and D is a b.s. of both B and C. This PR doesn't implement any behavioural subtyping checks yet, but maybe that's what your goal was for now @JonasAlaif? |
|
A more general question is about how we treat specification items more generally. Consider the following example: #[requires(y > 0)]
#[ensures(result == 42)]
fn fun<T>(x: T, y: i32) -> i32 { ... }In reality, this should really be: #[requires(false)]
#[ensures(true)]
#[refine_spec(where T: Sized, [requires(y > 0), ensures(result == 42)])]This is due to the implicit |
We have discussed with @JonasAlaif to implement behavioral subtyping checks in a separate PR later. |
I don't think so? The method is not callable when Besides, the b.s. check for the above is trivially true, so we don't really need to emit it, and any actual |
Since we are talking about multiple #[requires(<cond1>)
#[refine_spec(where <where1>, [requires(<cond2>])]
#[refine_spec(where <where2>, [requires(<cond3>])]Should we generate the following? What if the user were to now manually add a refinement with If the user would now supply a type that is both |
I have not yet pushed the changes that would reject multiple refinements as I'm still figuring out the exact semantics. Maybe we could aim for a structure like so: https://hackmd.io/@7_G-0elATUmQnp4YTzUs0A/SkYJtr3YWl |
5d792c1 to
9db797a
Compare
This has been fixed by grouping related
This is now not allowed and will cause a compiler crash (we opt for crashing over reporting an error as there might be an external |
|
One last thing that we might want to address at some point is the fact that the way we currently construct the impl bounds includes extra constraints that the user did not write. Looking at this example: #[refine_spec(where T: Foo, [requires(v > 0])]
fn fun<T, U: Bar>(v: i32) {}This results in the following requires item: fn fun_pre<T, U: Bar>(v: i32) where T: Foo {}Causing us to generate a check: While the following would suffice: To my knowledge this does not affect correctness, but does introduce unnecessary overhead. #[refine_spec(where T: ?Sized, [requires(v > 0)])]
#[requires(v > 127)]
#[trusted]
fn my_function<T>(v: i32) {}This results in something a bit unexpected. The effective bound becomes |
| self.specs_with_constraints | ||
| .values_mut() | ||
| .for_each(|s| s.posts.push(post.to_def_id())); |
There was a problem hiding this comment.
Not sure about this change. I think what we will want to do is to check behavioral subtyping by emitting appropriate checks, and thus I think we should keep the refined and unrefined postconditions separate as we do with preconditions. Having them together makes it tricky to find the correct specification item to generate a viper condition.
| ty::TermKind::Ty(ty) => { | ||
| let projection = trait_.assoc_types[&proj_pred.def_id()]( | ||
| args.get_ty(), | ||
| args.get_const(), | ||
| ); | ||
| let decomp = RustTyDecomposition::from_ty(ty, did); | ||
| let gparams = deps.require_dep::<GenericParamsEnc>(did.into()).unwrap(); | ||
| let ty_expr = gparams.ty_expr(deps, decomp); | ||
| checks.push(vcx.mk_eq_expr(projection, ty_expr)); | ||
| } | ||
| ty::TermKind::Const(_) => { | ||
| todo!("Implement const projections in `refine_spec` bounds") | ||
| } |
There was a problem hiding this comment.
There is quite a bit overlap in what I do here and what I do in #137 in trait_impls.rs. We could try to reuse the functionality from there, or extract it for reusability.
| /// | ||
| /// If this postcondition has a constraint it will be attached to the corresponding | ||
| /// constrained spec **and** the base spec, otherwise just to the base spec. | ||
| /// constrained spec, otherwise just to the base spec. |
There was a problem hiding this comment.
Let's be careful (this isn't just about the comment): the constraints here aren't just the ones from the refine spec clause. Consider:
#[requires(base_pre)]
#[refine_spec(where T: Bar, [requires(refined_pre)])]
fn foo<T: Foo>(x: T);(The T: Foo above is equivalent to the clause where T: Foo.)
The spec items we want to generate are:
fn spec_item_pre_1<T>(x: T) -> bool
where
T: Foo, // constraint from method
{ base_pre }
fn spec_item_pre_2<T>(x: T) -> bool
where
T: Foo, // constraint from method
T: Bar, // constraint from refine_spec
{ refined_pre }There was a problem hiding this comment.
Unless I misunderstand your comment, the code you are pointing at is not really responsible for what the doc comment seems to imply. Here, it's not about attaching the type constraints, but rather the constraint itself. The generated spec items are still as expected.
#[prusti::spec_only]
#[prusti::spec_id = "18c13dce80414c678bf977582ea4520d"]
fn prusti_pre_item_foo_18c13dce80414c678bf977582ea4520d<T: Foo>(x: T)
-> bool {
let val: bool = (true) || (false);
val
}
#[prusti::spec_only]
#[prusti::spec_id = "e96feb1f9dee4b93a1eb03f11cb5a94c"]
#[prusti::type_cond_spec_trait_bounds_in_where_clause =
"ff0b081e-b912-4643-8298-bd50c8d06910"]
fn prusti_pre_item_foo_e96feb1f9dee4b93a1eb03f11cb5a94c<T: Foo>(x: T) -> bool
where T: Bar {
let val: bool = true;
val
}This was never correct as it was processing order dependent.
| self.set_monomorphised(def_id, substs, caller_def_id, body) | ||
| } | ||
|
|
||
| pub fn get_caller_bounds(&self, def_id: DefId) -> Vec<ty::Clause<'tcx>> { |
There was a problem hiding this comment.
This is not the right place for this. I would incorporate this as part of GParams (they already have the param_env): you could even have an encoder that takes a GParams as input and returns the bounds as viper expressions as output.
There was a problem hiding this comment.
I would just introduce it in this PR: it's strange to have it in a separate one where it's completely unused/untested
| .entry(constraint) | ||
| .or_insert_with(|| self.base_spec.clone()) | ||
| .or_insert_with(|| { | ||
| let mut spec = self.base_spec.clone(); |
There was a problem hiding this comment.
I remember discussing that we wouldn't clone the whole base_spec here. Could you make that change?
While the
refine_specprocedural macro created the appropriate specification function, it was currently ignored in the verification process. This change wires up the specification function to generate appropriate Viper code.Example:
will roughly translate to
Support for:
where i32: Copy)requiresandensuresclauseswhere T::Item: Bar)where T: Foo<Item = i32>)Unsupported:
refine_specs to one per function, as the expected behavior with multiple refine specs on a single function is rather unclear.constprojections inrefine_specs #147Results:
With this PR, we are now able to verify programs like these. Assuming that
Sized,MetaSizedandTupletraits are correctly encoded, and one manually fixes issues caused by #152.