The difference is that ownership rules across function boundaries are designed such that you never have to pass the drop flag between functions because whether ownership is passed or not is unambiguous from the function signature. Drop flags are only needed inside a function as a local variable. Whereas the proposed invariant checkers would have to be passed between functions (and between crates) alongside mutable reference parameters.
I suppose the type id could carry the information? I'm not exactly knowledgeable about the deep parts of the compiler.
On second thought, it wouldn't be required on all functions. Just on the trait impls that take a mutable reference. I'm not entirely sure how we would deal with public fields, but then if you want invariants you would not normally use public fields.
Also, perhaps an invariant trait or block would work better, as @idanarye suggested.
&mut u8 couldn't, but &mut TypeWithInvariant could. The compiler knows when it's dereferenced, and can insert extra calls around that. This might be implementable as MIR post-processing step?
After thinking for a bit, I thought of this:
- We always add checks around the type constructor and methods that take a mutable reference.
- For
structs withpubfields, we add guards around any modification of the struct using MIR post-processing like @kornel said.
Adding on to the whole topic:
We could allow invariants using where clauses (but not compile-time enforced) in functions and trait methods for both inputs and outputs.
For outputs, we could add a weak keyword output that is only a keyword in function parameters (i.e. it is possible to define variables and functions with the name output)
Could the invariant-carrying types implement any traits (e.g. Debug) without imposing extraordinarly large costs of all and any trait object usages which would have to call the invariant checker through vtable, though?
use std::fmt::Debug;
// this could be coming from another trait
fn work_upon(r: &mut dyn Debug) {
println!("r = {r:?}"); // doesn't this access require invariant checking?
// if so, it must be carried in metadata
}
#[derive(Debug)]
#[invariant(...)]
struct Indicator(i32);
fn main() {
let mut a: Box<dyn Debug> = Box::new(4);
work_upon(&mut a);
let mut b: Box<dyn Debug> = Box::new(Indicator(6));
work_upon(&mut b);
}
First of all, since Debug does not take mutable references or UnsafeCell, I don't think this example will require invariants checking.
Secondly, since the invariants will be runtime-checked, we will be able to run them without significant overhead IMO.