|
| 1 | +- Feature Name: pi-types |
| 2 | +- Start Date: 2016-06-22 |
| 3 | +- RFC PR: (leave this empty) |
| 4 | +- Rust Issue: (leave this empty) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +I propose a simple, yet sufficiently expressive, addition of dependent-types |
| 10 | +(also known as, Π-types and value-types). |
| 11 | + |
| 12 | +Type checking remains decidable. |
| 13 | + |
| 14 | +# Motivation |
| 15 | +[motivation]: #motivation |
| 16 | + |
| 17 | +An often requested feature is the "type-level numerals", which enables generic |
| 18 | +length arrays. The current proposals are often limited to integers or even lack |
| 19 | +of value maps, and other critical features. |
| 20 | + |
| 21 | +There is a whole lot of other usecases as well. These allows certain often |
| 22 | +requested features to live in standalone libraries (e.g., bounded-integers, |
| 23 | +type level arithmetics, lattice types). |
| 24 | + |
| 25 | +# Detailed design |
| 26 | +[design]: #detailed-design |
| 27 | + |
| 28 | +## The new value-type construct, `const` |
| 29 | + |
| 30 | +The first construct, I will introduce is `ε → τ` constructor, `const`. All this |
| 31 | +does is taking a const-expr (struct construction, arithmetic expression, and so |
| 32 | +on) and constructs a _type-level version_ of this. |
| 33 | + |
| 34 | +In particular, we extend the type grammar with an additional `const C`, a type |
| 35 | +whose semantics can be described as follows, |
| 36 | + |
| 37 | + ValueTypeRepresentation: |
| 38 | + Π ⊢ x: const c |
| 39 | + -------------- |
| 40 | + Π ⊢ x = c |
| 41 | + |
| 42 | +In other words, if `x` has type `const c`, its value _is_ `c`. That is, any |
| 43 | +constexpr, `c`, will either be of its underlying type or of the type, `const |
| 44 | +c`. |
| 45 | + |
| 46 | +It is important to understand that values of `const c` are constexprs, and |
| 47 | +follows their rules. |
| 48 | + |
| 49 | +## `const fn`s as Π-constructors |
| 50 | + |
| 51 | +We are interested in value dependency, but at the same time, we want to avoid |
| 52 | +complications such as SMT-solvers and so on. |
| 53 | + |
| 54 | +Thus, we follow a purely constructible model, by using `const fn`s. |
| 55 | + |
| 56 | +Let `f` be a `const fn` function. From the rules of `const fn`s and constexprs, |
| 57 | +we can derive the rule, |
| 58 | + |
| 59 | + PiConstructorInference: |
| 60 | + Π ⊢ x: const c |
| 61 | + Π ⊢ f(c): τ |
| 62 | + -------------- |
| 63 | + Π ⊢ f(x): const τ |
| 64 | + |
| 65 | +This allows one to take some const parameter and map it by some arbitrary, pure |
| 66 | +function. |
| 67 | + |
| 68 | +## Type inference |
| 69 | + |
| 70 | +Since we are able to evaluate the function on compile time, we can easily infer |
| 71 | +const types, by adding an unification relation, from the rule above. |
| 72 | + |
| 73 | +The relational edge between two const types is simple a const fn, which is |
| 74 | +resolved under unification. |
| 75 | + |
| 76 | +## `where` clauses |
| 77 | + |
| 78 | +Often, it is wanted to have some statically checked clause satisfied by the |
| 79 | +constant parameters. To archive this, in a reasonable manner, we use const |
| 80 | +exprs, returning a boolean. |
| 81 | + |
| 82 | +We allow such constexprs in `where` clauses of functions. Whenever the |
| 83 | +function is invoked given constant parameters `<a, b...>`, the compiler |
| 84 | +evaluates this expression, and if it returns `false`, an aborting error is |
| 85 | +invoked. |
| 86 | + |
| 87 | +## The type grammar |
| 88 | + |
| 89 | +These extensions expand the type grammar to: |
| 90 | + |
| 91 | + T = scalar (i32, u32, ...) // Scalars |
| 92 | + | X // Type variable |
| 93 | + | Id<P0..Pn> // Nominal type (struct, enum) |
| 94 | + | &r T // Reference (mut doesn't matter here) |
| 95 | + | O0..On+r // Object type |
| 96 | + | [T] // Slice type |
| 97 | + | for<r..> fn(T1..Tn) -> T0 // Function pointer |
| 98 | + | <P0 as Trait<P1..Pn>>::Id // Projection |
| 99 | + | C // const types |
| 100 | + F = c // const fn name |
| 101 | + C = E // Pi constructed const type |
| 102 | + P = r // Region name |
| 103 | + | T // Type |
| 104 | + O = for<r..> TraitId<P1..Pn> // Object type fragment |
| 105 | + r = 'x // Region name |
| 106 | + E = F(E) // Constant function application. |
| 107 | + | p // const type parameter |
| 108 | + | [...] // etc. |
| 109 | + |
| 110 | +Note that the `const` prefix is only used when declaring the parameter. |
| 111 | + |
| 112 | +## An example |
| 113 | + |
| 114 | +This is the proposed syntax: |
| 115 | + |
| 116 | +```rust |
| 117 | +use std::{mem, ptr}; |
| 118 | + |
| 119 | +// We start by declaring a struct which is value dependent. |
| 120 | +struct Array<n: const usize, T> { |
| 121 | + // `n` is a constexpr, sharing similar behavior with `const`s, thus this |
| 122 | + // is possible. |
| 123 | + content: [T; n], |
| 124 | +} |
| 125 | + |
| 126 | +// We are interested in exploring the `where` clauses and Π-constructors: |
| 127 | +impl<n: const usize, T> Array<n, T> { |
| 128 | + // This is simple statically checked indexing. |
| 129 | + fn const_index<i: const usize>(&self) -> &T where i < n { |
| 130 | + // note that this is constexpr ^^^^^ |
| 131 | + unsafe { self.content.unchecked_index(i) } |
| 132 | + } |
| 133 | + |
| 134 | + // "Push" a new element, incrementing its length **statically**. |
| 135 | + fn push(self, elem: T) -> Array<n + 1, T> { |
| 136 | + let mut new: [T; n + 1] = mem::uninitialized(); |
| 137 | + // ^^^^^ constexpr |
| 138 | + unsafe { |
| 139 | + ptr::copy(self.content.as_ptr(), new.as_mut_ptr(), n); |
| 140 | + ptr::write(new.as_mut_ptr().offset(n), elem); |
| 141 | + } |
| 142 | + |
| 143 | + // Don't call destructors. |
| 144 | + mem::forget(self.content); |
| 145 | + |
| 146 | + // So, the compiler knows the type of `new`. Thus, it can easily check |
| 147 | + // if the return type is matching. By siply evaluation `n + 1`, then |
| 148 | + // comparing against the given return type. |
| 149 | + Array { content: new } |
| 150 | + } |
| 151 | +} |
| 152 | + |
| 153 | +fn main() { |
| 154 | + let array: Array<2, u32> = Array { content: [1, 2] }; |
| 155 | + |
| 156 | + assert_eq!(array.const_index::<0>(), 1); |
| 157 | + assert_eq!(array.const_index::<1>(), 2); |
| 158 | + assert_eq!(array.push(3).const_index::<2>(), 3); |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +# Drawbacks |
| 163 | +[drawbacks]: #drawbacks |
| 164 | + |
| 165 | +If we want to have type-level Turing completeness, the halting problem is |
| 166 | +inevitable. One could "fix" this by adding timeouts, like the current recursion |
| 167 | +bounds. |
| 168 | + |
| 169 | +Another draw back is the lack of implication proves. |
| 170 | + |
| 171 | +# Alternatives |
| 172 | +[alternatives]: #alternatives |
| 173 | + |
| 174 | +Use full SMT-based dependent types. These are more expressive, but severely |
| 175 | +more complex as well. |
| 176 | + |
| 177 | +# Unresolved questions |
| 178 | +[unresolved]: #unresolved-questions |
| 179 | + |
| 180 | +What syntax is preferred? How does this play together with HKP? Can we improve |
| 181 | +the converse type inference? What should be the naming conventions? Should we |
| 182 | +segregate the value parameters and type parameters by `;`? |
0 commit comments