feat(compiler): implement "enableIvy" compiler option (#21427)

The "enableIvy" compiler option is the initial implementation
of the Render3 (or Ivy) code generation. This commit enables
generation generating "Hello, World" (example in the test)
but not much else. It is currenly only useful for internal Ivy
testing as Ivy is in development.

PR Close #21427
This commit is contained in:
Chuck Jazdzewski
2017-11-20 10:21:17 -08:00
committed by Miško Hevery
parent ce8b5877e2
commit 64d16dee02
27 changed files with 1484 additions and 47 deletions

View File

@ -38,6 +38,8 @@ export function isNgDiagnostic(diagnostic: any): diagnostic is Diagnostic {
}
export interface CompilerOptions extends ts.CompilerOptions {
// NOTE: These comments and aio/content/guides/aot-compiler.md should be kept in sync.
// Write statistics about compilation (e.g. total time, ...)
// Note: this is the --diagnostics command line option from TS (which is @internal
// on ts.CompilerOptions interface).
@ -159,6 +161,17 @@ export interface CompilerOptions extends ts.CompilerOptions {
*/
enableSummariesForJit?: boolean;
/**
* Tells the compiler to generate definitions using the Render3 style code generation.
* This option defaults to `false`.
*
* Not all features are supported with this option enabled. It is only supported
* for experimentation and testing of Render3 style code generation.
*
* @experimental
*/
enableIvy?: boolean;
/** @internal */
collectAllErrors?: boolean;
}

View File

@ -6,8 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, CompileIdentifierMetadata, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceFile, ParseSourceSpan, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StaticSymbol, StmtModifier, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassMethod, ClassStmt, CommaExpr, CommentStmt, CompileIdentifierMetadata, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StaticSymbol, StmtModifier, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {error} from './util';
export interface Node { sourceSpan: ParseSourceSpan|null; }
@ -50,6 +51,98 @@ export class TypeScriptNodeEmitter {
}
}
/**
* Update the given source file to include the changes specified in module.
*
* The module parameter is treated as a partial module meaning that the statements are added to
* the module instead of replacing the module. Also, any classes are treated as partial classes
* and the included members are added to the class with the same name instead of a new class
* being created.
*/
export function updateSourceFile(
sourceFile: ts.SourceFile, module: PartialModule,
context: ts.TransformationContext): [ts.SourceFile, Map<ts.Node, Node>] {
const converter = new _NodeEmitterVisitor();
const prefixStatements = module.statements.filter(statement => !(statement instanceof ClassStmt));
const classes =
module.statements.filter(statement => statement instanceof ClassStmt) as ClassStmt[];
const classMap = new Map(
classes.map<[string, ClassStmt]>(classStatement => [classStatement.name, classStatement]));
const classNames = new Set(classes.map(classStatement => classStatement.name));
const prefix =
<ts.Statement[]>prefixStatements.map(statement => statement.visitStatement(converter, null));
// Add static methods to all the classes referenced in module.
let newStatements = sourceFile.statements.map(node => {
if (node.kind == ts.SyntaxKind.ClassDeclaration) {
const classDeclaration = node as ts.ClassDeclaration;
const name = classDeclaration.name;
if (name) {
const classStatement = classMap.get(name.text);
if (classStatement) {
classNames.delete(name.text);
const classMemberHolder =
converter.visitDeclareClassStmt(classStatement) as ts.ClassDeclaration;
const newMethods =
classMemberHolder.members.filter(member => member.kind !== ts.SyntaxKind.Constructor);
const newMembers = [...classDeclaration.members, ...newMethods];
return ts.updateClassDeclaration(
classDeclaration,
/* decorators */ classDeclaration.decorators,
/* modifiers */ classDeclaration.modifiers,
/* name */ classDeclaration.name,
/* typeParameters */ classDeclaration.typeParameters,
/* heritageClauses */ classDeclaration.heritageClauses || [],
/* members */ newMembers);
}
}
}
return node;
});
// Validate that all the classes have been generated
classNames.size == 0 ||
error(
`${classNames.size == 1 ? 'Class' : 'Classes'} "${Array.from(classNames.keys()).join(', ')}" not generated`);
// Add imports to the module required by the new methods
const imports = converter.getImports();
if (imports && imports.length) {
// Find where the new imports should go
const index = firstAfter(
newStatements, statement => statement.kind === ts.SyntaxKind.ImportDeclaration ||
statement.kind === ts.SyntaxKind.ImportEqualsDeclaration);
newStatements =
[...newStatements.slice(0, index), ...imports, ...prefix, ...newStatements.slice(index)];
} else {
newStatements = [...prefix, ...newStatements];
}
converter.updateSourceMap(newStatements);
const newSourceFile = ts.updateSourceFileNode(sourceFile, newStatements);
return [newSourceFile, converter.getNodeMap()];
}
// Return the index after the first value in `a` that doesn't match the predicate after a value that
// does or 0 if no values match.
function firstAfter<T>(a: T[], predicate: (value: T) => boolean) {
let index = 0;
const len = a.length;
for (; index < len; index++) {
const value = a[index];
if (predicate(value)) break;
}
if (index >= len) return 0;
for (; index < len; index++) {
const value = a[index];
if (!predicate(value)) break;
}
return index;
}
// A recorded node is a subtype of the node that is marked as being recoreded. This is used
// to ensure that NodeEmitterVisitor.record has been called on all nodes returned by the
// NodeEmitterVisitor
@ -232,9 +325,12 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
const modifiers = this.getModifiers(stmt);
const fields = stmt.fields.map(
field => ts.createProperty(
/* decorators */ undefined, /* modifiers */ undefined, field.name,
/* decorators */ undefined, /* modifiers */ translateModifiers(field.modifiers),
field.name,
/* questionToken */ undefined,
/* type */ undefined, ts.createNull()));
/* type */ undefined,
field.initializer == null ? ts.createNull() :
field.initializer.visitExpression(this, null)));
const getters = stmt.getters.map(
getter => ts.createGetAccessor(
/* decorators */ undefined, /* modifiers */ undefined, getter.name, /* parameters */[],
@ -256,7 +352,8 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
const methods = stmt.methods.filter(method => method.name)
.map(
method => ts.createMethod(
/* decorators */ undefined, /* modifiers */ undefined,
/* decorators */ undefined,
/* modifiers */ translateModifiers(method.modifiers),
/* astriskToken */ undefined, method.name !/* guarded by filter */,
/* questionToken */ undefined, /* typeParameters */ undefined,
method.params.map(
@ -547,3 +644,21 @@ function getMethodName(methodRef: {name: string | null; builtin: BuiltinMethod |
}
throw new Error('Unexpected method reference form');
}
function modifierFromModifier(modifier: StmtModifier): ts.Modifier {
switch (modifier) {
case StmtModifier.Exported:
return ts.createToken(ts.SyntaxKind.ExportKeyword);
case StmtModifier.Final:
return ts.createToken(ts.SyntaxKind.ConstKeyword);
case StmtModifier.Private:
return ts.createToken(ts.SyntaxKind.PrivateKeyword);
case StmtModifier.Static:
return ts.createToken(ts.SyntaxKind.StaticKeyword);
}
return error(`unknown statement modifier`);
}
function translateModifiers(modifiers: StmtModifier[] | null): ts.Modifier[]|undefined {
return modifiers == null ? undefined : modifiers !.map(modifierFromModifier);
}

View File

@ -1,3 +1,4 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
@ -6,7 +7,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
@ -18,7 +19,8 @@ import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, D
import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host';
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused} from './util';
import {getAngularClassTransformerFactory} from './r3_transform';
import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util';
/**
@ -65,9 +67,10 @@ class AngularCompilerProgram implements Program {
private host: CompilerHost, oldProgram?: Program) {
this.rootNames = [...rootNames];
const [major, minor] = ts.version.split('.');
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 4)) {
throw new Error('The Angular Compiler requires TypeScript >= 2.4.');
}
Number(major) > 2 || (Number(major) === 2 && Number(minor) >= 4) ||
userError('The Angular Compiler requires TypeScript >= 2.4.');
this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined;
if (oldProgram) {
this.oldProgramLibrarySummaries = oldProgram.getLibrarySummaries();
@ -79,8 +82,6 @@ class AngularCompilerProgram implements Program {
const {host: bundleHost, indexName, errors} =
createBundleIndexHost(options, this.rootNames, host);
if (errors) {
// TODO(tbosch): once we move MetadataBundler from tsc_wrapped into compiler_cli,
// directly create ng.Diagnostic instead of using ts.Diagnostic here.
this._optionsDiagnostics.push(...errors.map(e => ({
category: e.category,
messageText: e.messageText as string,
@ -196,7 +197,55 @@ class AngularCompilerProgram implements Program {
return this.compiler.listLazyRoutes(route, route ? undefined : this.analyzedModules);
}
emit(
emit(parameters: {
emitFlags?: EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: CustomTransformers,
emitCallback?: TsEmitCallback
} = {}): ts.EmitResult {
return this.options.enableIvy === true ? this._emitRender3(parameters) :
this._emitRender2(parameters);
}
private _emitRender3(
{emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
emitCallback = defaultEmitCallback}: {
emitFlags?: EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: CustomTransformers,
emitCallback?: TsEmitCallback
} = {}): ts.EmitResult {
const emitStart = Date.now();
if ((emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Codegen)) ===
0) {
return {emitSkipped: true, diagnostics: [], emittedFiles: []};
}
const modules = this.compiler.emitAllPartialModules(this.analyzedModules);
const writeTsFile: ts.WriteFileCallback =
(outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => {
const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null;
let genFile: GeneratedFile|undefined;
this.writeFile(outFileName, outData, writeByteOrderMark, onError, undefined, sourceFiles);
};
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
const tsCustomTansformers = this.calculateTransforms(
/* genFiles */ undefined, /* partialModules */ modules, customTransformers);
const emitResult = emitCallback({
program: this.tsProgram,
host: this.host,
options: this.options,
writeFile: writeTsFile, emitOnlyDtsFiles,
customTransformers: tsCustomTansformers
});
return emitResult;
}
private _emitRender2(
{emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
emitCallback = defaultEmitCallback}: {
emitFlags?: EmitFlags,
@ -244,7 +293,8 @@ class AngularCompilerProgram implements Program {
}
this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles);
};
const tsCustomTansformers = this.calculateTransforms(genFileByFileName, customTransformers);
const tsCustomTansformers = this.calculateTransforms(
genFileByFileName, /* partialModules */ undefined, customTransformers);
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
// Restore the original references before we emit so TypeScript doesn't emit
// a reference to the .d.ts file.
@ -400,13 +450,18 @@ class AngularCompilerProgram implements Program {
}
private calculateTransforms(
genFiles: Map<string, GeneratedFile>,
genFiles: Map<string, GeneratedFile>|undefined, partialModules: PartialModule[]|undefined,
customTransformers?: CustomTransformers): ts.CustomTransformers {
const beforeTs: ts.TransformerFactory<ts.SourceFile>[] = [];
if (!this.options.disableExpressionLowering) {
beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache, this.tsProgram));
}
beforeTs.push(getAngularEmitterTransformFactory(genFiles, this.getTsProgram()));
if (genFiles) {
beforeTs.push(getAngularEmitterTransformFactory(genFiles, this.getTsProgram()));
}
if (partialModules) {
beforeTs.push(getAngularClassTransformerFactory(partialModules));
}
if (customTransformers && customTransformers.beforeTs) {
beforeTs.push(...customTransformers.beforeTs);
}

View File

@ -0,0 +1,36 @@
/**
* @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 {PartialModule, Statement, StaticSymbol} from '@angular/compiler';
import * as ts from 'typescript';
import {updateSourceFile} from './node_emitter';
export type Transformer = (sourceFile: ts.SourceFile) => ts.SourceFile;
export type TransformerFactory = (context: ts.TransformationContext) => Transformer;
/**
* Returns a transformer that adds the requested static methods specified by modules.
*/
export function getAngularClassTransformerFactory(modules: PartialModule[]): TransformerFactory {
if (modules.length === 0) {
// If no modules are specified, just return an identity transform.
return () => sf => sf;
}
const moduleMap = new Map(modules.map<[string, PartialModule]>(m => [m.fileName, m]));
return function(context: ts.TransformationContext) {
return function(sourceFile: ts.SourceFile): ts.SourceFile {
const module = moduleMap.get(sourceFile.fileName);
if (module) {
const [newSourceFile] = updateSourceFile(sourceFile, module, context);
return newSourceFile;
}
return sourceFile;
};
};
}

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {syntaxError} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
@ -21,6 +22,14 @@ export function tsStructureIsReused(program: ts.Program): StructureIsReused {
return (program as any).structureIsReused;
}
export function error(msg: string): never {
throw new Error(`Internal error: ${msg}`);
}
export function userError(msg: string): never {
throw syntaxError(msg);
}
export function createMessageDiagnostic(messageText: string): ts.Diagnostic&Diagnostic {
return {
file: undefined,