Skip to content

Commit 20813da

Browse files
authored
RFC for const generics (#42)
* RFC for const generics * implementation roadmap * more details about monomorphization, method call selection and type system * more adjustments * RFC metadata * PR suggestions * fix readme
1 parent 443ef9e commit 20813da

File tree

2 files changed

+306
-0
lines changed

2 files changed

+306
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@
1515
| [0011](rfcs/0011-references.md) | References |
1616
| [0012](rfcs/0012-expressive-diagnostics.md) | Expressive Diagnostics |
1717
| [0013](rfcs/0013-changes-lifecycle.md) | Changes Lifecycle |
18+
| [0014](rfcs/0014-abi-errors.md) | Abi Errors |
19+
| [0015](rfcs/0015-const-generics.md) | Const Generics |

rfcs/0015-const-generics.md

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
- Feature Name: const_generics
2+
- Start Date: 2024-10-27
3+
- RFC PR: [FuelLabs/sway-rfcs#42](https://github.com/FuelLabs/sway-rfcs/pull/42)
4+
- Sway Issue: [FueLabs/sway#0000](https://github.com/FuelLabs/sway/issues/001)
5+
6+
# Summary
7+
8+
[summary]: #summary
9+
10+
Allows constant values as generic arguments.
11+
12+
# Motivation
13+
14+
[motivation]: #motivation
15+
16+
Some types have constants, specifically unsigned integers as part of their definition (e.g. arrays and string arrays). Without `const generics` it is impossible to have a single `impl` item for all instances of these types.
17+
18+
# Guide-level explanation
19+
20+
[guide-level-explanation]: #guide-level-explanation
21+
22+
`const generics` refer to the language syntax where generic parameters are defined as constant values, instead of types.
23+
24+
A simple example would be:
25+
26+
```rust
27+
fn id<const SIZE: u64>(array: [u64; SIZE]) -> [u64; SIZE] {
28+
array
29+
}
30+
```
31+
32+
This also allows `impl` items such as
33+
34+
```rust
35+
impl<const N: u64> AbiEncode for str[N] {
36+
...
37+
}
38+
```
39+
40+
This constant can be inferred or explicitly specified. When inferred, the syntax is no different than just using the item:
41+
42+
```rust
43+
id([1u8])
44+
```
45+
46+
In the example above, the Sway compiler will infer `SIZE` to be one, because `id` parameter will be infered to be `[u8; 1]`.
47+
48+
For the cases where the compiler cannot infer this value, or this value comes from an expression, it is possible to do:
49+
50+
```rust
51+
id::<1>([1u8]);
52+
id::<{1 + 1}>([1u8, 2u8]);
53+
```
54+
55+
When the value is not a literal, but an expression, it is named "const generic expression" and it needs to be enclosed by curly braces. This will fail if the expression cannot be evaluated as `const`.
56+
57+
# Reference-level explanation
58+
59+
[reference-level-explanation]: #reference-level-explanation
60+
61+
This new syntax has three forms: declarations, instantiations, and references.
62+
63+
"Const generics declarations" can appear anywhere all other generic arguments declarations are valid:
64+
65+
1. Function/method declaration;
66+
1. Struct/Enum declaration;
67+
1. `impl` declarations.
68+
69+
```rust
70+
// 1
71+
fn id<const N: u64>(...) { ... }
72+
73+
// 2
74+
struct SpecialArray<const N: u64> {
75+
inner: [u8; N],
76+
}
77+
78+
//3
79+
impl<const N: u64> AbiEncode for str[N] {
80+
...
81+
}
82+
```
83+
84+
"Const generics instantiations" can appear anywhere all other generic argument instantiations are valid:
85+
86+
1. Function/method reference;
87+
1. Struct/Enum reference;
88+
1. Fully qualified call paths.
89+
90+
```rust
91+
// 1
92+
id::<1>([1u8]);
93+
special_array.do_something::<1>();
94+
95+
// 2
96+
SpecialArray::<1>::new();
97+
98+
// 3
99+
<SpecialArray::<1> as SpecialArrayTrait::<1>>::f();
100+
```
101+
102+
"Const generics references" can appear anywhere any other identifier appears. For semantic purposes, there is no difference between the reference of a "const generics" and a "normal" const.
103+
104+
```rust
105+
fn f<const I: u64>() {
106+
__log(I);
107+
}
108+
```
109+
110+
Different from type generics, const generics cannot appear at:
111+
112+
1. constraints;
113+
1. where types are expected.
114+
115+
```rust
116+
// 1 - INVALID
117+
fn f<const I: u64>() where I > 10 {
118+
...
119+
}
120+
121+
// 2 - INVALID
122+
fn f<const I: u64>() -> Vec<I> {
123+
Vec::<I>::new()
124+
}
125+
```
126+
127+
## Const Value Specialization
128+
129+
By the nature of `impl` declarations, it is possible to specialize `impl` items for some specific types. For example:
130+
131+
```rust
132+
impl SomeStruct<bool> {
133+
fn f() {
134+
}
135+
}
136+
```
137+
138+
In the example above, `f` is only available when the generic argument of `SomeStruct` is known to be `bool`. This kind of specialization will not be supported for "const generics", which means that the example below will not be supported:
139+
140+
```rust
141+
impl SomeIntegerStruct<1> {
142+
...
143+
}
144+
145+
impl SomeBoolStruct<false> {
146+
...
147+
}
148+
```
149+
150+
The main reason for forbidding this is that apart from `bool`, which only needs two values, all other constants would demand complex syntax to guarantee the completeness and uniqueness of all implementations, the same way that `match` expressions do.
151+
152+
## Monomorphization
153+
154+
As with other generic arguments, `const generics` monormorphize functions, which means that a new "TyFunctionDecl", for example, will be created for which value that is instantiated.
155+
156+
Prevention of code bloat will be the responsibility of the optimizer.
157+
158+
Monomorphization for const generics has one extra complexity. To support arbitrary expressions it is needed to "solve" an equation. For example, if a variable is typed as `[u64; 1]` and a method with its `Self` type as `[u64; N + 1]` is called, the monomorphization process needs to know that `N` needs to be valued as `0` and if the variable is `[u64; 2]`, `N` will be `1`.
159+
160+
## Type Engine changes
161+
162+
By the nature of `const generics`, it will be possible to write expressions inside types. Initially, only simple references will be
163+
supported, but at some point, more complex expressions will be needed, for example:
164+
165+
```sway
166+
fn len<T, const N: u64>(a: [T; N]) { ... }
167+
168+
fn bigger<const N: u64>(a: [u64; N]) -> [u64; N + 1] {
169+
[0; N + 1]
170+
}
171+
```
172+
173+
This poses the challenge of having an expression tree inside of the type system. Currently Sway already
174+
has three expression trees: `Expr`, `ExpressionKind` and `TyExpressionVariant`. This demands a new one,
175+
given that the first two allow much more complex expressions than what `const generics` wants to support;
176+
and the last one is only created after some `TypeInfo` already exists, and thus cannot be used.
177+
178+
At the same time, the parser should be able to parse any expression and return a friendly error that such expression
179+
is not supported.
180+
181+
So, in case of an unsupported expression the parser will parser the `const generic` expression as it does for normal `Expr`and will lower it to the type system expression enum, but in the place of the unsupported expression will return a `TypeInfo::ErrorRecovery`.
182+
183+
These expressions will also increase the complexity of all type related algorithms such as:
184+
185+
1. Unification
186+
2. PartialEq
187+
3. Hash
188+
189+
In the simplest case, it is very clear how to unify `TypeInfo::Array(..., Length::Literal(1))` and `TypeInfo::Array(..., Length::Expression("N"))`.
190+
But more complex cases such as `TypeInfo::Array(..., Length::Expression("N"))` and `TypeInfo::Array(..., Length::Expression("N + 1"))`, is not clear
191+
if these types are unifiable, equal or simply different.
192+
193+
## Method call search algorithm
194+
195+
When a method is called, the algorithm that searches which method is called uses the method `TraitMap::get_impls`.
196+
Currently, this method does an `O(1)` search to find all methods applicable to a type. For example:
197+
198+
```sway
199+
impl [u64; 1] {
200+
fn len_for_size_one(&self) { ... }
201+
}
202+
```
203+
204+
would create a map with something like
205+
206+
```
207+
Placeholder -> [...]
208+
...
209+
[u64; 1] -> [..., len_for_size_one,...]
210+
...
211+
```
212+
213+
The algorithms first create a `TypeRootFilter`, which is very similar to `TypeInfo`. And uses this `enum` to search the hash table.
214+
After that, it "generalizes" the filter and searches for `TypeRootFilter::Placeholder`.
215+
216+
To fully support `const generics` and `const value specialization`, the compiler will now keep generalizing the
217+
searched type until it hits `Placeholder`. For example, searching for `[u64; 1]` will actually search for:
218+
219+
1. [u64; 1];
220+
1. [u64; Placeholder];
221+
1. Placeholder;
222+
223+
The initial implementation will do this generalization only for `const generics`, but it also makes sense to
224+
generalize this with other types such as `Vec<u64>`.
225+
226+
1. Vec\<u64>;
227+
1. Vec\<Placeholder>;
228+
1. Placeholder;
229+
230+
This gets more complex as the number of `generics` and `const generics` increases. For example:
231+
232+
```sway
233+
struct VecWithSmallVecOptimization<T, const N: u64> { ... }
234+
```
235+
236+
Searching for this type would search:
237+
238+
1. VecWithSmallVecOptimization\<u64, 1>
239+
1. VecWithSmallVecOptimization\<Placeholder, 1>
240+
1. VecWithSmallVecOptimization\<u64, Placeholder>
241+
1. VecWithSmallVecOptimization\<Placeholder, Placeholder>
242+
1. Placeholder
243+
244+
More research is needed to understand if this change can potentially change the semantics of any program written in Sway.
245+
246+
# Implementation Roadmap
247+
248+
1. Creation of the feature flag `const_generics`;
249+
1. Implementation of "const generics references";
250+
```rust
251+
fn f<const I: u64>() { __log(I); }
252+
```
253+
3. The compiler will be able to encode arrays of any size; Which means being able to implement the following in the "core" lib and using arrays of any size as "configurables";
254+
```rust
255+
impl<T, const N: u64> AbiEncode for [T; N] { ... }
256+
```
257+
4. Being able to `abi_encode` arrays of any size;
258+
```rust
259+
fn f<T, const N: u64>(s: [T; N]) -> raw_slice {
260+
<[T; N] as AbiEncode>::abi_encode(...);
261+
}
262+
f::<1>([1])
263+
```
264+
5. Inference of the example above;
265+
```rust
266+
f([1]);
267+
core::encode([1]);
268+
```
269+
6. Struct/enum support for const generics;
270+
7. Function/method declaration;
271+
8. `impl` declarations;
272+
9. Function/method reference.
273+
274+
# Drawbacks
275+
276+
[drawbacks]: #drawbacks
277+
278+
None
279+
280+
# Rationale and alternatives
281+
282+
[rationale-and-alternatives]: #rationale-and-alternatives
283+
284+
# Prior art
285+
286+
[prior-art]: #prior-art
287+
288+
This RFC is partially based on Rust's own const generic system:
289+
- https://doc.rust-lang.org/reference/items/generics.html#const-generics
290+
- https://blog.rust-lang.org/inside-rust/2021/09/06/Splitting-const-generics.html
291+
- https://rust-lang.github.io/rfcs/2000-const-generics.html
292+
- https://doc.rust-lang.org/beta/unstable-book/language-features/generic-const-exprs.html
293+
294+
# Unresolved questions
295+
296+
[unresolved-questions]: #unresolved-questions
297+
298+
1. What is the impact of changing the "method call search algorithm"?
299+
300+
# Future possibilities
301+
302+
[future-possibilities]: #future-possibilities
303+
304+
As mentioned above, implementing constraints like where N > 0.

0 commit comments

Comments
 (0)