Skip to content

Commit

Permalink
Recovering to owned values when assigning fields
Browse files Browse the repository at this point in the history
When assigning a process' field, we allow the value type to be owned
provided it originated from a `recover` expression, allowing for code
such as this:

    class async Foo[T] {
      let @field: T
    }

    Foo { @field = recover { ... } }

This way one can take `uni T` values as input but explicitly store them
as owned `T` values.

This commit also fixes a bug where it would be possible to assign
process field values using the syntax `receiver.process = value`, which
shouldn't be allowed.

This fixes #641.

Changelog: added
  • Loading branch information
yorickpeterse committed Dec 6, 2023
1 parent c9c01ac commit 1b61961
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 14 deletions.
17 changes: 17 additions & 0 deletions compiler/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,23 @@ impl Diagnostics {
);
}

pub(crate) fn unavailable_process_field(
&mut self,
name: &str,
file: PathBuf,
location: SourceLocation,
) {
self.error(
DiagnosticId::InvalidSymbol,
format!(
"the field '{}' can only be used by the owning process",
name
),
file,
location,
);
}

pub(crate) fn duplicate_symbol(
&mut self,
name: &str,
Expand Down
22 changes: 22 additions & 0 deletions compiler/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,10 @@ impl Expression {
pub(crate) fn is_self(&self) -> bool {
matches!(self, Expression::SelfObject(_))
}

pub(crate) fn is_recover(&self) -> bool {
matches!(self, Expression::Recover(_))
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -6128,4 +6132,22 @@ mod tests {
}))
);
}

#[test]
fn test_expression_is_recover() {
let int = Expression::Int(Box::new(IntLiteral {
value: 0,
resolved_type: types::TypeRef::Unknown,
location: cols(1, 1),
}));

let recover = Expression::Recover(Box::new(Recover {
resolved_type: types::TypeRef::Unknown,
body: vec![int.clone()],
location: cols(1, 1),
}));

assert!(!int.is_recover());
assert!(recover.is_recover());
}
}
41 changes: 27 additions & 14 deletions compiler/src/type_check/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1650,7 +1650,14 @@ impl<'a> CheckMethodBody<'a> {
);
}

if require_send && !value.is_sendable(self.db()) {
// The values assigned to fields of processes must be sendable as
// part of the assignment. If the value is a `recover` expression
// that returns an owned value we _do_ allow this, because at that
// point the owned value is sendable.
if require_send
&& !value.is_sendable(self.db())
&& !field.value.is_recover()
{
self.state.diagnostics.unsendable_field_value(
name,
format_type(self.db(), value),
Expand Down Expand Up @@ -3401,6 +3408,23 @@ impl<'a> CheckMethodBody<'a> {
scope: &mut LexicalScope,
) -> bool {
let name = &node.name.name;
let (ins, field) = if let TypeId::ClassInstance(ins) = receiver_id {
if let Some(field) = ins.instance_of().field(self.db(), name) {
(ins, field)
} else {
return false;
}
} else {
return false;
};

if ins.instance_of().kind(self.db()).is_async() {
self.state.diagnostics.unavailable_process_field(
name,
self.file(),
node.location.clone(),
);
}

// When using `self.field = value`, none of the below is applicable, nor
// do we need to calculate the field type as it's already cached.
Expand All @@ -3424,16 +3448,6 @@ impl<'a> CheckMethodBody<'a> {
};
}

let (ins, field) = if let TypeId::ClassInstance(ins) = receiver_id {
if let Some(field) = ins.instance_of().field(self.db(), name) {
(ins, field)
} else {
return false;
}
} else {
return false;
};

if !field.is_visible_to(self.db(), self.module) {
self.state.diagnostics.private_field(
name,
Expand Down Expand Up @@ -3866,9 +3880,8 @@ impl<'a> CheckMethodBody<'a> {
// same non-generic process (e.g. every instance `class async Foo {}`
// has the same TypeId).
if ins.instance_of().kind(self.db()).is_async() {
self.state.diagnostics.error(
DiagnosticId::InvalidSymbol,
"process fields are only available inside the process itself",
self.state.diagnostics.unavailable_process_field(
name,
self.file(),
node.location.clone(),
);
Expand Down
10 changes: 10 additions & 0 deletions std/test/diagnostics/assign_async_field_recover_owned.inko
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class async Thing[A, B] {
let @a: A
let @b: B

fn static new(a: A, b: uni B) -> Thing[A, B] {
Thing { @a = a, @b = recover b }
}
}

# assign_async_field_recover_owned.inko:6:18 error(invalid-symbol): the field 'a' can't be assigned a value of type 'A', as it's not sendable
19 changes: 19 additions & 0 deletions std/test/diagnostics/process_fields.inko
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class async Foo {
let @value: Int

fn async mut update(value: Int) {
# This is OK because it's safe to assign fields from within the process that
# owns them.
@value = value
}
}

fn example {
let foo = Foo { @value = 42 }

foo.value
foo.value = 50
}

# process_fields.inko:14:3 error(invalid-symbol): the field 'value' can only be used by the owning process
# process_fields.inko:15:3 error(invalid-symbol): the field 'value' can only be used by the owning process

0 comments on commit 1b61961

Please sign in to comment.