Skip to content

Instantly share code, notes, and snippets.

@lrhn
Last active November 3, 2022 15:53
Show Gist options
  • Save lrhn/26265f84410de1055630ee67d986f801 to your computer and use it in GitHub Desktop.
Save lrhn/26265f84410de1055630ee67d986f801 to your computer and use it in GitHub Desktop.
View specification using `view`/`is`, with simple zero-cost forwarding.

There is a new version: https://gist.github.com/lrhn/ab2b5d81c22d0fc0ad8926b5743a50ed

Dart view specification

Author: lrn@google.com
Version: 0.9

Syntax

<view-declaration> ::=
  <metadata>
  'view' <identifier> <typeParams>? <parameterList> ('is' typeList)? '{'
     <member-declaration>*
  '}'

Example:

view MyString(String _string) is String {
  int operator[](int index) => _string.codeUnitAt(index);  
}

The <argumentList> must have precisely one parameter. Can be positional or named, and required or optional.

The type of that parameter is called the “representation type” of the view.

If the view is generic (has a <typeParams>), the representation type can depend on the type parameters, as can the is-clause’s type list.

  • The <member-declaration>s must not contain any instance variable declarations or non-redirecting generative constructors.
  • The view must not declare an unnamed constructor. (The primary constructor syntax implicitly introduces an unnamed constructor.)
  • The view must not declare a member with the same name as the primary constructor parameter.
  • The view must not declare a concrete member with the same base name as an instance member of Object (==, hashCode, toString, runtimeType, noSuchMethod).

Static semantics

Declaration

The declaration

view I<X1 extends B1, ..., Xn extends Bn>(T<X1, ..., Xn> name) is S1<X1, ..., Xn>, ..., Sk<X1, ..., Xn> { … }

introduces a new type I per type instantiation of X1 ..Xn. This is a view type with representation type T<X1,..,Xn>.

The Si<X1,…,Xn> represents some type expression which can be parameterized by any of X1..Xn. They must no be dynamic or Never.

  • If Si<X1,..,XN> is a view type, with representation type Ti, then T<X1,..,Xn> must be a subtype of Ti.
  • Otherwise, T<X1,..,Xn> must be a subtype of Si<X1,..,XN>.

There must not be any cycles in the is-clause types. The types must form a DAG (the same type can occur more than once, we allow diamond hiearchies).

The same generic type (view or non-view) must not occur multiple times in the supertype DAG with different type instantiations. (No view Foo(List<int> x) is List<num>, List<Object>, not view C<X>(X x){} and view D(int) is C<num>, C<Object>, and not even if the types are only transitive is-super-types.)

The type I<X1, …, Xn> is then an immediate subtype of each Si<X1, …, Xn>.

The instance members of I have three different forms: View members (refers to a concrete view member declaration), interface members (has a member signature which is a subtype of the signature of the same member on the representation type), and conflicted members (which cannot be invoked).

  • A synthetic unnamed constructor induced by the primary constructor syntax:

  • external const I(T<X1, ..., Xn> name);

    which effectively does name as I<X1, …, Xn>, a cast which cannot fail for a valid argument.

  • A synthetic getter named name of the form T<X1, …, Xn> get name => this as T<X1, …, Xn>;.

  • The members declared by I itself.

    • If the member declaration is not abstract (it’s a view member), the member must not be an instance variable declaration, and parameters cannot be declared covariant. Such members are view declarations referring to themselves.
    • If the member is abstract (no body, abstract instance variable), the introduced signatur(s, if non-final field) must be a supertype of the signature of the same-named member of the representation type. Such members are interface members with the same signature as the declaration.
  • For each name n where any of S1, …, Sk has a member of that name, and I does not declare a member with the same name, and the name is not name:

    • I has a declaration for n found as follows:
    • Let M be the set of declarations of n of S1..Sk.
      • If Si does not have a member of that name, there is no declaration for Si.
      • If Si is a view type with a member named n, then M contains the declaration of that view member of Si (found recursively using this algorithm.)
      • If Si is a non-view type with a member named n, then M contains the member signature for n in Si.
    • If M contains precisely one element (either because only one of Si..Sk had an n member, or because multiple ones had a member, but they all referred to the same view member declaration, or all had the same member signature), the declaration of n in I is that member. (It can be a view member declaration, and interface member signature, or a conflict).
    • If M contains a conflict, the declaration of n in I is a conflict.
    • If M contains two distinct view member declarations, the declaration of n in I is a conflict.
    • If M contains a view member declaration and an interface signature, the declaration of n in I is a conflict.
    • If M contains no view members or conflicts, only multiple distinct interface member signatures, use the algorithm used for resolving multiple interface member signatures in an interface. If that succeeds and finds a signature which is a valid override of all the signatures of M, then that is the declaration of n in I. If not, the declaration of n in I is a conflict.
  • If k is zero, treat as is Object?, which has only the interface members declared by Object and Null.

