Description
I would like to suggest splitting changes to identity()
rules from the records proposal into a separate proposal and allow defining classes with deep structural identity.
My proposal would be as follows: any class can be marked as having structural identity. For the purposes if this proposal we can think that this can be done by annotating a class with @pragma('value-type')
, though a more user-friendly syntax can also be considered.
If identical(x, y)
is called with x
and y
being instances of the same class C
and this class uses structural identity then identical(x, y)
is defined recursively as
identical(x, y) := identical(x.runtimeType, y.runtimeType) ∧ ∀ f ∈ fieldsOf(C) . identical(x.f, y.f);
Motivation
I would like to make this change to:
- Resolve semantical discrepancies around handling of SIMD values by the Dart VM (see Dart VM does not preserve identities for SIMD objects sdk#43255).
- Open new unboxing opportunities for the Dart VM, e.g. by allowing users to define their own value types which could be stored inline in the enclosing object.
Current state
Currently int
and double
values have no discernible identity in Dart (neither natively or when compiled to JS).
String
has distinct and preserved identity in native Dart, but no discernible identity when compiled to JS.
Dart VM does not preserve identity of SIMD values (in violation of the spec), but dart2js does.
Implementation Concerns
-
There is a concern that introducing special treatment into
identical
would degrade its performance too much. Potential mitigation could be to restrict which classes can be marked as having structural identity, e.g. requiring such classes toextend Object
and not have any superinterfaces would allow to optimise allidentical(x,y)
invocations where eitherx
ory
are known to have a non-Object
, non-structural-identity static type. -
Capitalising on unboxing opportunities might require some from of derived pointers, consider
@pragma('value-type') class Rect { final double x,y,w,h; } void op(Rect r) { } class A { Rect r; void foo() { // If [r] is unboxed then naive implementation would require // copying [r] to the stack to pass it to [op]. A more sophisticated // implementation would be to pass a (derived) pointer to [this.r] // instead - something that requires some level of sophistication in GC. // Possible approach to pass a pair `(this, &this.r)`. op(r); } }