Skip to content

Commit

Permalink
Marker traits (#6871)
Browse files Browse the repository at this point in the history
## Description

This PR introduces the concept of marker traits to the language. It is
the first step towards implementing the [ABI errors
RFC](https://github.com/FuelLabs/sway-rfcs/blob/master/rfcs/0014-abi-errors.md).

Marker traits are traits automatically generated by the compiler. They
represent certain properties of types and cannot be explicitly
implemented by developers.

The PR implements a common infrastructure for generating and
type-checking marker traits as well as two concrete marker traits:
- `Error`: represents a type whose instances can be used as arguments
for the `panic` expression. (The `panic` expression is yet to be
implemented.)
- `Enum`: represents an enum type.

Combining these two marker traits in trait constraints allow expressing
constraints such is, e.g., "the error type must be an error enum":

```
fn panic_with_error<E>(err: E) where E: Error + Enum {
    panic err;
}
```

Note that the generic name `Enum` is sometimes used in our tests to
represent a dummy enum. In tests, it is almost always defined locally,
and sometimes explicitly imported, so it will never clash with the
`Enum` marker trait. A single test in which the clash occurred was
easily adapted by explicitly importing the dummy `Enum`.

The PR is the first step in implementing #6765.

## Checklist

- [x] I have linked to any relevant issues.
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [x] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [ ] If my change requires substantial documentation changes, I have
[requested support from the DevRel
team](https://github.com/FuelLabs/devrel-requests/issues/new/choose)
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [ ] I have added (or requested a maintainer to add) the necessary
`Breaking*` or `New Feature` labels where relevant.
- [x] I have done my best to ensure that my PR adheres to [the Fuel Labs
Code Review
Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md).
- [x] I have requested a review from the relevant team or maintainers.
  • Loading branch information
ironcev authored Feb 7, 2025
1 parent 8696e2c commit 1270bfa
Show file tree
Hide file tree
Showing 47 changed files with 1,181 additions and 325 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,10 @@ jobs:
run: cargo run --locked --release -p forc -- build --experimental storage_domains --release --locked --path ./test/src/sdk-harness
- name: Cargo Test sway-lib-std - Experimental Feature 'storage_domains'
run: cargo test --locked --release --manifest-path ./test/src/sdk-harness/Cargo.toml -- --nocapture
- name: Build All Tests - Experimental Feature 'error_type'
run: cargo run --locked --release -p forc -- build --experimental error_type --release --locked --path ./test/src/sdk-harness
- name: Cargo Test sway-lib-std - Experimental Feature 'error_type'
run: cargo test --locked --release --manifest-path ./test/src/sdk-harness/Cargo.toml -- --nocapture

forc-run-benchmarks:
runs-on: buildjet-4vcpu-ubuntu-2204
Expand Down Expand Up @@ -541,14 +545,20 @@ jobs:
run: forc build --path sway-lib-core && forc test --path sway-lib-core
- name: Run Core Unit Tests - Experimental feature 'storage_domains'
run: forc build --experimental storage_domains --path sway-lib-core && forc test --experimental storage_domains --path sway-lib-core
- name: Run Core Unit Tests - Experimental feature 'error_type'
run: forc build --experimental error_type --path sway-lib-core && forc test --experimental error_type --path sway-lib-core
- name: Run Std Unit Tests
run: forc build --path sway-lib-std && forc test --path sway-lib-std
- name: Run Std Unit Tests - Experimental feature 'storage_domains'
run: forc build --experimental storage_domains --path sway-lib-std && forc test --experimental storage_domains --path sway-lib-std
- name: Run Std Unit Tests - Experimental feature 'error_type'
run: forc build --experimental error_type --path sway-lib-std && forc test --experimental error_type --path sway-lib-std
- name: Run In Language Unit Tests
run: forc build --path test/src/in_language_tests && forc test --path test/src/in_language_tests
- name: Run In Language Unit Tests - Experimental feature 'storage_domains'
run: forc build --experimental storage_domains --path test/src/in_language_tests && forc test --experimental storage_domains --path test/src/in_language_tests
- name: Run In Language Unit Tests - Experimental feature 'error_type'
run: forc build --experimental error_type --path test/src/in_language_tests && forc test --experimental error_type --path test/src/in_language_tests

forc-pkg-fuels-deps-check:
runs-on: buildjet-4vcpu-ubuntu-2204
Expand Down
39 changes: 37 additions & 2 deletions docs/book/src/advanced/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,43 @@ trait MyTrait {

Check the `associated types` section on [associated types](./associated_types.md) page.

## Trait Constraints

When writing generic code, you can constraint the choice of types for a generic argument by using the `where` keyword. The `where` keyword specifies which traits the concrete generic parameter must implement. In the below example, the function `expects_some_trait` can be called only if the parameter `t` is of a type that has `SomeTrait` implemented. To call the `expects_both_traits`, parameter `t` must be of a type that implements _both_ `SomeTrait` and `SomeOtherTrait`.

```sway
trait SomeTrait { }
trait SomeOtherTrait { }
fn expects_some_trait<T>(t: T) where T: SomeTrait {
// ...
}
fn expects_some_other_trait<T>(t: T) where T: SomeOtherTrait {
// ...
}
fn expects_both_traits<T>(t: T) where T: SomeTrait + SomeOtherTrait {
// ...
}
```

## Marker Traits

Sway types can be classified in various ways according to their intrinsic properties. These classifications are represented as marker traits. Marker traits are implemented by the compiler and cannot be explicitly implemented in code.

E.g., all types whose instances can be used in the `panic` expression automatically implement the `Error` marker trait. We can use that trait, e.g., to specify that a generic argument must be compatible with the `panic` expression:

```sway
fn panic_with_error<E>(err: E) where E: Error {
panic err;
}
```

> **Note** `panic` expression and error types [have not yet been implemented](https://github.com/FuelLabs/sway/issues/6765)
All marker traits are defined in the `core::marker` module.

## Use Cases

### Custom Types (structs, enums)
Expand All @@ -160,8 +197,6 @@ fn play_game_with_deck<T>(a: Vec<T>) where T: Card {
}
```

> **Note** Trait constraints (i.e. using the `where` keyword) [have not yet been implemented](https://github.com/FuelLabs/sway/issues/970)
Now, if you want to use the function `play_game_with_deck` with your struct, you must implement `Card` for your struct. Note that the following code example assumes a dependency _games_ has been included in the `Forc.toml` file.

```sway
Expand Down
45 changes: 39 additions & 6 deletions sway-core/src/language/call_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
OrdWithEnginesContext, PartialEqWithEngines, PartialEqWithEnginesContext,
},
parsed::QualifiedPathType,
Engines, Ident, Namespace,
Engines, Ident, Namespace, TypeArgument,
};
use serde::{Deserialize, Serialize};
use std::{
Expand Down Expand Up @@ -284,20 +284,26 @@ impl<T: Spanned> Spanned for CallPath<T> {
if self.prefixes.is_empty() {
self.suffix.span()
} else {
let suffix_span = self.suffix.span();
let mut prefixes_spans = self
.prefixes
.iter()
.map(|x| x.span())
//LOC below should be removed when #21 goes in
// Depending on how the call path is constructed, we
// might have a situation that the parts do not belong
// to the same source and do not have the same source id.
// In that case, we will take only the suffix' span, as
// the span for the whole call path. Otherwise, we join
// the spans of all the parts.
.filter(|x| {
Arc::ptr_eq(x.src(), self.suffix.span().src())
&& x.source_id() == self.suffix.span().source_id()
Arc::ptr_eq(x.src(), suffix_span.src())
&& x.source_id() == suffix_span.source_id()
})
.peekable();
if prefixes_spans.peek().is_some() {
Span::join(Span::join_all(prefixes_spans), &self.suffix.span())
Span::join(Span::join_all(prefixes_spans), &suffix_span)
} else {
self.suffix.span()
suffix_span
}
}
}
Expand Down Expand Up @@ -391,6 +397,33 @@ impl CallPath {
}
converted
}

/// Create a string form of the given [CallPath] and zero or more [TypeArgument]s.
/// The returned string is convenient for displaying full names, including generic arguments, in help messages.
/// E.g.:
/// - `some::module::SomeType`
/// - `some::module::SomeGenericType<T, u64>`
///
/// Note that the trailing arguments are never separated by `::` from the suffix.
pub(crate) fn to_string_with_args(&self, engines: &Engines, args: &[TypeArgument]) -> String {
let args = args
.iter()
.map(|type_arg| engines.help_out(type_arg).to_string())
.collect::<Vec<_>>()
.join(", ");

format!(
"{}{}",
// TODO: Replace with a context aware string representation of the path
// once https://github.com/FuelLabs/sway/issues/6873 is fixed.
&self,
if args.is_empty() {
String::new()
} else {
format!("<{args}>")
}
)
}
}

impl<T: Clone> CallPath<T> {
Expand Down
1 change: 1 addition & 0 deletions sway-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod debug_generation;
pub mod decl_engine;
pub mod ir_generation;
pub mod language;
pub mod marker_traits;
mod metadata;
pub mod query_engine;
pub mod semantic_analysis;
Expand Down
35 changes: 35 additions & 0 deletions sway-core/src/marker_traits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use sway_types::{Ident, SourceEngine};

use crate::{
language::{parsed::ImplSelfOrTrait, ty::TyTraitDecl, CallPathType},
namespace::Module,
};

impl TyTraitDecl {
pub(crate) fn is_marker_trait(&self) -> bool {
assert!(
matches!(self.call_path.callpath_type, CallPathType::Full),
"call paths of trait declarations must always be full paths"
);

is_core_marker_module_path(&self.call_path.prefixes)
}
}

impl Module {
pub(crate) fn is_core_marker_module(&self) -> bool {
is_core_marker_module_path(self.mod_path())
}
}

impl ImplSelfOrTrait {
pub(crate) fn is_autogenerated(&self, source_engine: &SourceEngine) -> bool {
source_engine
.is_span_in_autogenerated(&self.block_span)
.unwrap_or(false)
}
}

fn is_core_marker_module_path(path: &[Ident]) -> bool {
path.len() == 2 && path[0].as_str() == "core" && path[1].as_str() == "marker"
}
Loading

0 comments on commit 1270bfa

Please sign in to comment.