feat(ivy): first steps towards JIT compilation (#23833)

This commit adds a mechanism by which the @angular/core annotations
for @Component, @Injectable, and @NgModule become decorators which,
when executed at runtime, trigger just-in-time compilation of their
associated types. The activation of these decorators is configured
by the ivy_switch mechanism, ensuring that the Ivy JIT engine does
not get included in Angular bundles unless specifically requested.

PR Close #23833
This commit is contained in:
Alex Rickabaugh
2018-05-09 08:35:25 -07:00
committed by Matias Niemelä
parent 1b6b936ef4
commit 919f42fea1
37 changed files with 1248 additions and 156 deletions

View File

@ -22,10 +22,10 @@ import {NgModuleCompiler} from '../ng_module_compiler';
import {OutputEmitter} from '../output/abstract_emitter';
import * as o from '../output/output_ast';
import {ParseError} from '../parse_util';
import {compileNgModule as compileIvyModule} from '../render3/r3_module_compiler';
import {compilePipe as compileIvyPipe} from '../render3/r3_pipe_compiler';
import {compileNgModuleFromRender2 as compileR3Module} from '../render3/r3_module_compiler';
import {compilePipe as compileR3Pipe} from '../render3/r3_pipe_compiler';
import {htmlAstToRender3Ast} from '../render3/r3_template_transform';
import {compileComponentFromRender2 as compileIvyComponent, compileDirectiveFromRender2 as compileIvyDirective} from '../render3/view/compiler';
import {compileComponentFromRender2 as compileR3Component, compileDirectiveFromRender2 as compileR3Directive} from '../render3/view/compiler';
import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry';
import {CompiledStylesheet, StyleCompiler} from '../style_compiler';
import {SummaryResolver} from '../summary_resolver';
@ -362,7 +362,7 @@ export class AotCompiler {
private _compileShallowModules(
fileName: string, shallowModules: CompileShallowModuleMetadata[],
context: OutputContext): void {
shallowModules.forEach(module => compileIvyModule(context, module, this._injectableCompiler));
shallowModules.forEach(module => compileR3Module(context, module, this._injectableCompiler));
}
private _compilePartialModule(
@ -413,18 +413,18 @@ export class AotCompiler {
pipes.forEach(pipe => { pipeTypeByName.set(pipe.name, pipe.type.reference); });
compileIvyComponent(
compileR3Component(
context, directiveMetadata, render3Ast, this.reflector, hostBindingParser,
directiveTypeBySel, pipeTypeByName);
} else {
compileIvyDirective(context, directiveMetadata, this.reflector, hostBindingParser);
compileR3Directive(context, directiveMetadata, this.reflector, hostBindingParser);
}
});
pipes.forEach(pipeType => {
const pipeMetadata = this._metadataResolver.getPipeMetadata(pipeType);
if (pipeMetadata) {
compileIvyPipe(context, pipeMetadata, this.reflector);
compileR3Pipe(context, pipeMetadata, this.reflector);
}
});

View File

@ -50,6 +50,7 @@ export {JitCompiler} from './jit/compiler';
export * from './compile_reflector';
export * from './url_resolver';
export * from './resource_loader';
export {ConstantPool} from './constant_pool';
export {DirectiveResolver} from './directive_resolver';
export {PipeResolver} from './pipe_resolver';
export {NgModuleResolver} from './ng_module_resolver';
@ -79,4 +80,10 @@ export {ViewCompiler} from './view_compiler/view_compiler';
export {getParseErrors, isSyntaxError, syntaxError, Version} from './util';
export {SourceMap} from './output/source_map';
export * from './injectable_compiler_2';
export * from './render3/view/api';
export {jitPatchDefinition} from './render3/r3_jit';
export {R3DependencyMetadata, R3FactoryMetadata, R3ResolvedDependencyType} from './render3/r3_factory';
export {compileNgModule, R3NgModuleMetadata} from './render3/r3_module_compiler';
export {makeBindingParser, parseTemplate} from './render3/view/template';
export {compileComponent, compileDirective} from './render3/view/compiler';
// This file only reexports content of the `src` folder. Keep it that way.

View File

@ -65,6 +65,7 @@ export class Identifiers {
static INJECTOR: o.ExternalReference = {name: 'INJECTOR', moduleName: CORE};
static Injector: o.ExternalReference = {name: 'Injector', moduleName: CORE};
static defineInjectable: o.ExternalReference = {name: 'defineInjectable', moduleName: CORE};
static InjectableDef: o.ExternalReference = {name: 'InjectableDef', moduleName: CORE};
static ViewEncapsulation: o.ExternalReference = {
name: 'ViewEncapsulation',
moduleName: CORE,

View File

@ -7,96 +7,107 @@
*/
import {InjectFlags} from './core';
import {Identifiers} from './identifiers';
import * as o from './output/output_ast';
import {Identifiers} from './render3/r3_identifiers';
type MapEntry = {
key: string; quoted: boolean; value: o.Expression;
};
function mapToMapExpression(map: {[key: string]: o.Expression}): o.LiteralMapExpr {
const result = Object.keys(map).map(key => ({key, value: map[key], quoted: false}));
return o.literalMap(result);
}
import {R3DependencyMetadata, compileFactoryFunction} from './render3/r3_factory';
import {mapToMapExpression} from './render3/util';
export interface InjectableDef {
expression: o.Expression;
type: o.Type;
}
export interface IvyInjectableDep {
token: o.Expression;
optional: boolean;
self: boolean;
skipSelf: boolean;
attribute: boolean;
}
export interface IvyInjectableMetadata {
export interface R3InjectableMetadata {
name: string;
type: o.Expression;
providedIn: o.Expression;
useType?: IvyInjectableDep[];
useClass?: o.Expression;
useFactory?: {factory: o.Expression; deps: IvyInjectableDep[];};
useFactory?: o.Expression;
useExisting?: o.Expression;
useValue?: o.Expression;
deps?: R3DependencyMetadata[];
}
export function compileIvyInjectable(meta: IvyInjectableMetadata): InjectableDef {
let ret: o.Expression = o.NULL_EXPR;
if (meta.useType !== undefined) {
const args = meta.useType.map(dep => injectDep(dep));
ret = new o.InstantiateExpr(meta.type, args);
} else if (meta.useClass !== undefined) {
const factory =
new o.ReadPropExpr(new o.ReadPropExpr(meta.useClass, 'ngInjectableDef'), 'factory');
ret = new o.InvokeFunctionExpr(factory, []);
export function compileInjectable(meta: R3InjectableMetadata): InjectableDef {
let factory: o.Expression = o.NULL_EXPR;
function makeFn(ret: o.Expression): o.Expression {
return o.fn([], [new o.ReturnStatement(ret)], undefined, undefined, `${meta.name}_Factory`);
}
if (meta.useClass !== undefined || meta.useFactory !== undefined) {
// First, handle useClass and useFactory together, since both involve a similar call to
// `compileFactoryFunction`. Either dependencies are explicitly specified, in which case
// a factory function call is generated, or they're not specified and the calls are special-
// cased.
if (meta.deps !== undefined) {
// Either call `new meta.useClass(...)` or `meta.useFactory(...)`.
const fnOrClass: o.Expression = meta.useClass || meta.useFactory !;
// useNew: true if meta.useClass, false for meta.useFactory.
const useNew = meta.useClass !== undefined;
factory = compileFactoryFunction({
name: meta.name,
fnOrClass,
useNew,
injectFn: Identifiers.inject,
useOptionalParam: true,
deps: meta.deps,
});
} else if (meta.useClass !== undefined) {
// Special case for useClass where the factory from the class's ngInjectableDef is used.
if (meta.useClass.isEquivalent(meta.type)) {
// For the injectable compiler, useClass represents a foreign type that should be
// instantiated to satisfy construction of the given type. It's not valid to specify
// useClass === type, since the useClass type is expected to already be compiled.
throw new Error(
`useClass is the same as the type, but no deps specified, which is invalid.`);
}
factory =
makeFn(new o.ReadPropExpr(new o.ReadPropExpr(meta.useClass, 'ngInjectableDef'), 'factory')
.callFn([]));
} else if (meta.useFactory !== undefined) {
// Special case for useFactory where no arguments are passed.
factory = meta.useFactory.callFn([]);
} else {
// Can't happen - outer conditional guards against both useClass and useFactory being
// undefined.
throw new Error('Reached unreachable block in injectable compiler.');
}
} else if (meta.useValue !== undefined) {
ret = meta.useValue;
// Note: it's safe to use `meta.useValue` instead of the `USE_VALUE in meta` check used for
// client code because meta.useValue is an Expression which will be defined even if the actual
// value is undefined.
factory = makeFn(meta.useValue);
} else if (meta.useExisting !== undefined) {
ret = o.importExpr(Identifiers.inject).callFn([meta.useExisting]);
} else if (meta.useFactory !== undefined) {
const args = meta.useFactory.deps.map(dep => injectDep(dep));
ret = new o.InvokeFunctionExpr(meta.useFactory.factory, args);
// useExisting is an `inject` call on the existing token.
factory = makeFn(o.importExpr(Identifiers.inject).callFn([meta.useExisting]));
} else {
throw new Error('No instructions for injectable compiler!');
// A strict type is compiled according to useClass semantics, except the dependencies are
// required.
if (meta.deps === undefined) {
throw new Error(`Type compilation of an injectable requires dependencies.`);
}
factory = compileFactoryFunction({
name: meta.name,
fnOrClass: meta.type,
useNew: true,
injectFn: Identifiers.inject,
useOptionalParam: true,
deps: meta.deps,
});
}
const token = meta.type;
const providedIn = meta.providedIn;
const factory =
o.fn([], [new o.ReturnStatement(ret)], undefined, undefined, `${meta.name}_Factory`);
const expression = o.importExpr({
moduleName: '@angular/core',
name: 'defineInjectable',
}).callFn([mapToMapExpression({token, factory, providedIn})]);
const type = new o.ExpressionType(o.importExpr(
{
moduleName: '@angular/core',
name: 'InjectableDef',
},
[new o.ExpressionType(meta.type)]));
const expression = o.importExpr(Identifiers.defineInjectable).callFn([mapToMapExpression(
{token, factory, providedIn})]);
const type = new o.ExpressionType(
o.importExpr(Identifiers.InjectableDef, [new o.ExpressionType(meta.type)]));
return {
expression, type,
};
}
function injectDep(dep: IvyInjectableDep): o.Expression {
const defaultValue = dep.optional ? o.NULL_EXPR : o.literal(undefined);
const flags = o.literal(
InjectFlags.Default | (dep.self && InjectFlags.Self || 0) |
(dep.skipSelf && InjectFlags.SkipSelf || 0));
if (!dep.optional && !dep.skipSelf && !dep.self) {
return o.importExpr(Identifiers.inject).callFn([dep.token]);
} else {
return o.importExpr(Identifiers.inject).callFn([
dep.token,
defaultValue,
flags,
]);
}
}

View File

@ -312,7 +312,7 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex
ctx.print(expr, `)`);
return null;
}
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: EmitterVisitorContext): never {
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: EmitterVisitorContext): any {
throw new Error('Abstract emitter cannot visit WrappedNodeExpr.');
}
visitReadVarExpr(ast: o.ReadVarExpr, ctx: EmitterVisitorContext): any {

View File

@ -70,9 +70,10 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
ctx.println(stmt, `};`);
}
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: EmitterVisitorContext): never {
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: EmitterVisitorContext): any {
throw new Error('Cannot emit a WrappedNodeExpr in Javascript.');
}
visitReadVarExpr(ast: o.ReadVarExpr, ctx: EmitterVisitorContext): string|null {
if (ast.builtin === o.BuiltinVar.This) {
ctx.print(ast, 'self');

View File

@ -68,15 +68,12 @@ export class JitEmitterVisitor extends AbstractJsEmitterVisitor {
}
visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any {
const value = this.reflector.resolveExternalReference(ast.value);
let id = this._evalArgValues.indexOf(value);
if (id === -1) {
id = this._evalArgValues.length;
this._evalArgValues.push(value);
const name = identifierName({reference: value}) || 'val';
this._evalArgNames.push(`jit_${name}_${id}`);
}
ctx.print(ast, this._evalArgNames[id]);
this._emitReferenceToExternal(ast, this.reflector.resolveExternalReference(ast.value), ctx);
return null;
}
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: EmitterVisitorContext): any {
this._emitReferenceToExternal(ast, ast.node, ctx);
return null;
}
@ -100,4 +97,16 @@ export class JitEmitterVisitor extends AbstractJsEmitterVisitor {
}
return super.visitDeclareClassStmt(stmt, ctx);
}
private _emitReferenceToExternal(ast: o.Expression, value: any, ctx: EmitterVisitorContext):
void {
let id = this._evalArgValues.indexOf(value);
if (id === -1) {
id = this._evalArgValues.length;
this._evalArgValues.push(value);
const name = identifierName({reference: value}) || 'val';
this._evalArgNames.push(`jit_${name}_${id}`);
}
ctx.print(ast, this._evalArgNames[id]);
}
}

View File

@ -110,6 +110,8 @@ export class Identifiers {
moduleName: CORE,
};
static defineNgModule: o.ExternalReference = {name: 'ɵdefineNgModule', moduleName: CORE};
static definePipe: o.ExternalReference = {name: 'ɵdefinePipe', moduleName: CORE};
static query: o.ExternalReference = {name: 'ɵQ', moduleName: CORE};

View File

@ -0,0 +1,78 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CompileReflector} from '../compile_reflector';
import {ConstantPool} from '../constant_pool';
import * as o from '../output/output_ast';
import {jitStatements} from '../output/output_jit';
/**
* Implementation of `CompileReflector` which resolves references to @angular/core
* symbols at runtime, according to a consumer-provided mapping.
*
* Only supports `resolveExternalReference`, all other methods throw.
*/
class R3JitReflector implements CompileReflector {
constructor(private context: {[key: string]: any}) {}
resolveExternalReference(ref: o.ExternalReference): any {
// This reflector only handles @angular/core imports.
if (ref.moduleName !== '@angular/core') {
throw new Error(
`Cannot resolve external reference to ${ref.moduleName}, only references to @angular/core are supported.`);
}
if (!this.context.hasOwnProperty(ref.name !)) {
throw new Error(`No value provided for @angular/core symbol '${ref.name!}'.`);
}
return this.context[ref.name !];
}
parameters(typeOrFunc: any): any[][] { throw new Error('Not implemented.'); }
annotations(typeOrFunc: any): any[] { throw new Error('Not implemented.'); }
shallowAnnotations(typeOrFunc: any): any[] { throw new Error('Not implemented.'); }
tryAnnotations(typeOrFunc: any): any[] { throw new Error('Not implemented.'); }
propMetadata(typeOrFunc: any): {[key: string]: any[];} { throw new Error('Not implemented.'); }
hasLifecycleHook(type: any, lcProperty: string): boolean { throw new Error('Not implemented.'); }
guards(typeOrFunc: any): {[key: string]: any;} { throw new Error('Not implemented.'); }
componentModuleUrl(type: any, cmpMetadata: any): string { throw new Error('Not implemented.'); }
}
/**
* JIT compiles an expression and monkey-patches the result of executing the expression onto a given
* type.
*
* @param type the type which will receive the monkey-patched result
* @param field name of the field on the type to monkey-patch
* @param def the definition which will be compiled and executed to get the value to patch
* @param context an object map of @angular/core symbol names to symbols which will be available in
* the context of the compiled expression
* @param constantPool an optional `ConstantPool` which contains constants used in the expression
*/
export function jitPatchDefinition(
type: any, field: string, def: o.Expression, context: {[key: string]: any},
constantPool?: ConstantPool): void {
// The ConstantPool may contain Statements which declare variables used in the final expression.
// Therefore, its statements need to precede the actual JIT operation. The final statement is a
// declaration of $def which is set to the expression being compiled.
const statements: o.Statement[] = [
...(constantPool !== undefined ? constantPool.statements : []),
new o.DeclareVarStmt('$def', def, undefined, [o.StmtModifier.Exported]),
];
// Monkey patch the field on the given type with the result of compilation.
// TODO(alxhub): consider a better source url.
type[field] = jitStatements(
`ng://${type && type.name}/${field}`, statements, new R3JitReflector(context), false)['$def'];
}

View File

@ -14,22 +14,72 @@ import * as o from '../output/output_ast';
import {OutputContext} from '../util';
import {Identifiers as R3} from './r3_identifiers';
import {convertMetaToOutput, mapToMapExpression} from './util';
function convertMetaToOutput(meta: any, ctx: OutputContext): o.Expression {
if (Array.isArray(meta)) {
return o.literalArr(meta.map(entry => convertMetaToOutput(entry, ctx)));
}
if (meta instanceof StaticSymbol) {
return ctx.importExpr(meta);
}
if (meta == null) {
return o.literal(meta);
}
throw new Error(`Internal error: Unsupported or unknown metadata: ${meta}`);
export interface R3NgModuleDef {
expression: o.Expression;
type: o.Type;
additionalStatements: o.Statement[];
}
export function compileNgModule(
/**
* Metadata required by the module compiler to generate a `ngModuleDef` for a type.
*/
export interface R3NgModuleMetadata {
/**
* An expression representing the module type being compiled.
*/
type: o.Expression;
/**
* An array of expressions representing the bootstrap components specified by the module.
*/
bootstrap: o.Expression[];
/**
* An array of expressions representing the directives and pipes declared by the module.
*/
declarations: o.Expression[];
/**
* An array of expressions representing the imports of the module.
*/
imports: o.Expression[];
/**
* An array of expressions representing the exports of the module.
*/
exports: o.Expression[];
/**
* Whether to emit the selector scope values (declarations, imports, exports) inline into the
* module definition, or to generate additional statements which patch them on. Inline emission
* does not allow components to be tree-shaken, but is useful for JIT mode.
*/
emitInline: boolean;
}
/**
* Construct an `R3NgModuleDef` for the given `R3NgModuleMetadata`.
*/
export function compileNgModule(meta: R3NgModuleMetadata): R3NgModuleDef {
const {type: moduleType, bootstrap, declarations, imports, exports} = meta;
const expression = o.importExpr(R3.defineNgModule).callFn([mapToMapExpression({
type: moduleType,
bootstrap: o.literalArr(bootstrap),
declarations: o.literalArr(declarations),
imports: o.literalArr(imports),
exports: o.literalArr(exports),
})]);
// TODO(alxhub): write a proper type reference when AOT compilation of @NgModule is implemented.
const type = new o.ExpressionType(o.NULL_EXPR);
const additionalStatements: o.Statement[] = [];
return {expression, type, additionalStatements};
}
// TODO(alxhub): integrate this with `compileNgModule`. Currently the two are separate operations.
export function compileNgModuleFromRender2(
ctx: OutputContext, ngModule: CompileShallowModuleMetadata,
injectableCompiler: InjectableCompiler): void {
const className = identifierName(ngModule.type) !;
@ -57,4 +107,9 @@ export function compileNgModule(
/* getters */[],
/* constructorMethod */ new o.ClassMethod(null, [], []),
/* methods */[]));
}
}
function accessExportScope(module: o.Expression): o.Expression {
const selectorScope = new o.ReadPropExpr(module, 'ngModuleDef');
return new o.ReadPropExpr(selectorScope, 'exported');
}

View File

@ -0,0 +1,38 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {StaticSymbol} from '../aot/static_symbol';
import * as o from '../output/output_ast';
import {OutputContext} from '../util';
/**
* Convert an object map with `Expression` values into a `LiteralMapExpr`.
*/
export function mapToMapExpression(map: {[key: string]: o.Expression}): o.LiteralMapExpr {
const result = Object.keys(map).map(key => ({key, value: map[key], quoted: false}));
return o.literalMap(result);
}
/**
* Convert metadata into an `Expression` in the given `OutputContext`.
*
* This operation will handle arrays, references to symbols, or literal `null` or `undefined`.
*/
export function convertMetaToOutput(meta: any, ctx: OutputContext): o.Expression {
if (Array.isArray(meta)) {
return o.literalArr(meta.map(entry => convertMetaToOutput(entry, ctx)));
}
if (meta instanceof StaticSymbol) {
return ctx.importExpr(meta);
}
if (meta == null) {
return o.literal(meta);
}
throw new Error(`Internal error: Unsupported or unknown metadata: ${meta}`);
}

View File

@ -12,12 +12,20 @@ import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, c
import {ConstantPool} from '../../constant_pool';
import * as core from '../../core';
import {AST, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, LiteralArray, LiteralMap, LiteralPrimitive, PropertyRead} from '../../expression_parser/ast';
import {Lexer} from '../../expression_parser/lexer';
import {Parser} from '../../expression_parser/parser';
import * as html from '../../ml_parser/ast';
import {HtmlParser} from '../../ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config';
import * as o from '../../output/output_ast';
import {ParseSourceSpan} from '../../parse_util';
import {ParseError, ParseSourceSpan} from '../../parse_util';
import {DomElementSchemaRegistry} from '../../schema/dom_element_schema_registry';
import {CssSelector, SelectorMatcher} from '../../selector';
import {BindingParser} from '../../template_parser/binding_parser';
import {OutputContext, error} from '../../util';
import * as t from '../r3_ast';
import {Identifiers as R3} from '../r3_identifiers';
import {htmlAstToRender3Ast} from '../r3_template_transform';
import {R3QueryMetadata} from './api';
import {CONTEXT_NAME, I18N_ATTR, I18N_ATTR_PREFIX, ID_SEPARATOR, IMPLICIT_REFERENCE, MEANING_SEPARATOR, REFERENCE_PREFIX, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, getQueryPredicate, invalid, mapToExpression, noop, temporaryAllocator, trimTrailingNulls, unsupported} from './util';
@ -713,3 +721,35 @@ function interpolate(args: o.Expression[]): o.Expression {
error(`Invalid interpolation argument length ${args.length}`);
return o.importExpr(R3.interpolationV).callFn([o.literalArr(args)]);
}
/**
* Parse a template into render3 `Node`s and additional metadata, with no other dependencies.
*
* @param template text of the template to parse
* @param templateUrl URL to use for source mapping of the parsed template
*/
export function parseTemplate(template: string, templateUrl: string):
{errors?: ParseError[], nodes: t.Node[], hasNgContent: boolean, ngContentSelectors: string[]} {
const bindingParser = makeBindingParser();
const htmlParser = new HtmlParser();
const parseResult = htmlParser.parse(template, templateUrl);
if (parseResult.errors && parseResult.errors.length > 0) {
return {errors: parseResult.errors, nodes: [], hasNgContent: false, ngContentSelectors: []};
}
const {nodes, hasNgContent, ngContentSelectors, errors} =
htmlAstToRender3Ast(parseResult.rootNodes, bindingParser);
if (errors && errors.length > 0) {
return {errors, nodes: [], hasNgContent: false, ngContentSelectors: []};
}
return {nodes, hasNgContent, ngContentSelectors};
}
/**
* Construct a `BindingParser` with a default configuration.
*/
export function makeBindingParser(): BindingParser {
return new BindingParser(
new Parser(new Lexer()), DEFAULT_INTERPOLATION_CONFIG, new DomElementSchemaRegistry(), [],
[]);
}