Skip to content

Commit

Permalink
Dereferencing operator . on struct field access (#5538)
Browse files Browse the repository at this point in the history
## Description

This PR implements struct field access operator `.` for
[references](https://github.com/FuelLabs/sway-rfcs/blob/ironcev/amend-references/files/0010-references.sw).
The overall effort related to references is tracked in #5063.

`.` is defined for references to structs using this recursive
definition: `<reference>.<field name> := (*<reference>).<field name>`.

This eliminates the need for the dereferencing operator `*` when working
with references to structs:

```Sway
let s = Struct { x: 0 };

let r = &&&s;

assert((***r).x == r.x);
```
```Sway
let r = &&&Struct { r_a: &&A { x: 1 }, &B { y: 2 } ];
assert(r.r_a.x == 1);
assert(r.r_b.y == 2);
```

Additionally, the PR adds a `Diagnostic` for the
`StorageFieldDoesNotExist` error and aligns it with the
`StructFieldDoesNotExist` error.

## Demo
![Field access requires a struct - On
expression](https://github.com/FuelLabs/sway/assets/4142833/6dcad917-2a91-47ba-8ff7-aa13dc681bcb))

![Field access requires a struct - Storage
variable](https://github.com/FuelLabs/sway/assets/4142833/ab3fc2d4-bc87-48dd-855c-b4001c43199e)

![Storage field does not
exist](https://github.com/FuelLabs/sway/assets/4142833/6a7f1800-dbad-4e0a-af4a-ed5f9f393f70)

## Checklist

- [x] I have linked to any relevant issues.
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [ ] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [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 4, 2024
1 parent d6734b4 commit 575d2bb
Show file tree
Hide file tree
Showing 25 changed files with 947 additions and 235 deletions.
19 changes: 14 additions & 5 deletions sway-core/src/ir_generation/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,25 @@ pub(super) fn get_struct_for_types(
Ok(Type::new_struct(context, types))
}

/// For the [TypeInfo::Struct] given by `struct_type_id` and the
/// [ty::ProjectionKind::StructField] given by `field_kind`
/// returns the name of the struct, and the field index within
/// the struct together with the field [TypeId] if the field exists
/// on the struct.
///
/// Returns `None` if the `struct_type_id` is not a [TypeInfo::Struct]
/// or an alias to a [TypeInfo::Struct] or if the `field_kind`
/// is not a [ty::ProjectionKind::StructField].
pub(super) fn get_struct_name_field_index_and_type(
type_engine: &TypeEngine,
decl_engine: &DeclEngine,
field_type: TypeId,
struct_type_id: TypeId,
field_kind: ty::ProjectionKind,
) -> Option<(String, Option<(u64, TypeId)>)> {
let ty_info = type_engine
.to_typeinfo(field_type, &field_kind.span())
let struct_ty_info = type_engine
.to_typeinfo(struct_type_id, &field_kind.span())
.ok()?;
match (ty_info, &field_kind) {
match (struct_ty_info, &field_kind) {
(TypeInfo::Struct(decl_ref), ty::ProjectionKind::StructField { name: field_name }) => {
let decl = decl_engine.get_struct(&decl_ref);
Some((
Expand All @@ -110,7 +119,7 @@ pub(super) fn get_struct_name_field_index_and_type(
},
_,
) => get_struct_name_field_index_and_type(type_engine, decl_engine, type_id, field_kind),
_otherwise => None,
_ => None,
}
}

Expand Down
16 changes: 13 additions & 3 deletions sway-core/src/language/ty/declaration/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ impl TyStorageDecl {
}
None => {
return Err(handler.emit_err(CompileError::StorageFieldDoesNotExist {
name: first_field.clone(),
span: first_field.span(),
field_name: first_field.into(),
available_fields: storage_fields.iter().map(|sf| sf.name.clone()).collect(),
storage_decl_span: self.span(),
}));
}
};
Expand All @@ -113,6 +114,13 @@ impl TyStorageDecl {
previous_field = first_field;
previous_field_type_id = initial_field_type;

// Storage cannot contain references, so there is no need for checking
// if the declaration is a reference to a struct. References can still
// be erroneously declared in the storage, and the type behind a concrete
// field access might be a reference to struct, but we do not treat that
// as a special case but just another one "not a struct".
// The FieldAccessOnNonStruct error message will explain that in the case
// of storage access, fields can be accessed only on structs.
let get_struct_decl = |type_id: TypeId| match &*type_engine.get(type_id) {
TypeInfo::Struct(decl_ref) => Some(decl_engine.get_struct(decl_ref)),
_ => None,
Expand Down Expand Up @@ -191,8 +199,10 @@ impl TyStorageDecl {
}
None => {
return Err(handler.emit_err(CompileError::FieldAccessOnNonStruct {
span: previous_field.span(),
actually: engines.help_out(previous_field_type_id).to_string(),
storage_variable: Some(previous_field.to_string()),
field_name: field.into(),
span: previous_field.span(),
}))
}
};
Expand Down
7 changes: 7 additions & 0 deletions sway-core/src/language/ty/expression/expression_variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ pub enum TyExpressionVariant {
prefix: Box<TyExpression>,
field_to_access: TyStructField,
field_instantiation_span: Span,
/// Final resolved type of the `prefix` part
/// of the expression. This will always be
/// a [TypeId] of a struct, never an alias
/// or a reference to a struct.
/// The original parent might be an alias
/// or a direct or indirect reference to a
/// struct.
resolved_type_of_parent: TypeId,
},
TupleElemAccess {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1851,6 +1851,7 @@ impl ty::TyExpression {

current_type = type_engine.get_unaliased(referenced_type_id);
}
TypeInfo::ErrorRecovery(err) => return Err(*err),
_ => {
return Err(handler.emit_err(CompileError::NotIndexable {
actually: engines.help_out(prefix_type_id).to_string(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
use sway_error::handler::{ErrorEmitted, Handler};
use sway_error::{
error::{CompileError, StructFieldUsageContext},
handler::{ErrorEmitted, Handler},
warning::{CompileWarning, Warning},
};
use sway_types::{Ident, Span, Spanned};

use crate::{language::ty, Engines, Namespace};
use crate::{
language::ty::{self, StructAccessInfo},
Engines, Namespace, TypeInfo,
};

pub(crate) fn instantiate_struct_field_access(
handler: &Handler,
Expand All @@ -12,23 +19,101 @@ pub(crate) fn instantiate_struct_field_access(
span: Span,
) -> Result<ty::TyExpression, ErrorEmitted> {
let type_engine = engines.te();
let field_instantiation_span = field_to_access.span();
let field = type_engine.get(parent.return_type).apply_subfields(
handler,
engines,
namespace,
&[field_to_access],
&parent.span,
)?;
let exp = ty::TyExpression {

let mut current_prefix_te = Box::new(parent);
let mut current_type = type_engine.get_unaliased(current_prefix_te.return_type);

let prefix_type_id = current_prefix_te.return_type;
let prefix_span = current_prefix_te.span.clone();

// Create the prefix part of the final struct field access expression.
// This might be an expression that directly evaluates to a struct type,
// or an arbitrary number of dereferencing expressions where the last one
// dereferences to a struct type.
//
// We will either hit a struct at the end or return an error, so the
// loop cannot be endless.
while !current_type.is_struct() {
match &*current_type {
TypeInfo::Ref(referenced_type) => {
let referenced_type_id = referenced_type.type_id;

current_prefix_te = Box::new(ty::TyExpression {
expression: ty::TyExpressionVariant::Deref(current_prefix_te),
return_type: referenced_type_id,
span: prefix_span.clone(),
});

current_type = type_engine.get_unaliased(referenced_type_id);
}
TypeInfo::ErrorRecovery(err) => return Err(*err),
_ => {
return Err(handler.emit_err(CompileError::FieldAccessOnNonStruct {
actually: engines.help_out(prefix_type_id).to_string(),
storage_variable: None,
field_name: (&field_to_access).into(),
span: prefix_span.clone(),
}))
}
};
}

let TypeInfo::Struct(struct_decl_ref) = &*current_type else {
panic!("The current type must be a struct.");
};

let decl = engines.de().get_struct(struct_decl_ref);
let (struct_can_be_changed, is_public_struct_access) =
StructAccessInfo::get_info(&decl, namespace).into();

let field = match decl.find_field(&field_to_access) {
Some(field) => {
if is_public_struct_access && field.is_private() {
// TODO: Uncomment this code and delete the one with warnings once struct field privacy becomes a hard error.
// https://github.com/FuelLabs/sway/issues/5520
// return Err(handler.emit_err(CompileError::StructFieldIsPrivate {
// field_name: (&field_to_access).into(),
// struct_name: decl.call_path.suffix.clone(),
// field_decl_span: field.name.span(),
// struct_can_be_changed,
// usage_context: StructFieldUsageContext::StructFieldAccess,
// }));
handler.emit_warn(CompileWarning {
span: field_to_access.span(),
warning_content: Warning::StructFieldIsPrivate {
field_name: (&field_to_access).into(),
struct_name: decl.call_path.suffix.clone(),
field_decl_span: field.name.span(),
struct_can_be_changed,
usage_context: StructFieldUsageContext::StructFieldAccess,
},
});
}

field.clone()
}
None => {
return Err(handler.emit_err(CompileError::StructFieldDoesNotExist {
field_name: (&field_to_access).into(),
available_fields: decl.accessible_fields_names(is_public_struct_access),
is_public_struct_access,
struct_name: decl.call_path.suffix.clone(),
struct_decl_span: decl.span(),
struct_is_empty: decl.is_empty(),
usage_context: StructFieldUsageContext::StructFieldAccess,
}));
}
};

let return_type = field.type_argument.type_id;
Ok(ty::TyExpression {
expression: ty::TyExpressionVariant::StructFieldAccess {
resolved_type_of_parent: parent.return_type,
prefix: Box::new(parent),
field_to_access: field.clone(),
field_instantiation_span,
resolved_type_of_parent: current_prefix_te.return_type,
prefix: current_prefix_te,
field_to_access: field,
field_instantiation_span: field_to_access.span(),
},
return_type: field.type_argument.type_id,
return_type,
span,
};
Ok(exp)
})
}
6 changes: 4 additions & 2 deletions sway-core/src/semantic_analysis/namespace/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,10 +452,12 @@ impl Items {
// TODO: Include the closing square bracket into the error span.
full_span_for_error = Span::join(full_span_for_error, index_span.clone());
}
(actually, ty::ProjectionKind::StructField { .. }) => {
(actually, ty::ProjectionKind::StructField { name }) => {
return Err(handler.emit_err(CompileError::FieldAccessOnNonStruct {
span: full_span_for_error,
actually: engines.help_out(actually).to_string(),
storage_variable: None,
field_name: name.into(),
span: full_span_for_error,
}));
}
(actually, ty::ProjectionKind::TupleField { .. }) => {
Expand Down
110 changes: 7 additions & 103 deletions sway-core/src/type_system/info.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
use crate::{
decl_engine::{DeclEngine, DeclRefEnum, DeclRefStruct},
engine_threading::*,
language::{
ty::{self, StructAccessInfo},
CallPath, QualifiedCallPath,
},
language::{ty, CallPath, QualifiedCallPath},
type_system::priv_prelude::*,
Ident, Namespace,
Ident,
};
use sway_error::{
error::{CompileError, StructFieldUsageContext},
error::CompileError,
handler::{ErrorEmitted, Handler},
warning::{CompileWarning, Warning},
};
use sway_types::{integer_bits::IntegerBits, span::Span, SourceId, Spanned};

Expand Down Expand Up @@ -1096,6 +1092,10 @@ impl TypeInfo {
matches!(self, TypeInfo::Array(_, _))
}

pub fn is_struct(&self) -> bool {
matches!(self, TypeInfo::Struct(_))
}

pub(crate) fn apply_type_arguments(
self,
handler: &Handler,
Expand Down Expand Up @@ -1245,102 +1245,6 @@ impl TypeInfo {
}
}

/// Given a [TypeInfo] `self` and a list of [Ident]'s `subfields`,
/// iterate through the elements of `subfields` as `subfield`,
/// and recursively apply `subfield` to `self`.
///
/// Returns a [ty::TyStructField] when all `subfields` could be
/// applied without error.
///
/// Returns an error when subfields could not be applied:
/// 1) in the case where `self` is not a [TypeInfo::Struct]
/// 2) in the case where `subfields` is empty
/// 3) in the case where a `subfield` does not exist on `self`
/// 4) in the case where a `subfield` is private and only public subfields can be accessed
pub(crate) fn apply_subfields(
&self,
handler: &Handler,
engines: &Engines,
namespace: &Namespace,
subfields: &[Ident],
span: &Span,
) -> Result<ty::TyStructField, ErrorEmitted> {
let type_engine = engines.te();
let decl_engine = engines.de();
match (self, subfields.split_first()) {
(TypeInfo::Struct { .. } | TypeInfo::Alias { .. }, None) => {
panic!("Trying to apply an empty list of subfields");
}
(TypeInfo::Struct(decl_ref), Some((first, rest))) => {
let decl = decl_engine.get_struct(decl_ref);
let (struct_can_be_changed, is_public_struct_access) =
StructAccessInfo::get_info(&decl, namespace).into();

let field = match decl.find_field(first) {
Some(field) => {
if is_public_struct_access && field.is_private() {
// TODO: Uncomment this code and delete the one with warnings once struct field privacy becomes a hard error.
// https://github.com/FuelLabs/sway/issues/5520
// return Err(handler.emit_err(CompileError::StructFieldIsPrivate {
// field_name: first.into(),
// struct_name: decl.call_path.suffix.clone(),
// field_decl_span: field.name.span(),
// struct_can_be_changed,
// usage_context: StructFieldUsageContext::StructFieldAccess,
// }));
handler.emit_warn(CompileWarning {
span: first.span(),
warning_content: Warning::StructFieldIsPrivate {
field_name: first.into(),
struct_name: decl.call_path.suffix.clone(),
field_decl_span: field.name.span(),
struct_can_be_changed,
usage_context: StructFieldUsageContext::StructFieldAccess,
},
});
}

field.clone()
}
None => {
return Err(handler.emit_err(CompileError::StructFieldDoesNotExist {
field_name: first.into(),
available_fields: decl.accessible_fields_names(is_public_struct_access),
is_public_struct_access,
struct_name: decl.call_path.suffix.clone(),
struct_decl_span: decl.span(),
struct_is_empty: decl.is_empty(),
usage_context: StructFieldUsageContext::StructFieldAccess,
}));
}
};
let field = if rest.is_empty() {
field
} else {
type_engine
.get(field.type_argument.type_id)
.apply_subfields(handler, engines, namespace, rest, span)?
};
Ok(field)
}
(
TypeInfo::Alias {
ty: TypeArgument { type_id, .. },
..
},
_,
) => type_engine
.get(*type_id)
.apply_subfields(handler, engines, namespace, subfields, span),
(TypeInfo::ErrorRecovery(err), _) => Err(*err),
// TODO-IG: Take a close look on this when implementing dereferencing.
(type_info, _) => Err(handler.emit_err(CompileError::FieldAccessOnNonStruct {
actually: format!("{:?}", engines.help_out(type_info)),
span: span.clone(),
})),
}
}

pub(crate) fn can_change(&self, decl_engine: &DeclEngine) -> bool {
// TODO: there might be an optimization here that if the type params hold
// only non-dynamic types, then it doesn't matter that there are type params
Expand Down
Loading

0 comments on commit 575d2bb

Please sign in to comment.