| # Interceptors | |
| ## Interceptors | |
| The usual way to add methods to a JavaScript 'class' is to add properties to the | |
| prototype of the constructor function. This works well for user defined | |
| constructor functions. It does not work well for the types provided by the | |
| JavaScript implementation since the prototypes of the builtin types are a shared | |
| resource between all the scripts running in the same page. | |
| **dart2js** maps Dart types to JavaScript builtin types for reasons of | |
| efficiency and compatibility with the browser environment, e.g. a Dart String is | |
| a JavaScript string. In order to avoid unwanted interactions between scripts, | |
| dart2js avoids putting properties on the builtin constructor prototypes. | |
| An interceptor is an object containing the methods and raw type properties of an | |
| instance. An interceptor is a prototype chain without the instance that can be | |
| used in place of the object's actual prototype chain. Having a replacement | |
| prototype chain allows us to make any object, like the builtin numbers and | |
| strings and browser DOM objects, behave like user defined objects. The compiler | |
| can generate interceptor class hierarchies the same as regular class | |
| hierarchies. The difference is that the classes (i.e. JavaScript constructor | |
| functions) are used for their prototypes and never invoked with new. | |
| When the operation `y = x.add(a)` goes via an interceptor the generated | |
| JavaScript code looks like this: | |
| ```js | |
| interceptor = getInterceptor(x); | |
| y = interceptor.add$1(x, a); | |
| ``` | |
| The receiver is passed to the method as an explicit receiver, followed by the | |
| original arguments. The implicit receiver or 'this' argument tracks the type of | |
| the receiver in interceptor space. The call to getInterceptor is a dynamic | |
| dispatch, which in the general case is expensive since it requires a case | |
| analysis of the argument. The code is large too, since every call becomes two | |
| calls in the generated code. Thus is it critical to avoid the full cost | |
| wherever possible. | |
| Interceptors have some properties useful for optimization. The interceptor is a | |
| function of the value, so two calls to getInterceptor with the same argument | |
| will return the same interceptor object. getInterceptor is effect-free. | |
| ## Constant interceptors | |
| If the receiver is known to be a leaf class in the inheritance hierarchy, it can | |
| have only one type. In this case, the interceptor is a constant. In this | |
| example, constant FooClass_methods is the same as FooClass.prototype: | |
| ```js | |
| interceptor = getInterceptor(x); | |
| y = interceptor.add$1(x, a); | |
| --> | |
| interceptor = FooClass_methods; // constant interceptor | |
| y = interceptor.add$1(x, a); | |
| --> | |
| y = FooClass_methods.add$1(x, a); | |
| ``` | |
| In addition to replacing the expensive call with a constant, the code now | |
| contains one fewer reference to x. This means that chained code can be | |
| generated with fewer temporaries and tends to be more readable: | |
| ```dart | |
| r.x.add(a) // Dart source | |
| ``` | |
| --> | |
| ```js | |
| temp_x = r.get$x(); | |
| interceptor = getInterceptor(temp_x); | |
| y = interceptor.add$1(temp_x, a); | |
| --> | |
| temp_x = r.get$x(); | |
| interceptor = FooClass_methods; | |
| y = interceptor.add$1(temp_x, a); | |
| --> | |
| y = FooClass_methods.add$1(r.get$x(), a); | |
| ``` | |
| ## Almost-constant interceptors | |
| If the receiver is FooClass or null and all uses of the interceptor for null | |
| should throw noSuchMethod on null, then we can select between the interceptor or | |
| null: | |
| ```js | |
| (x && FooClass_methods).add$(r.get$x(), a); | |
| ``` | |
| Note that we can't do this for num, bool or String since they have falsy values. | |
| (Implemented in SSA). | |
| ## Specializations of getInterceptor | |
| getInterceptor is a big if-then-else chain. This can be specialized to the types | |
| possible at the call sites, and specialized to prioritize the types where the | |
| uses of the interceptor succeed. Consider: | |
| ```dart | |
| dynamic x = ... | |
| [x.codeUnitAt(0), x.length] | |
| ``` | |
| The getInterceptor specialization for `x` can be tuned for the use pattern. | |
| It should check for `String` first. | |
| The getInterceptor may omit tests for types that are dominated by a failure (we | |
| don't need to check for `Array` since `x.length` will only be reached for a `String`). | |
| (Part 2 is implemented in SSA). | |
| ## Specializations of call sites | |
| ### Dummy receiver | |
| If the receiver cannot be an intercepted class and the selector is not used in a | |
| mixin into an interceptor class, then the method does not use the explicit | |
| receiver (the receiver is available as the 'this' parameter). In this case the ignored | |
| explicit receiver can be replaced by any expression. | |
| ```dart | |
| findList().add(1) | |
| ``` | |
| ```js | |
| --> | |
| temp = findList$0(); | |
| interceptor = getInterceptor(temp); | |
| interceptor.add$1(temp, 1); | |
| --> | |
| temp = findList$0(); | |
| temp.add$1(temp, 1); | |
| --> | |
| temp = findList$0(); | |
| temp.add$1(0, 1); // dummy receiver | |
| --> | |
| findList$0().add$1(0, 1) | |
| ``` | |
| It is unlikely that passing 0 as a dummy value is faster, but the code is smaller. | |
| (Implemented in SSA) | |
| ## GVN optimizations | |
| `getInterceptor` always returns the same value for the same input and has not | |
| side effects and cannot throw, i.e. the operation is pure. | |
| ### CSE | |
| Re-using the result of a dominating call to `getInterceptor` is always beneficial. | |
| ### LICM | |
| Hoisting `getInterceptor` calls out of a loop is beneficial. | |
| Improvement: `getInterceptor` is expensive, so we generally don't want to hoist | |
| calls out of zero trip loops unless the interceptor is always eventually used, | |
| either in the zero-trip case or somewhere else. | |
| ### PRE | |
| Classic partial redundancy elimination (PRE) eliminates computations that are | |
| redundant on some paths. | |
| ```dart | |
| a = getList(...); | |
| if (prefix != null) a.add(prefix); | |
| a.add(data); | |
| ``` | |
| ```js | |
| --> | |
| var a, prefix, interceptor, data; | |
| a = getList(...); | |
| if (prefix != null) { | |
| interceptor = getInterceptor(a); | |
| interceptor.add$1(a, prefix); | |
| } | |
| interceptor = getInterceptor(a); | |
| interceptor.add$1(a, data); | |
| --> | |
| var a, prefix, interceptor, data; | |
| a = getList(...); | |
| if (prefix != null) { | |
| interceptor = getInterceptor(a); | |
| interceptor.add$1(a, prefix); | |
| } else { | |
| interceptor = getInterceptor(a); | |
| } | |
| interceptor.add$1(a, data); | |
| ``` | |
| This is not currently implemented. It would be especially advantageous if the | |
| interceptor is constant on one path. | |
| ### Dynamic PRE | |
| ```js | |
| var a, interceptor; | |
| while (...) { | |
| a = ...; | |
| if (x != null) { | |
| interceptor = getInterceptor(a); | |
| interceptor.add$1(a, x); | |
| } | |
| if (y != null) { | |
| interceptor = getInterceptor(a); | |
| interceptor.add$1(a, y); | |
| } | |
| } | |
| --> | |
| while (...) { | |
| var a = ...; | |
| if (x != null) { | |
| interceptor = getInterceptor(a); | |
| interceptor.add$1(a, x); | |
| } | |
| if (y != null) { | |
| if (!interceptor) interceptor = getInterceptor(a); | |
| interceptor.add$1(a, y); | |
| } | |
| } | |
| ``` | |
| ## One-shot interceptors | |
| One-shot interceptors are code size optimization that removes the need for a | |
| temporary to hold the receiver. | |
| ```js | |
| // r = a.foo() + b; | |
| var t = a.foo$0(); | |
| var r = getInterceptor(t).$add(t, b); | |
| --> | |
| $add = function(x, y) { | |
| return getInterceptor(x).$add(x, y); | |
| } | |
| ... | |
| r = $add(a.foo(), b); | |
| ``` | |
| One-shot interceptors are a special case of outlining. | |
| Single-use interceptors used in type tests (`is` expressions) can sometimes be | |
| replaced with `instanceof`-based type tests | |
| https://github.com/dart-lang/sdk/issues/22016 . | |
| ### Customized one-shot interceptors | |
| One-shot interceptors for common operations like `+` are customized with a quick | |
| dispatch for common input types. | |
| ```js | |
| $add = function(x, y) { | |
| if (typeof x == "number" && typeof y == "number") return x + y; | |
| return getInterceptor(x).$add(x, y); | |
| } | |
| ... | |
| // r = a.foo() + b; | |
| r = $add(a.foo(), b); | |
| ``` | |
| ## Sufficing | |
| (Not implemented) | |
| Sufficing is an extension of constant interceptors. If the receiver is known to | |
| be of a non-leaf class in the hierarchy, it might be possible to use the | |
| constant interceptor for the non-leaf class. Explained further: | |
| https://github.com/dart-lang/sdk/issues/22199 | |
| ## Interprocedural GVN | |
| Shadow field or closed variable | |
| https://github.com/dart-lang/sdk/issues/23686 | |