Skip to content
Merged
Changes from 1 commit
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
167 changes: 167 additions & 0 deletions working/0125-static-immutability/feature-specification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Shared immutable objects

[email protected]

Status: Draft

This describes a possible solution for:
- [Communication between isolates](https://github.com/dart-lang/language/issues/124)
- [Building immutable collections](https://github.com/dart-lang/language/issues/117)
- [Unwanted mutation of lists in Flutter](https://github.com/dart-lang/sdk/issues/27755)

## Summary

This describes a way to declare classes that produce deeply immutable object
graphs that are shared across isolates.

## Syntax

We add a section to class headers for expressing class and generic constraints,
along with an "immutable" constraint.

```dart
class Value<T> extends Scalar<T> implements Constant is immutable
where T is immutable {
}
```

Mixin declarations may also be marked `immutable`.

Generic method headers may also express generic constraints.

```
foo<S, T, where T is immutable>(Value<T> v) {

}
```

### Alternative syntax

Instead of adding constraints, a simpler approach is to add a marker interface
`Immutable`. The property expressed by the constraint `T is immutable` then
becomes expressed by `implements Immutable` in the case of a class, or `T
extends Immutable` in the case of a type variable `T`.

## Static checking
A class marked with `immutable` is subject to the following additional static
checks.

- Every field in an immutable class (including any superclass fields) must be
final.
- Every field in an immutable class (including any superclass fields) must have
a static type which is immutable.
- Every other class which implements the interface of an immutable class
(including via extension or mixing in) must also be immutable.

The types `int`, `double`, `bool`, `String`, `Type`, and `Symbol` are considered
immutable.

## Allocation of immutable objects

Immutable objects are allocated as usual in an isolate local
nursery. (Alternatively, it might be preferable to maintain a separate isolate
local shared object nursery for allocating only shared objects). However, when
they are tenured, they are tenured to a global heap which is shared by all
isolates in the process, and which is inhabited solely by immutable shared
objects.

The shared object heap cannot have pointers into the isolate local heaps, and so
garbage collection of an isolate local heap does not require coordination with
other isolates.

The isolate local heap can have pointers into the shared global heap, and so
either these must be tracked via write barriers and treated as roots when
collecting the shared global heap, or else collection of the shared global heap
might require cross-isolate coordination.

Tenuring objects into the shared global heap requires locking or pausing
isolates. Bulk reservation of allocation regions could potentially be used to
mitigate this.

Issue: It is possible that a large object may need to be tenured before it has
been fully initialized. This would allow writes into the shared heap. This
should not be problematic semantically since the object cannot be visible in
other isolates prior to initialization, but it may complicate the GC model.
This does not seem deeply problematic - a number of solutions seem plausible.

## Sharing of immutable objects

The SendPort class is extended with a new method `void share<T, where T is
immutable>(T message)` which given a reference to an immutable object graph,
shares that reference with all receivers of the SentPort. Note that the object
Copy link
Member

Choose a reason for hiding this comment

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

What does "shares" mean?
Is it sent on the port, or is it just prepared for sending.

What class does the object have on the "other side"?
Can it access static state? Other types? Can an immutable object have a method like:

foo() => new _Foo(_staticCounter++);

If the receiving isolate has not imported the library declaring the class of the object, can you send it? If you do, what is its run-time type? Which static state can it access?

If the receiving isolate has imported the same library, what is the run-time type of the received object? We have two different instances of the library (they have different static state), so which static state will the object access?

Copy link
Member Author

Choose a reason for hiding this comment

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

It is sent on the port.

I would expect that it can access static state, and every isolate has its own instance of the static state.

Broadly, to your questions, I would expect isolates to share the same code, hence have the same classes loaded, etc. This is what the (very minimal, vague) docs for Isolate.spawn seem to describe? If there are other ways to create isolates that don't share this property, I would suggest that they not be able to access the shared heap.

Copy link
Member

Choose a reason for hiding this comment

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

You can use Isolate.spawnUri to run arbitrary code that doesn't need to have anything in common with the spawning isolate. You can usually only send JSON-like structures between such isolates.

Copy link
Member Author

Choose a reason for hiding this comment

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

is not copied since it and all sub-components of it are in the shared heap.

An object which is shared before it has been tenured will likely need to be
tenured when it is shared.

It should be the case that every object is fully initialized before it can be
shared. The intent of the static checks specified above are to guarantee this.

It should be the case that no object that has been shared can be mutated. The
intent of the static checks specified above are to guarantee this.

## Immutable collections

The following additional immutable classes are added to the core libraries:
`ImmutableList` which implements `List`, `ImmutableMap` which implements `Map`,
and `ImmutableSet` which implements `Set`.

### Collection initialization
Instances of these collections may be allocated and assigned to local variables
in a modifiable state. Mutation operations may be performed on such an instance
up until the first point at which the instance escapes (that is, is captured by
a closure, is assigned to another variable or setter, or is passed as a
parameter). It is a static error if a mutation operation is performed on an
Copy link
Member

Choose a reason for hiding this comment

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

So we have "mutate until frozen" collections.
Do we allow that for other, user written, immutable types too?

(We can perhaps get away with this by saying that you can only mutate the object using cascades on the object creation expression, and that mutating methods on an immutable object may not leak this in any way. Still sounds complicated).

Copy link
Member Author

Choose a reason for hiding this comment

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

I raised this question below - if there's need for it we can do it. It's basically a limited use of typestate. How limited depends on how much power we need or don't need.

instance of one of these classes:
- at any point not intra-procedurally dominated by the allocation point of the
instance
- at any point where the instance escapes along any path from the allocation
point to the mutation operation.

Instances that are allocated to initialize fields or top level variables are
always initialized in an umodifiable state.

### Runtime immutability
As with the result of the current `List.unmodifiable` constructor, mutation
operations on an instance of an immutable collection shall throw (except in the
limited cases described in the initialization section above). Note that the
static checks described above prevent mutation operations from being accessed on
an instance of immutable type. However, the immutable collections implement
their mutable interfaces, and hence the mutation operations may be reached by
subsuming into the mutable type.

### Literals

A collection literal which appears in a context where the static type required
by the context is an immutable collection type shall be allocated as an
immutable collection.

```
ImmutableList<int> l = [ 3 ];
```
Question: Do we need additional syntax for the case where a static type context
is not required?

```
Copy link
Member

Choose a reason for hiding this comment

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

Yes, please! immutability is really useful for collections outside of supporting issolates, etc

Copy link

Choose a reason for hiding this comment

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

True, but if we get the semantics wrong we may find ourselves trying to retrofit immutables onto isolates. So I really prefer that we consider isolates immediately.

Copy link
Member

Choose a reason for hiding this comment

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

Sure. Just saying that it's nice on its own, too.

Copy link
Member

Choose a reason for hiding this comment

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

There is a difference between shallowly unmodifiable and deeply immutable objects, and we probably want both.

Copy link
Member Author

Choose a reason for hiding this comment

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

What does shallow immutability give you that we don't already have with final fields?

var l = ^[3];
```

## Immutable functions

There is no way to describe the type of an immutable function. If important, we
could add a type for immutable closures. A function is immutable if every free
variable of the function is immutable.

## Immutable top type

There is no top type for immutable types. It might be useful to have a type
`Immutable`, to express the type of fields of immutable objects which are
intended to hold instances of multiple types which do not otherwise share a
common super-interface.

## Javascript

Currently, isolates are not supported in Javascript. If we revisit that, we are
unlikely to be able to support this in full on the web. It is possible that we
Copy link
Member

Choose a reason for hiding this comment

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

I assume that all Immutable types will work fine in JS – just the isolate APIs won't be available.

I'm sure there will be places where we can generate better JS if we know things are immutable, though – so I still consider this a "feature" – even for web-only code.

CC @rakudrama @jmesserly for comments...

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it's the communication that wouldn't work out of the box.

Copy link

Choose a reason for hiding this comment

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

I think cloning objects should be a sufficient polyfill. You don't get the performance benefits from memory sharing, but you still get the benefit of a second isolate (i.e. web worker).

may be able to define a subset of immutable objects which can be implemented as
a layer over shared typed data buffers.