The Rust already supports safe uninit
types. It supports not with MaybeUninit<T>
, but with type-check: type-checker grants no use before initializing!
let a : i32; // a : uninit i32;
a = 2; // a : init i32;
I propose to extend this - extend type-checking to grant no read (instead of "no use") before initializing.
Next is described here - is a realization of this extended rule.
Safe Uninit Types is a "easy to implement" extension, which is extendable and flexible in use.
(J) Rust allow to write safe uninitialized variables for each types. We add uninit
before type-name to indicate that this type is uninitialized:
UninitializedType: uninit TypeNoBounds
TypeNoBounds: ... | ReferenceType | UninitializedType | ...
Most important: Uninitialized variable after initialization ("dropping" Uninitialization), is no longer uninit
!
let a : i32;
// a : uninit i32;
a = 7;
// a : i32; uninit is "dropped"/initialized
Now we add more abilities to Uninitialized Types!
(J1) Uninitialized variable is movable: uninitialization is "moved" by move from sender to receiver.
let a : i32;
// a : uninit i32;
let b = a;
// b : uninit i32;
// a : i32; // not longer uninit, but moved
let c : &i32;
// c : uninit &i32; // uninitialized reference
let d = c;
// d : uninit &i32;
// c : &i32; // not longer uninit, but moved
(J2) Referential Uninitialization is a bit complicated.
(1) Uninitialization is "moved" by move from sender to inside receiver (reference)
(2) Inner-Uninitialized Reference is always "exclusive", regardless if it mutable or not (till drop).
(3) Inner-Uninitialized dereferenceble Variable is always at least once write-only, regardless if it mutable or not
(4) Inner-Uninitialized Reference is forbidden to move (after initialization, reference is not longer inner-Uninitialized).
(5) Inner-Uninitialized Reference is forbidden to drop (after initialization, reference is not longer inner-Uninitialized).
let a : i32;
// a : uninit i32;
let b = &a;
// b : & uninit i32; // initialized reference to uninitialized variable
// a : i32; // not longer uninit, but exclusive borrowed!
let c = &b;
// b : && uninit i32;
// b : & i32; // not longer uninit, but exclusive borrowed!
**c = 7;
// c : && i32; // uninit is "dropped"/initialized
// now reference 'c' could be dropped
drop(c);
drop(b);
// a == 7
Note: a : uninit & i32
is an uninitialized variable with referential type, but b : & uninit i32
is initialized reference to uninitialized variable
(I+) Partial Uninitialized Types - we also add partiality next to uninit
- Partial Uninitialization
for Product Types (Structs and Tuples first of all)
UninitializedType: PartialUninit TypeNoBounds
PartialUninit: uninit Partiality?
And we could write:
struct S4 {a : i32, b : i32, c : i32, d : i32}
let s_bd : uninit.{a, c} S4 = S4 {b: 7, d: 9, ..uninit};
If we also use extension (E), then we could also write uninitialization for tuples and more flexible for Structs
let s_bd : uninit.{c} S4 = S4 {a: 3, b: 7, uninit c, d: 9};
let t_1 : uninit.{1} (i32, u16, f64, f32) = (8, uninit, 4, 9.0);
let t_3 : (i32, uninit u16, uninit f64, f32) = (5, uninit, uninit, 9.0f32);
Now we could create Self-Referential Types:
struct SR <T>{
val : T,
lnk : & T, // reference to val
}
let x = SR {val : 5, uninit lnk };
// x : uninit.{lnk} SR<i32>
x.lnk = & x.val;
// x : SR<i32>;
(J3) Uninitialized Parameters are similar to Referential Uninitialization
(1) Uninitialization must be written explicitly at Type
(2) If Uninitialized Parameter is a not-reference, then it behaves same as Uninitialized variable.
(3) If Uninitialized Parameter is an inner-uninitialized reference, then it behaves same as inner-uninitialized reference.
(4) If Uninitialized Parameter is an inner-uninitialized reference, then uninitialization must be moved or initialization must happens before return or together with return.
struct S4 {a : i32, b : i32, c : i32, d : i32}
impl S4 {
fn init_a(self : & unit.{a} Self) {
*self.a = 5;
}
}
If we also use extension (B), we could write more specific sub-type:
impl S4 {
fn init_a(self : & unit.{a} Self.{a}) {
*self.a = 5;
}
}
(J4) Uninitialized arguments, again easy: uninitialization of argument and parameter must match! It is error if not.
struct S4 {a : i32, b : i32, c : i32, d : i32}
let s : uninit.{a} S4 = S4 { uninit a, b : 7, c : 33, d : 4};
s.init_a();
P.S.
Note: Safe Uninit Types(J) is not a part of "Partial Types" proposal, but I've described how we could extend Uninit Types to Partial Uninit Types with (I) and even further to Partial of Partial Uninit Types (with (I + B) or (J + B) or (I + J + B)).
More details about partial types are proposed here Partial Types (v3) and it is discussed here (Pre? RFC) Partial types and partial mutability.