"If this field is zero then this other field must also be zero" annotations e.g. to create niches

Make NonZero usable as Const param is about something else but it got me thinking, what if we had a way to tell the compiler that if one field of a struct is zero then another field must also be zero? Or more generally, invariant relationships among fields?

#[invariant(val.is_null() == (len == 0))]
struct RawFatPointer<T> {
    val: *const T,
    len: usize,
}

(#[...] notation used just to illustrate, the syntax isn't the important part)

Now RawFatPointer has 2^sizeof(*const T) niches, and I'm sure there's plenty of other interesting things the compiler could do with this information.

EDIT: Come to think of it, this is exactly the same concept as a multi-column CHECK constraint in SQL, isn't it? Only the SQL design goal is trapping data integrity errors as early as possible, rather than creating optimization opportunities (though I'm sure SQL query planners do use CHECK constraints for optimization)

2 Likes

I think the most important question here is: how is this invariant enforced? Does writing an unacceptable value to the field panic? Is it unsafe to write to the field?

5 Likes

It's not exactly a generic solution, but you can get those niches for RawFatPointer using the following type:

enum RawFatPointer<T> {
    Null,
    NonNull {
        val: core::ptr::NonNull<T>,
        len: core::num::NonZero<usize>,
    }
}

Edit: apparently the compiler cannot optimize this properly to use all niches, but in theory it should give it that information.

7 Likes

I was thinking "it's unsafe to write to the field, except in cases where the compiler can prove that the write cannot possibly break the invariant", kinda like how NonNull works today. The challenge is of course pinning down how powerful we document the compiler's proofs to be.

I feel like "the compiler automatically inserts a dbg_assert! before each assignment" would also be useful, but I'd want that to be an opt-in mode. You need to know when you're electing to have assignments possibly panic.

2 Likes

This is much like the unsafe field proposal, except that that proposal does not mention how the compiler could be aware of the invariant.

Like the example given in the design meeting notes:

struct CacheArcCount<T> {
    // SAFETY: This `Arc`'s `ref_count` must equal the
    // value of the `ref_count` field.
    unsafe arc: Arc<T>,
    // SAFETY: See [`CacheArcCount::arc`].
    unsafe ref_count: usize,
}
3 Likes

I thought some more about how to represent checked updates and came up with this:

trait HasCheckedInvariants {
    fn try_update(&mut self, FnOnce(&mut Self) -> ())
        -> Result<(), InvariantBrokenError>;
}

The try_update callback gets a mutable reference to a copy of the object, and can do whatever it wants to its fields. On exit, the invariants are all checked. If all are good, the changes are copied back to the original object and try_update returns Ok, otherwise the changes are discarded and it returns Err.

This could probably be prototyped in a library with some derive macros.

(Unsolved problem: what do you do if the object is too big to copy twice for each update, or has uncopyable fields?)

There’s also an ongoing design effort to add contract programming to Rust, including invariant annotations, but I don’t know what its status is.

Aside: C++26 Contracts

In an unprecedented turn of events, the C++26 DIS (Draft International Standard, the final draft that was to become the C++26 IS modulo editorial fixes) was just rejected by the ISO National Bodies in a vote that was supposed to be largely a formality. The reason, according to the opponents, is the Contracts feature which hasn’t exactly lacked in the controversy department over its many years of design effort. Luckily Rust is in many ways a better fit for contracts, design-wise.