Adventures in automatically binding Dart to TypeScript.
This was originally posted here: https://chgibb.github.io/closing-over-type-formals-satisying-type-parameters/
Hydro-SDK is a project with one large, ambitious goal. "Become React Native for Flutter".
It aims to do that by:
- Decoupling the API surface of Flutter from the Dart programming language.
- Decoupling the development time experience of Flutter from the Dart programming language.
- Providing first-class support for over-the-air distribution of code.
- Providing an ecosystem of packages from
pub.dev, automatically projected to supported languages and published to other package systems.
Structured Wrapper and Interface generator for Dart (SWID) is a component of Hydro-SDK supporting goals 1 and 4. SWID takes as input a Dart package (like
package:flutter) and produces as output TypeScript files representing the input Dart package's public API and Dart files to allow for glueing TypeScript code written against that API together into a host app that can be run on Hydro-SDK's Common Flutter Runtime (CFR). This process is referred to as "language projection".
Representing Dart constructs in TypeScript has come with many challenges. The most interesting so far has been Dart's
factory constructors. Consider the
Iterable class from
empty factory allows consumers to write code like the following:
Factory constructors are nothing new. While TypeScript doesn't have direct support at the language level as Dart does, the same syntax for consumers can be had by the following class:
Which would enable consumers to call the
empty method from TypeScript as the following:
It would, except the definition of
Iterable.empty in our TypeScript
Iterable class above is actually invalid. The TypeScript compiler tells us that
Static members cannot reference class type parameters.(2302). There is a fairly lengthy discussion on why the TS2302 error exists and why this is the case over on the TypeScript repo here which is out of scope for this post. The jist of the problem is that our
empty method is trying to reference the type parameter
E which is defined on the class.
SWID is structured as a frontend which takes in Dart source code, producing an intermediate representation (IR). This IR is then passed onto the TypeScript backend to produce TypeScript source code. SWID IR closely mirrors an abstract syntax tree (AST) of the input Dart code. The
factory constructor in the original
Iterable class defines no type parameters of its own and makes references to
E defined on the class and therefore so does the IR and the output TypeScript. From a purely code generation perspective, our TypeScript
Iterable class is perfect. From a perspective of semantics preservation from our input language (and simply a correctness perspective), this is obviously far from perfect.
In SWID IR, the declaration of a generic type is said to be a type formal while uses of that generic are said to be type parameters. In SWID parlance, the
Iterable class declares a type formal
empty has a type parameter
In order to stop the TypeScript backend from producing broken code in the face of patterns like
Iterable, a type propagation pass was introduced prior to sending IR off to be turned into TypeScript. The type propagation pass is responsible for rewriting IR in order to satisfy type parameters that are unsatisfied.
In SWID IR, a type formal closure defines all of the type formals that are in scope for a particular IR node. A type parameter is said to be unsatisfied if there is no type formal in it's type formal closure that matches it. The extent to which a particular IR node closes over the type formals of its parent nodes depends on the position of the IR node.
map method in the
Iterable class for example has type parameters
T. The type formal closure of
map consists of
T is declared on
map itself as well as
map is an instance method of
empty method in the
Iterable class has a type parameter
E but its type formal closure contains nothing.
empty is a static method and therefore does not close over the type formals of it's parent
The type propagation pass will discover the unsatisfied type parameter
E in the
empty method, realise that it could be satisfied if the type formal
E declared on its parent
class was also made a type formal of
empty itself and perform the rewriting. This gives us the following
With consumers able to use it like the following:
The end result for consumers is having to shift where they ordinarily write their generics when calling
The type propagation pass itself can be found here. Tests for satisfying type parameters and propagating formals can be found here. The extent of type formal closures can be controlled by the
enum. SWID's TypeScript backend uses
SwidClassTypeFormalClosureKind.kNoCloseOverTypeFormalsInStaticMembers for propagation. Other options exist to preserve Dart's semantics should a future SWID backend need to.
Hopefully this problem and its solution was as fun to read about as it was to discover and solve. Hydro-SDK is an endless fractal of problems like this. Hopefully I'll be able to make "Adventures in automatically binding Dart to TypeScript" a series of posts.