Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC for const generics #42

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
| [0011](rfcs/0011-references.md) | References |
| [0012](rfcs/0012-expressive-diagnostics.md) | Expressive Diagnostics |
| [0013](rfcs/0013-changes-lifecycle.md) | Changes Lifecycle |
| [0014](rfcs/0014-abi-errors.md) | Abi Errors |
| [0014](rfcs/0015-const-generics.md) | Const Generics |
299 changes: 299 additions & 0 deletions rfcs/0015-const-generics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
- Feature Name: const_generics
- Start Date: 2024-10-27
- RFC PR: [FuelLabs/sway-rfcs#42](https://github.com/FuelLabs/sway-rfcs/pull/42)
- Sway Issue: [FueLabs/sway#0000](https://github.com/FuelLabs/sway/issues/001)

# Summary

[summary]: #summary

Allows constant values as generic arguments.

# Motivation

[motivation]: #motivation

Some types have constants, specifically unsigned integers, as their definition (e.g. arrays and string arrays). Without `const generics` it is impossible to have `impl` items for all instances of these types.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Some types have constants, specifically unsigned integers, as their definition (e.g. arrays and string arrays). Without `const generics` it is impossible to have `impl` items for all instances of these types.
Some types have constants, specifically unsigned integers, as a 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.


# Guide-level explanation

[guide-level-explanation]: #guide-level-explanation

`const generics` refer to the language syntax where generic parameters are defined as constant values, instead of types.

A simple example would be:

```rust
fn id<const SIZE: usize>(array: [u64; SIZE]) -> [u64; SIZE] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In most of the code samples usize is used which is misleading because this type does not exist in Sway. Was the intention maybe to emphasize that any unsigned type can be used?

Even in that case, the proposal would be to go with u64 or other concrete types in examples and specify in the RFC which types can be used in const generics.

Or to mention on the top of the Guide-level explanation that usize means any of the types: u8, ...

Still, in this particular example even that is misleading because currently array size must be u64.

Suggested change
fn id<const SIZE: usize>(array: [u64; SIZE]) -> [u64; SIZE] {
fn id<const SIZE: u64>(array: [u64; SIZE]) -> [u64; SIZE] {

array
}
```

This also allows `impl` items such as

```rust
impl<const N: usize> AbiEncode for str[N] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
impl<const N: usize> AbiEncode for str[N] {
impl<const N: u64> AbiEncode for str[N] {

...
}
```

This constant can be inferred or explicitly specified. When inferred, the syntax is no different than just using the item:

```rust
id([1u8])
```

In the example above, the Sway compiler will infer `SIZE` to be one, because `id` parameter will be infered to be `[u8; 1]`.

For the cases where the compiler cannot infer this value, or this value comes from an expression, it is possible to do:

```rust
id::<1>([1u8]);
id::<{1 + 1}>([1u8, 2u8]);
```

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`.

# Reference-level explanation

[reference-level-explanation]: #reference-level-explanation

This new syntax has three forms: declarations, instantiations and references.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This new syntax has three forms: declarations, instantiations and references.
This new syntax has three forms: declarations, instantiations, and references.


"const generics declarations" can appear anywhere all other generic arguments declarations are valid:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"const generics declarations" can appear anywhere all other generic arguments declarations are valid:
"Const generics declarations" can appear anywhere all other generic arguments declarations are valid:


1. Function/method declaration;
1. Struct/Enum declaration;
1. `impl` declarations.

```rust
// 1
fn id<const N: usize>(...) { ... }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn id<const N: usize>(...) { ... }
fn id<const N: u64>(...) { ... }


// 2
struct SpecialArray<const N: usize> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
struct SpecialArray<const N: usize> {
struct SpecialArray<const N: u64> {

inner: [u8; N],
}

//3
impl<const N: usize> AbiEncode for str[N] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
impl<const N: usize> AbiEncode for str[N] {
impl<const N: u64> AbiEncode for str[N] {

...
}
```

"const generics instantiations" can appear anywhere all other generic argument instantiations are valid:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"const generics instantiations" can appear anywhere all other generic argument instantiations are valid:
"Const generics instantiations" can appear anywhere all other generic argument instantiations are valid:


1. Function/method reference;
1. Struct/Enum reference;
1. Fully qualified call paths.

```rust
// 1
id::<1>([1u8]);
special_array.do_something::<1>();

// 2
SpecialArray::<1>::new();

// 3
<SpecialArray::<1> as SpecialArrayTrait::<1>>::f();
```

"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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"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.
"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.


```rust
fn f<const I: usize>() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn f<const I: usize>() {
fn f<const I: u32>() {

__log(I);
}
```

Different from type generics, const generics cannot appear at:

1. constraints;
1. where types are expected.

```rust
// 1 - INVALID
fn f<const I: usize>() where I > 10 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn f<const I: usize>() where I > 10 {
fn f<const I: u8>() where I > 10 {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supporting constraints can be added to future possibilities. One case I see as interesting, is ensuring that an array size is greater then zero. Or for small vec implementations to ensure the upper bound is not to high.

...
}

// 2 - INVALID
fn f<const I: usize>() -> Vec<I> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn f<const I: usize>() -> Vec<I> {
fn f<const I: u64>() -> Vec<I> {

Vec::<I>::new()
}
```

## Const Value Specialization

By the nature of `impl` declarations, it is possible to specialize for some specific types. For example:

```rust
impl SomeStruct<bool> {
fn f() {
}
}
```

In the example above, `f` only is available when the generic argument of `SomeStruct` is known to be `bool`. This will not be supported for "const generics", which means that the example below will not be supported:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In the example above, `f` only is available when the generic argument of `SomeStruct` is known to be `bool`. This will not be supported for "const generics", which means that the example below will not be supported:
In the example above, `f` is only available when the generic argument of `SomeStruct` is known to be `bool`. This will not be supported for "const generics", which means that the example below will not be supported:


```rust
impl SomeIntegerStruct<1> {
...
}

impl SomeBoolStruct<false> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned, we should explicitly specify which types are supported. Out of this example with bool I assume all uint types + bool will be supported. I don't see a reason not to.

...
}
```

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.

## Monomorphization

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.

Prevention of code bloat will be the responsability of the optimizer.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Prevention of code bloat will be the responsability of the optimizer.
Prevention of code bloat will be the responsibility of the optimizer.


Monomorphization for const generics has one extra complexity to support arbitrary expressions it is needed to "solve" an equation. For example,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Monomorphization for const generics has one extra complexity to support arbitrary expressions it is needed to "solve" an equation. For example,
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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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`.
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`.


## Type Engine changes

By the nature of `const generics`, it will be possible to write expressions inside types. Initially, only simple references will be
supported, but at some point, more complex expressions will be needed, for example:

```sway
fn len<T, const N: u64>(a: [T; N]) { ... }

fn bigger<const N: usize>(a: [u64; N]) -> [u64; N + 1] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn bigger<const N: usize>(a: [u64; N]) -> [u64; N + 1] {
fn bigger<const N: u64>(a: [u64; N]) -> [u64; N + 1] {

[0; N + 1]
}
```

This poses the challenge of having an expression tree inside the type system. Currently `sway` already
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This poses the challenge of having an expression tree inside the type system. Currently `sway` already
This poses the challenge of having an expression tree inside of the type system. Currently Sway already

has three expression trees: `Expr`, `ExpressionKind` and `TyExpressionVariant`. This demands a new one,
given that the first two allow much more complex expressions than what `const generics` wants to support;
and last one is only create after some `TypeInfo` already exist, and thus cannot be used.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
and last one is only create after some `TypeInfo` already exist, and thus cannot be used.
and the last one is only created after some `TypeInfo` already exists, and thus cannot be used.


At the same time, the parser should be able to parse any expression and return a friendly error that such expression,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
At the same time, the parser should be able to parse any expression and return a friendly error that such expression,
At the same time, the parser should be able to parse any expression and return a friendly error that such expression

is not supported.

So, in case of a 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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
So, in case of a 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`.
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`.


These expressions will also increase the complexity of all types related algorithms such as:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
These expressions will also increase the complexity of all types related algorithms such as:
These expressions will also increase the complexity of all type related algorithms such as:


1. Unification
2. PartialEq
3. Hash

In the simplest case, it is very clear how to unify `TypeInfo::Array(..., Length::Literal(1))` and `TypeInfo::Array(..., Length::Expression("N"))`.
But more complex cases such as `TypeInfo::Array(..., Length::Expression("N"))` and `TypeInfo::Array(..., Length::Expression("N + 1"))`, is not clear
if these types are unifiable, equal or simply different.

## Method call search algorithm

When a method is called, the algorithm that searches which method is called uses the method `TraitMap::get_impls`.
Currently, this method does an `O(1)` search to find all methods applicable to a type. For example:

```sway
impl [u64; 1] {
fn len_for_size_one(&self) { ... }
}
```

would create a map with something like

```
Placeholder -> [...]
...
[u64; 1] -> [..., len_for_size_one,...]
...
```

The algorithms first create a `TypeRootFilter`, which is very similar to `TypeInfo`. And uses this `enum` to search the hash table.
After that, it "generalizes" the filter and searches for `TypeRootFilter::Placeholder`.

To fully support `const generics` and `const value specialization`, the compiler will now keep generalizing the
searched type until it hits `Placeholder`. For example, searching for `[u64; 1]` will actually search for:

1. [u64; 1];
1. [u64; Placeholder];
1. Placeholder;

The initial implementation will do this generalization only for `const generics`, but it also makes sense to
generalize this with other types such as `Vec<u64>`.

1. Vec\<u64>;
1. Vec\<Placeholder>;
1. Placeholder;

This gets more complex as the number of `generics` and `const generics` increases. For example:

```sway
struct VecWithSmallVecOptimization<T, const N: u64> { ... }
```

Searching for this type would search:

1. VecWithSmallVecOptimization\<u64, 1>
1. VecWithSmallVecOptimization\<Placeholder, 1>
1. VecWithSmallVecOptimization\<u64, Placeholder>
1. VecWithSmallVecOptimization\<Placeholder, Placeholder>
1. Placeholder

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps to explicitly mention that the insertion into trait map must already search for conflicting implementations in regard to const generic arguments.

Suggested change
When inserting into the `TraitMap` the search for duplicated definitions will need to be extended for const generic arguments. E.g.:
```sway
struct S<const N:u8> { }
impl<const N:u8> S<N> {
fn a(&self) { }
}
impl S<0> {
fn a(&self) { } // <<<--- ERROR: Conflicting definition.
fn b(&self) { } // <<<--- OK.
}
trait Trait { }
impl<const N:u64> Trait for [u8;N] { }
impl Trait for [u8;2] { } <<<--- ERROR: Conflicting implementation.

More research is needed to understand if this change can potentially change the semantics of any program written in `sway`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
More research is needed to understand if this change can potentially change the semantics of any program written in `sway`.
More research is needed to understand if this change can potentially change the semantics of any program written in Sway.


# Implementation Roadmap

1. Creation of the feature flag `const-generics`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. Creation of the feature flag `const-generics`;
1. Creation of the feature flag `const_generics`;

1. Implementation of "const generics references";
```rust
fn f<const I: usize>() { __log(I); }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn f<const I: usize>() { __log(I); }
fn f<const I: u8>() { __log(I); }

```
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";
```rust
impl<T, const N: usize> AbiEncode for [T; N] { ... }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
impl<T, const N: usize> AbiEncode for [T; N] { ... }
impl<T, const N: u64> AbiEncode for [T; N] { ... }

```
4. Being able to `encode` arrays of any size;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
4. Being able to `encode` arrays of any size;
4. Being able to `abi_encode` arrays of any size;

```rust
fn f<T, const N: usize>(s: [T; N]) -> raw_slice {
<[T; N] as AbiEncode>::abi_encode(...);
}
f::<1>([1])
```
5. Inference of the example above
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
5. Inference of the example above
5. Inference of the example above;

```rust
f([1]);
core::encode([1]);
```
6. Struct/enum support for const generics
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
6. Struct/enum support for const generics
6. Struct/enum support for const generics;

7. Function/method declaration;
8. `impl` declarations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
8. `impl` declarations.
8. `impl` declarations;

9. Function/method reference;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
9. Function/method reference;
9. Function/method reference.


# Drawbacks

[drawbacks]: #drawbacks

None

# Rationale and alternatives

[rationale-and-alternatives]: #rationale-and-alternatives

# Prior art

[prior-art]: #prior-art

This RFC is partially based on Rust's own const generic system: https://doc.rust-lang.org/reference/items/generics.html#const-generics, https://blog.rust-lang.org/inside-rust/2021/09/06/Splitting-const-generics.html, https://rust-lang.github.io/rfcs/2000-const-generics.html, https://doc.rust-lang.org/beta/unstable-book/language-features/generic-const-exprs.html
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This RFC is partially based on Rust's own const generic system: https://doc.rust-lang.org/reference/items/generics.html#const-generics, https://blog.rust-lang.org/inside-rust/2021/09/06/Splitting-const-generics.html, https://rust-lang.github.io/rfcs/2000-const-generics.html, https://doc.rust-lang.org/beta/unstable-book/language-features/generic-const-exprs.html
This RFC is partially based on Rust's own const generic system:
- https://doc.rust-lang.org/reference/items/generics.html#const-generics
- https://blog.rust-lang.org/inside-rust/2021/09/06/Splitting-const-generics.html
- https://rust-lang.github.io/rfcs/2000-const-generics.html
- https://doc.rust-lang.org/beta/unstable-book/language-features/generic-const-exprs.html


# Unresolved questions

[unresolved-questions]: #unresolved-questions

1. What is the impact of changing the "method called algorithm"?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. What is the impact of changing the "method called algorithm"?
1. What is the impact of changing the "method call search algorithm"?


# Future possibilities
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, implementing constraints like where N > 0.


[future-possibilities]: #future-possibilities
Loading