Description
Not sure if here is the right place, but there are some ideas for filling const generic params with runtime values in ZSTs, that enables dependent types for "proof as program" style things.
Ghost types
A type is ghost if it has no runtime data and effect. That is, a ZST annotated with a ghost keyword:
ghost struct Le<const A: usize, const B: usize>(());
// we control instantiating of this type in a way that it has instance iff A <= B
Why someone would annotate type with ghost? Because it will enable for its const generic parameters to depend on runtime values, so this will be accepted:
let x = 2;
let prf: Le<x, 5> = Le::<x, 5>(());
The x
in Le<x, 5>
will be considered as a const variable, and only unifies with itself. only immutable variables and function parameters are allowed (that types doesn't contain mutable reference or interior mutablity) to be in const generics of ghost types.
This allows to write function with dependent signature:
fn do_smth(x: usize, y: usize, x_le_y: Le<x, y>) {
// we know x is lesser than y, so we can for example index an array without bound checking.
}
Which is the whole propose of ghost types. Benefits of powerful type system is clear, it can enable more correct and specified code, less unsafe code, less runtime checks, ... This kind of thing is actually the tool for formal verification in languages like Coq. I don't explain it more as you are probably already aware of.
Type checking
How we type check such function calls? It will reuse const generic infrastructures. For example that function will become roughly equivalent to this for type checking propose:
fn do_smth<const x: usize, const y: usize>(x_le_y: Le<x, y>);
And in call site, we consider each immutable binding as a const variable and each parameter as an abstract const expression, so in:
do_smth(f(a), g(2), prf);
f
andg
should beconst fn
- if we assume
g(2)
is7
, type ofprf
should unify withLe<f(a), 7>
.
so this is actually different with:
let b = f(a);
do_smth(b, g(2), prf);
as here f
can be anything and type of prf
is Le<b, 7>
in this case.
Code generation
After type checking, we replace all ghost types with ()
, so that function will become:
fn do_smth(x: usize, y: usize, x_le_y: ());
which is normal function today.
I have some extenstions like ghost fn
in mind, but I think this is enough for starting discussion.