Total spec closures and partial spec closures #838
Replies: 2 comments 3 replies
-
This is great. A syntax proposal that feels a bit Rustier to me, and based on @tjhance's
|
Beta Was this translation helpful? Give feedback.
-
I'm delighted to hear we're moving in this direction. My recent debugging work has been frustrating in large part because of not having effective eager checks on spec fns, which has been making me contemplate on how end users will experience total spec functions: they'll find it really confusing. So I'm extremely happy to hear we're moving in a direction that supports a more consistent eager error reporting experience for partial fn constraints. Three cheers! I also like ability to preserve the lightweight |
Beta Was this translation helpful? Give feedback.
-
There's been recent discussion on providing better support for checking preconditions on spec functions (see #764 ), with a primary goal of catching errors in specs earlier, when the spec is defined, rather than later, in the middle of using the spec to verify something more complicated. I'd like to propose extending this to higher-order specifications as well by allowing preconditions on spec closures, and having the preconditions be part of the spec closure in the style of Dafny's
-->
functions.Verus supports first-class spec functions via Rust closures. For example, inside a specification,
|n: int| n + 1
is a spec closure that defines a function that adds one to an integer argumentn
. Currently, it is possible to put preconditions (viarecommends
) on top-level functions (as inspec fn f(n: int) -> int recommends n != 0 { n + 1 }
), but not on closures (there is nothing like|n: int| recommends n != 0 { n + 1 }
yet). This proposal sketches what support for preconditions on spec closures might look like in Verus. More specifically, it proposes a new "partial" spec function type to complement the existing "total" spec function type, and proposes using Rust traits to make common library functions, such asMap::new
,Seq::new
, andSeq::map
polymorphic over both total and partial spec function types.Syntax (provisional)
I don't have a complete syntax in mind, but for the purposes of this proposal, I'll use the following syntax, sticking close to the current Verus syntax:
recommends
, since that's what's currently in use (but it could be calledwhen
,expects
,wants
,desires
, ...)|x1: t1, ..., xn: tn| expr
for total closure expressions, as in current Verus syntax|x1: t1, ..., xn: tn| recommends expr { expr }
for partial closure expressionsFnSpec(t1, ... tn) -> t0
for total function types, as in current Verus syntaxFnSpec(t1, ... tn) --> t0
for partial function types, following Dafny's convention on->
and-->
FnSpecTrait(t1, ... tn) -> t0
for the general spec function trait, implemented by both total and partial function typesPersonally, I'm not attached to
->
and-->
and I don't like the nameFnSpecTrait
(FnSpec
would make more sense, assuming we use something other thanFnSpec
for the function types, as has been proposed elsewhere).Motivation
Verus currently checks
recommends
clauses on top-level spec functions. In the following example,r
's call toq(n)
failsq
's recommends check:One way to fix this is add a
recommends
tor
(alternatively, as mentioned in #764 , one could suppress the check with some sort of local#[allow(...)]
):Suppose, though, that
r
's definition involves a closure, and the closure callsq
:In this case, it's not clear how the call to
q(x)
would satisfyq
's recommends clause, since there is nox != 0
constraint onx
. To motivate adding partial closure support, let's look at a few simpler alternative designs first.We could simply not perform the recommends check inside a closure. However, this would miss opportunities to catch spec errors early, and could surprise and confuse programmers who expect more consistent checking.
We could allow programmers to add recommends clauses on closures, and then simply assume that the recommends clauses hold at the call sites to the closures without any checks. Then you could write:
but this would fail to catch the case where
r
might actually pass in a value ofx = 0
:q(x)
withif x != 0 { q(x) } else { arbitrary() }
, so that the return value is arbitrary if the arguments don't satisfy the recommends. However, this still doesn't catch the recommends failure early, since there's still no recommends check on the call to the closure:Instead, the programmer would have to discover the problem much later when they try to call
r(0)
and discover that it fails to obey the expected substitution rule for a function call (i.e. the callf(0)
isn't equal to the body with the argument0
substituted for the parameterx
):If we want to catch recommends failures for closures early, we need to perform recommends checking on calls to the closures.
Design
Coq, F*, and Dafny all have mechanisms for attaching preconditions to specification "closures". Coq and F* attach the preconditions to the type of the closure using dependent types, which would be a major and difficult extension to Rust's type system. Dafny attaches the precondition to the closure value. This approach is much easier to integrate into Verus, so this proposal is based on this approach. Specifically, for a value
v
of partial function typeFnSpec(t1, ... tn) --> t0
, the programmer could check to see whether argumentsa1
, ...,an
satisfyv
's recommends clauses with an expression likev.recommends(a1, ..., an)
. (Again, I don't have a strong preference on the exact syntax here.) When a program callsv
with argumentsa1
, ...,an
, Verus would automatically insert a recommends check to make surev.recommends(a1, ..., an)
is true.Total closures, on the other hand, would always be valid for all arguments, and would not receive a recommends check. The tradeoff is that total closures cannot make any assumptions about their arguments, so that a total closure's body is checked for recommends failures with just an assumption of
true
on the closure's arguments. If this leads to spurious recommends failures, then the programmer would have to suppress the failures with#[allow(...)]
or switch to a partial closure. Nevertheless, total closures are still very convenient in many cases. For example, when defining a set withSet::new
, it's very natural to use a total closure of typeFnSpec(t) -> bool
.It should be easy to provide explicit coercions between total and partial closures (adding a
recommends true
when going from total to partial, or explicitly deciding to ignore the recommends when going from partial to total). However, many library functions should work seamlessly with both total and partial function types without having to use an explicit coercion. For example,Map::new
andSeq::new
make sense with partial closures, but are equally usable with total closures. We should be able to writeSeq::new(10, |n: int| n + 1)
, using the total closure|n: int| n + 1
, without having to explicitly coerce the total closure to a partial closure. For this, we can use a traitFnSpecTrait
that is implemented by both total and partial closures:In the case of a total closure,
f.recommends(i)
would be trivially satisfied.Implementation
The implementation of total closures should be unaffected by partial closures; it would not change from the current implementation. A total closure
FnSpec(t1, ..., tn) -> t0
is simply implemented as an AIR lambda of type(t1, ..., tn) -> t0
.A partial closure
FnSpec(t1, ..., tn) --> t0
can be implemented as a pair of AIR lambdas(((t1, ..., tn) -> bool), ((t1, ..., tn) -> t0))
, where the(t1, ..., tn) -> bool
lambda holds the domain of the partial closure.The trait
FnSpecTrait(t1, ..., tn) -> t0
can dispatch to the underlying total or partial closure implementations using the existing Verus trait mechanisms based on dynamic type identifiers. (Total and partial closures would have distinct dynamic type identifiers in the AIR encoding.) There would be some extra SMT overhead in this case for things likeMap::new
andSeq::new
compared to the current total-closure-only versions of these library functions, but I would guess it would not be a major overhead.Beta Was this translation helpful? Give feedback.
All reactions