Disclaimer: this post should be seen as the starter for a discussion and not as a direct proposition to change the Rust language.
The main idea is to move safety invariant from "SAFETY" comments, into machine-enforceable checks. I think that all the change I’m proposing are backward-compatible, but I do not expect them to be completely trivial to implement.
TL; DR:
-
Add
const_assert!
that has some static analysis capacity to check boolean expression at compile time. Not validating the condition would stop the compilation with an error. -
Add
unsafe assert_unchecked!
macro that gives hint to the compiler (the programmer is responsible to validate that the condition is effectively true). It’s especially useful to help the compiler validating aconst_assert
when its static analysis isn’t good enough. -
Add a way to express function pre-condition. The desugaring uses
const_assert!
at call site, andassert_unchecked
in the body of function with pre-condition. -
Optionally, add a way to express predicates that can’t be express using Rust type system (like a non-dangling pointer), and make them usable by
assert_unchecked!
andconst_assert!
All those combined (and only the 3 first points are really needed) should drastically reduce the use of the unsafe
keyword.
1. Adding const_assert!
The first change is to introduce a const_assert!
macro (similar to static_assert
in C++), that is evaluated at compile time. I never used this crate, but I assume that it would be similar to static_assertions. Furthermore, const_assert!
should be able to check such kind of code:
let condition: bool = get_bool();
// this line would not compile since `condition` is not guaranteed to be true
// const_assert!(condition);
if !condition {
return
}
// here condition is guaranteed to be `true` because `false` was diverging
const_assert!(condition); // must compile
Which means that this should transitively be valid:
let condition: bool = get_bool();
assert!(condition);
// here `condition` is guaranteed to be `true` otherwise `assert!` would have diverged
const_assert!(condition);
2. Add unsafe assert_unchecked!
The second change is to add an assert_unchecked!
macro which is unsafe
to call. I also never used that crate, but I assume it’s what assert_unchecked crate) provides. The programmer is responsible to validate that the condition is true (hence why it’s an unsafe macro). assert_unchecked!
doesn’t generate any code, but the compile can rely on its condition being true, especially is const_assert!
.
3. Add contracts to functions
The third change is to make it possible to express function pre-conditions:
fn foo(non_zero: i32)
where non_zero != 0
{ ... }
fn do_stuff_with_non_empty_vec(non_empty: &Vec<T>)
where !non_empty.empty()
{ ... }
Adding such where clause has two consequences:
- At call site, calling such functions is equivalent to do a
const_assert!
of the pre-conditions before doing the actual call:
let some_vec = …;
do_stuff_with_non_empty_vec(&some_vec);
would be desugared into:
let some_vec = …;
{ // using a block, this way the desugaring also works in expressions
const_assert!( !non_empty.empty() );
do_stuff_with_non_empty_vec(&some_vec);
}
Here this would obviously not compile because the const_assert!
condition can’t be validated at compile time. Therefore the user would have to add a diverging case, for example:
let some_vec = …;
if (!some_vec.empty()) {
// this time, the call is valid and will compile successfully
do_stuff_with_non_empty_vec(&some_vec);
}
Note: this is why we need a relatively powerful static analysis for const_assert
- Furthermore, in the callee, since the condition is guaranteed to be checked by the caller, the optimizer can use that information, as-if the first line of the body was
unsafe { assert_unchecked!(pre_condition) };
So
fn do_stuff_with_non_empty_vec(non_empty: &Vec<T>)
where !non_empty.empty()
{ ... }
is desugared into:
fn do_stuff_with_non_empty_vec(non_empty: &Vec<T>)
{
assert_unchecked!(!non_empty.empty());
…
}
Note: even if this is not strictly necessary, this should help calling function with the same pre-conditions (or a subset of them).
Here we should already have a relatively useful feature. It would already be possible to expose invariant of some unsafe functions, and make them safe (since all of there invariant are guaranteed to be checked by the caller).
For example Vec::get_unchecked()
and Vec::get()
ultimately call SliceIndex::get_unchecked()
](index.rs - source) and SliceIndex::get()
. But let assume that the internal machinery was a bit simpler:
// before:
impl Vec {
// SAFETY: `i` must be < self.size()
unsafe fn get_unchecked(&self, i: Index) -> &Self::Output;
fn get(&self, i: Index) -> Option<&Self::Output> {
if i < self.size() {
// SAFETY: all invariant of `get_unchecked` are validated: i < self.size()
Some { unsafe { self.get_unchecked(i) } }
} else {
None
}
}
}
With our change, it would become possible to change those to be:
// after
impl Vec {
// no longer unsafe
fn get_unchecked(&self, i: Index) -> &Self::Output
where i < self.size();
fn get(&self, i: Index) -> Option<&Self::Output> {
if i < self.size() {
// the function isn’t unsafe anymore, so no unsafe block needed
Some {
self.get_unchecked(i)
// the above call is desugared as:
// { const_assert!(i < self.size); self.get_unchecked(i) }
// so the invariant are correctly checked
}
} else {
None
}
}
}
4. Adding predicates
The last change would be to add predicates. They are pure function, always unsafe, and always return true, be can only be called in const context. They don’t have a body. Since they are pure function an unsafe { assert_unchecked!(some_predicate(some, arg, s)) }
followed by a const_assert!(some_predicate(some, arg, s))
is guaranteed to succeed (as long as the arguments where not used by a &mut
call, otherwise it’s a compile error).
predicate fn doesNotDangle(p: *T);
predicate fn noReferencesAreActive(p: *T);
Those predicate would only be useful when expressing contract, and in assert_unchecked!
:
// no need to make it unsafe, the predicate already express the pre-conditions
fn get_mut(p: *T) -> &mut T
where doesNotDangle(p) && noReferencesAreActive(p)
{
unsafe {*p} // note: the * operator could be made safe if it had the same contact
}
And when calling it:
let p = …;
// this would fail to compile since it would call predicate at runtime
// assert!(doesNotDangle(p) && noReferenceAreActive(p))
// that’s the only way to validate a predicate
unsafe { assert_unchecked!(doesNotDangle(p) && noReferenceAreActive(p)) }
// We can now is this function safely without unsafe block
let ref: &mut T = get_mut(p);
If I’m not mistaking, calling predicate functions in assert_unchecked!
would be the last place where we would use unsafe
blocks (except maybe extern C
functions).
I did gloss over many points, but you should get the general idea. It would also be possible to make it more practical by being able to express post-condition in order to be able to chain the output of a function into another that has preconditions more easily. I also did not discuss Fn
traits and fn
pointers, and how contract should be represented in the type system, etc… I also did not discussed about which limitation we intentionally want to add to the static analysis capacity of const_assert!
. I don’t see anything fundamentally blocking in those points, so I wanted to open the discussion about the merit of such idea without immediately going into the details. It’s because I may have totally missed something that would render the whole thing infeasible, and I don’t want anyone to lose time on details if the main idea doesn’t make sense.