Invocation

The type of a view declaration is, statically, like any other nominal type. It’s a subtype of the types defined above.

An expression of the form o.member(args) is type-inferenced as follows:

  • Infer types for o and Let v bet its static type.
  • If v is not a view type, proceed as normal.
  • Otherwise, if the declaration of member in v is:
    • A conflict, a compile-time error occurs.
    • An interface member signature s, continue as an invocation of that interface member signature.
    • A view member declaration m, continue as an invocation of that member’s interface signature.
  • If there is no declaration of member in v, continue as normal (extension methods may apply).

Runtime semantics

The view type is entirely erased at runtime, replaced by its representation type, as if it was a type alias.

Invocation

Member invocations o.member(args) on receivers, o, with a view type as static type, T are evaluated as follows:

  • Evaluate o to a value v.
  • Evaluate args to an argument list.
  • If the declaration of member on T is a view method declaration, invoke that method declaration with this bound to v and the argument list bound to the parameters.
  • If the declaration of member on T is a non-view method signature, invoke .member(args) on v as a normal method invocation.
  • Otherwise proceed as normal (possible extension member invocation).

Casts/type checks

Any runtime subtype check of the form is ViewType/as ViewType/on ViewType performs the same check using the representation type of ViewType.

Discussion

Conflicts

I’m going with lazy conflict errors. A conflict between super-type members only matters if not shadowed, and the member is actually invoked.

The alternative is to make it an early error, requiring the view declaration to declare members which resolve the conflict.

Forwarding

“Forwarding” to instance members happens automatically using is InterfaceType or an abstract int member();. The semantics is not forwarding, but to call the normal instance member directly.

There is no show/hide option to show part of an interface. You have to use abstract members for that. The is InterfaceType inherits every member which isn’t shadowed by another declaration.

You can restrict parameters in views, like

view SafeSet<T>(Set<T> _set) is Set<T> {
  // Shadows `bool Set.contains(Object?)`.
  bool contains(T value) => _set.contains(value);
}

or

view SafeSet<T>(Set<T> _) is Set<T> {
  // "Forwards" to `bool Set.contains(Object?)`.
  bool contains(T value);
}

In the latter case, the restricted type argument ensures that a valid invocation on SafeSet is also a valid invocation on the representation type.

Constructors

Making the primary constructor mandatory and public strongly signals that there is no restriction on which values of the representation type can be typed at the view type.

Adding other constructors, forwarding or factory, is still possible, but it’s not possible to pretend that view EvenInt(int x) can only contain even integers.

You can still create a type and pretend that it is somehow a subset of the view type, and as long as people use it carefully, it will be true. There will just always be a prominent primary constructor showing that it cannot possibly be enforced.

Always making the primary constructor be the unnamed constructor can get in the way of some designs. On the other hand, it’s very predictable and readable. Invoking an unnamed view constructor can be read as being a simple injection in the view type, without having to look up the constructor.

We can allow the default constructor to be named, with syntax view Name.name(args), if we want to. I think we shouldn’t.

@eernstg
Copy link

eernstg commented Nov 3, 2022

This is basically re-introducing the ability to unveil part of the representation type interface. I think we'll need to do that at some point anyway.

I'm not so convinced that the ability to have both superviews and representation-type-supertypes in the same clause (the is clause) is helpful: It doesn't communicate to the reader (or writer) of a view declaration that those two things are completely different (unveiled representation type members have OO dispatch, view members are resolved statically, representation type members will remain the same for dynamic invocations and for any typed invocation, but view members may change their behavior based on the static type, including the change from the current view type to one of the superviews).

So I'd like to have the features, but I'd want to specify superviews and representation-type-unveiling types in two separate clauses.

PS: The type argument lists in the section 'Declaration' should not be X1 .. Xn, each of T and Sj, j in 1 .. k, should have their own type argument lists (or Sj and T should be considered to be potentially parameterized types).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment