Skip to content
This repository has been archived by the owner on Dec 19, 2017. It is now read-only.

Declarative Initializers

Kathy Walrath edited this page Sep 15, 2015 · 3 revisions

This document presents a design for a package that allows users to express initializers in Dart. This design is motivated by use cases in supporting custom element APIs. The proposal in this document doesn't require any changes to the Dart language semantics, but this could motivate a design change in the language in the future.

This document is part of a larger design to restructure how we consume custom elements in Dart.

Motivation

We'd like to support using custom elements directly in Dart applications, without having a duplicate declaration and initialization for each custom element. JavaScript avoids this problem because JavaScript can execute code in script tags that are imported via HTML-imports.

In Dart, we've used mirrors and code transformers to facilitate the registration process. So users only have to invoke a single function to perform all the custom elements initializations.

None of the features needed to support this are specific to custom elements, so we'd like to generalize the techniques and provide a generic support for initialization. We decided to split the design in two layers, first a layer that adds static initialization in Dart proper (this document), and second a layer that leverages this design and applies it in the context of HTML documents with Dart script tags (see design doc).

Design

We'll create a package called initialize. The package declares a Initializer interface, which succinctly declares the logic of an initializer. The package also exposes a top-level function run that will execute all initializers. Any annotation implementing the Initializer interface and placed in front of directives or top-level elements (like classes and top-level functions), will be invoked during the initialization task. Initializers are executed by visiting the Dart import graph in a post-order fashion.

Declaration API

We introduce an interface to declare an initializer:

abstract class Initializer<T> {
  dynamic initialize(T target);
}

An initializer is an annotation that implements this interface and that is attached to a top-level declaration, such as a class or a top-level method, or directives. The initialization task will call initialize and pass in as an argument the annotated top-level element in the case of classes and top-level methods, the Symbol of the library name for library directives, or null otherwise. The return value of initialize is only used to support asynchronous initializers (more below).

Example 1: @CustomTag, an annotation used to declare a custom element and register it in the global browser context, can be implemented as follows:

import 'dart:html';
import 'package:initialize/initialize.dart';

class CustomTag implements Initializer<Type> {
  final String name;
  const CustomTag(this.name);

  @override
  initialize(Type t) {
    document.registerElement(name, t);
  }
}

when a user writes:

@CustomTag('x-foo')
class XFoo extends HtmlElement { … }

the static initialization package will run the following statement:

const CustomTag('x-foo').initialize(XFoo);

which basically registers x-foo in the document context.

Example 2: @initMethod, an annotation that directly invokes the function it is attached to (only if it takes zero arguments).

import 'dart:html':
import 'package:initialize/initialize.dart';

const initMethod = const InitMethod();
typedef dynamic _ZeroArg();
class InitMethod implements Initializer<_ZeroArg> {
  const InitMethod();

  @override
  initialize(_ZeroArg f) {
    if (f is _ZeroArg) f();
  }
}

This lets users to simply write:

@initMethod
myExtraInitializations() {
}

to include some additional static initializations in their code.

Initialization API

Because Dart has no native support for static initializers, users need to include an extra call in their main function to ensure all static initializers are executed:

import 'package:initialize/initialize.dart' as init;

main() {
  init.run();
}

If we were to extend the language, the Initializer interface could be added to dart:core and this invocation to init.run() could be done automatically by the Dart vm/dart2js.

Execution semantics

Each initializer is invoked only once. This keeps semantics simple and makes it easier to compose different libraries that use the static initialization package. This also means that, if all code is loaded upfront, init.run has no effect if it gets invoked a second time. However, if code is loaded in pieces (for example, with deferred imports or when supporting HTML imports), a subsequent run will only execute the newly added initializers.

Initializers are executed bottom-up in a well-defined deterministic order. Libraries are initialized in the post-order derived from the imports in the Dart program. Within a file, the order of the imports is based on the order in the text source. The order of the initializers within a library is based on the order in the source, except that if both a subclass and a superclass are annotated with initializers, we will run the initializer on the superclass first.

Starting library

By default init.run runs all initializers starting from the root library in the program. This function could take an argument to specify what library to start from:

init.run(from: "package:my_app/my_library.dart");

Initializers filters

By default all static initializers are invoked by init.run, but extra arguments can be provided to filter and control which initializers are to be run. This might be more relevant as more tools start using static initializers. For example, to only initialize custom elements, a user could write:

main() => init.run(filter: const [CustomTag]);

Note: this feature is currently needed in the context of Dart-to-HTML imports to guarantee consistency in the initialization order.

Asynchronous initializers

Asynchronous initializers can be supported by returning a Future from the initialize method of the Initializer. The static initialization package can chain futures to ensure that initializers are still executed in the same order. The init.run function would then return a future that completes when all initializers have been executed.

Note: this feature is required if Dart-to-HTML imports load additional Dart code, which won't be supported in its initial implementation.

Pubspec API

To support running code in dart2js, we plan to have several transformers to implement the static initialization semantics with code generation. This requires that users declare transformers in their pubspec. For example:

transformers:
- initialize

Support for initialization from HTML

Several features discussed above were specifically chosen to allow building static initializers that work well in the context of HTML documents and Dart-to-HTML imports. We discuss how this is implemented in a separate document, but we wanted to highlight here some of the motivation and design choices we made.

There is a circular dependency between static initializers and Dart-to-HTML imports: we want to initialize all code that is reachable, even via Dart-to-HTML imports, but at the same time the implementation of Dart-to-HTML imports can be simplified if we can use static initializers internally. Part of the motivation of our multi-tier design was to to break this circularity.

Four features in the static initialization design were highly motivated by this split:

  • Annotating directives: Dart-to-HTML imports can be expressed as annotations on library directives, this helps make it easier to ensure that Dart-To-HTML imports are processed before any other initializers within the library.

  • Asynchronous initializers: Dart-to-HTML imports might dynamically inject HTML imports, and hence additional Dart code might be loaded asynchronously. Support for asynchronous initializers allows us to ensure that the initialization order including Dart-to-HTML imports is deterministic and preserves the post-order semantics we want.

  • Allow to run init.run multiple times: as discussed earlier we allow running initializers for code that is loaded in pieces. This is important to be able to compose initialization of multiple Dart script tags that occur in an HTML file. It is also useful for the implementation of Dart-to-HTML to execute initializers in code that is loaded later in time.

  • Filters: to allow us to run Dart-to-HTML initializers separate from all other initializers.

Implementation Details

This section discusses some of the implementation techniques we intend to use to implement the static initialization package. Like many packages that rely on reflective APIs, we need two implementations: a development version that works without any code transformations, and another that uses transformations that can then be compiled with dart2js.

It's important to note that while part of the logic we need exists today in the polymer package, there are several clear differences to keep in mind:

  • Polymer has support for initializing scripts discovered through HTML imports only, it doesn't support crawling Dart imports as well.

  • Polymer has support for @CustomTag and @initMethod, but not for arbitrary annotations that implement the APIs specified earlier.

Mirror-based implementation

The init.run function will:

  • crawl Dart imports using the dart:mirror API to create an import tree (cycles removed)

  • visit each library in post-order, and within a library:

    • discover all annotated elements

      • report an error for private elements, since the transformer-based implementation will not support it upfront
    • invoke the static initializers in this order:

      • first run initializers on directives
      • then run initializers on methods and classes
      • run initializers attached on superclasses before the initializer of a subclass
      • otherwise preserve the order in which the code is written (we need to validate this, but dart:mirrors might not guarantee this).
  • record which initializers have been executed

    • not all initializers in a library are run at once (e.g. subclasses and filters might not let us do so), so we need to track at a fine-grain level which initializers have been executed.

We plan to minimize dependencies on mirrors in a similar way as in the smoke package: creating a small surface location where a transformer can simply cut all dependencies with the dart:mirrors library.

Transformer-based implementation

The static initialization package will have a transformer that will use the analyzer package to parse and crawl the Dart program and generate a new entrypoint that bootstraps the initialization of the application. This can be implemented in a similar fashion as we do in polymer.dart (a subset of script_compactor.dart).