fix(ivy): convert context code into a tree-shakable instruction (#24943)

PR Close #24943
This commit is contained in:
Kara Erickson
2018-07-25 17:25:22 -07:00
committed by Igor Minar
parent fe14f180a6
commit 2ef777b0b2
24 changed files with 1058 additions and 519 deletions

View File

@ -338,6 +338,8 @@ export class WriteVarExpr extends Expression {
toDeclStmt(type?: Type|null, modifiers?: StmtModifier[]|null): DeclareVarStmt {
return new DeclareVarStmt(this.name, this.value, type, modifiers, this.sourceSpan);
}
toConstDecl(): DeclareVarStmt { return this.toDeclStmt(INFERRED_TYPE, [StmtModifier.Final]); }
}

View File

@ -45,6 +45,8 @@ export class Identifiers {
static containerCreate: o.ExternalReference = {name: 'ɵC', moduleName: CORE};
static nextContext: o.ExternalReference = {name: 'ɵx', moduleName: CORE};
static text: o.ExternalReference = {name: 'ɵT', moduleName: CORE};
static textBinding: o.ExternalReference = {name: 'ɵt', moduleName: CORE};

View File

@ -149,7 +149,7 @@ export function compileComponentFromMetadata(
const template = meta.template;
const templateFunctionExpression =
new TemplateDefinitionBuilder(
constantPool, CONTEXT_NAME, BindingScope.ROOT_SCOPE, 0, templateTypeName, templateName,
constantPool, BindingScope.ROOT_SCOPE, 0, templateTypeName, templateName,
meta.viewQueries, directiveMatcher, directivesUsed, meta.pipes, pipesUsed,
R3.namespaceHTML)
.buildTemplateFunction(

View File

@ -57,12 +57,33 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
private _bindingContext = 0;
private _prefixCode: o.Statement[] = [];
private _creationCode: o.Statement[] = [];
private _variableCode: o.Statement[] = [];
private _bindingCode: (() => o.Statement)[] = [];
private _nestedTemplates: (() => void)[] = [];
/**
* List of callbacks to generate update mode instructions. We store them here as we process
* the template so bindings are resolved only once all nodes have been visited. This ensures
* all local refs and context variables are available for matching.
*/
private _updateCodeFns: (() => o.Statement)[] = [];
/** Temporary variable declarations generated from visiting pipes, literals, etc. */
private _tempVariables: o.Statement[] = [];
/**
* List of callbacks to build nested templates. Nested templates must not be visited until
* after the parent template has finished visiting all of its nodes. This ensures that all
* local ref bindings in nested templates are able to find local ref values if the refs
* are defined after the template declaration.
*/
private _nestedTemplateFns: (() => void)[] = [];
/**
* This scope contains local variables declared in the update mode block of the template.
* (e.g. refs and context vars in bindings)
*/
private _updateScope: BindingScope;
/**
* This scope contains local variables declared in the creation mode block of the template
* (e.g. refs and context vars in listeners)
*/
private _creationScope: BindingScope;
private _valueConverter: ValueConverter;
private _unsupported = unsupported;
private _bindingScope: BindingScope;
// Whether we are inside a translatable element (`<p i18n>... somewhere here ... </p>)
private _inI18nSection: boolean = false;
@ -74,20 +95,19 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
private _pureFunctionSlots = 0;
constructor(
private constantPool: ConstantPool, private contextParameter: string,
parentBindingScope: BindingScope, private level = 0, private contextName: string|null,
private templateName: string|null, private viewQueries: R3QueryMetadata[],
private directiveMatcher: SelectorMatcher|null, private directives: Set<o.Expression>,
private pipeTypeByName: Map<string, o.Expression>, private pipes: Set<o.Expression>,
private _namespace: o.ExternalReference) {
private constantPool: ConstantPool, parentBindingScope: BindingScope, private level = 0,
private contextName: string|null, private templateName: string|null,
private viewQueries: R3QueryMetadata[], private directiveMatcher: SelectorMatcher|null,
private directives: Set<o.Expression>, private pipeTypeByName: Map<string, o.Expression>,
private pipes: Set<o.Expression>, private _namespace: o.ExternalReference) {
// view queries can take up space in data and allocation happens earlier (in the "viewQuery"
// function)
this._dataIndex = viewQueries.length;
this._bindingScope =
parentBindingScope.nestedScope(level, (lhsVar: o.ReadVarExpr, rhsExpr: o.Expression) => {
this._variableCode.push(
lhsVar.set(rhsExpr).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
});
// TODO(kara): generate restore instruction in listener to replace creation scope
this._creationScope = parentBindingScope.nestedScope(level);
this._updateScope = parentBindingScope.nestedScope(level);
this._valueConverter = new ValueConverter(
constantPool, () => this.allocateDataSlot(),
(numSlots: number): number => this._pureFunctionSlots += numSlots,
@ -96,12 +116,33 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (pipeType) {
this.pipes.add(pipeType);
}
this._bindingScope.set(localName, value);
this._updateScope.set(this.level, localName, value);
this._creationCode.push(
o.importExpr(R3.pipe).callFn([o.literal(slot), o.literal(name)]).toStmt());
});
}
registerContextVariables(variable: t.Variable, retrievalScope: BindingScope) {
const scopedName = retrievalScope.freshReferenceName();
const retrievalLevel = this.level;
const lhs = o.variable(variable.name + scopedName);
retrievalScope.set(
retrievalLevel, variable.name, lhs, DeclarationPriority.CONTEXT,
(scope: BindingScope, relativeLevel: number) => {
let rhs: o.Expression;
if (scope.bindingLevel === retrievalLevel) {
// e.g. ctx
rhs = o.variable(CONTEXT_NAME);
} else {
const sharedCtxVar = scope.getSharedContextName(retrievalLevel);
// e.g. ctx_r0 OR x(2);
rhs = sharedCtxVar ? sharedCtxVar : generateNextContextExpr(relativeLevel);
}
// e.g. const $item$ = x(2).$implicit;
return [lhs.set(rhs.prop(variable.value || IMPLICIT_REFERENCE)).toConstDecl()];
});
}
buildTemplateFunction(
nodes: t.Node[], variables: t.Variable[], hasNgContent: boolean = false,
ngContentSelectors: string[] = []): o.FunctionExpr {
@ -111,12 +152,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Create variable bindings
for (const variable of variables) {
const variableName = variable.name;
const expression =
o.variable(this.contextParameter).prop(variable.value || IMPLICIT_REFERENCE);
const scopedName = this._bindingScope.freshReferenceName();
// Add the reference to the local scope.
this._bindingScope.set(variableName, o.variable(variableName + scopedName), expression);
this.registerContextVariables(variable, this._creationScope);
this.registerContextVariables(variable, this._updateScope);
}
// Output a `ProjectionDef` instruction when some `<ng-content>` are present
@ -135,42 +173,50 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this.creationInstruction(null, R3.projectionDef, ...parameters);
}
// This is the initial pass through the nodes of this template. In this pass, we
// generate all creation mode instructions & queue all update mode instructions for
// generation in the second pass. It's necessary to separate the passes to ensure
// local refs are defined before resolving bindings.
t.visitAll(this, nodes);
const creationCode = this._creationCode.length > 0 ?
[renderFlagCheckIfStmt(core.RenderFlags.Create, this._creationCode)] :
[];
const updateCode = this._bindingCode.length > 0 ?
[renderFlagCheckIfStmt(core.RenderFlags.Update, this._variableCode.concat(
this._variableCode.concat(this._bindingCode.map((fn: () => o.Statement) => fn()))))] :
[];
// Generate all the update mode instructions as the second pass (e.g. resolve bindings)
const updateStatements = this._updateCodeFns.map((fn: () => o.Statement) => fn());
// To count slots for the reserveSlots() instruction, all bindings must have been visited.
if (this._pureFunctionSlots > 0) {
this.creationInstruction(null, R3.reserveSlots, o.literal(this._pureFunctionSlots));
}
const creationCode = this._creationCode.length > 0 ?
[renderFlagCheckIfStmt(
core.RenderFlags.Create,
this._creationScope.variableDeclarations().concat(this._creationCode))] :
[];
// This must occur after binding resolution so we can generate context instructions that
// build on each other. e.g. const row = x().$implicit; const table = x().$implicit();
const updateVariables = this._updateScope.variableDeclarations().concat(this._tempVariables);
const updateCode = this._updateCodeFns.length > 0 ?
[renderFlagCheckIfStmt(core.RenderFlags.Update, updateVariables.concat(updateStatements))] :
[];
// Generate maps of placeholder name to node indexes
// TODO(vicb): This is a WIP, not fully supported yet
for (const phToNodeIdx of this._phToNodeIdxes) {
if (Object.keys(phToNodeIdx).length > 0) {
const scopedName = this._bindingScope.freshReferenceName();
const phMap = o.variable(scopedName)
.set(mapToExpression(phToNodeIdx, true))
.toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
const scopedName = this._updateScope.freshReferenceName();
const phMap = o.variable(scopedName).set(mapToExpression(phToNodeIdx, true)).toConstDecl();
this._prefixCode.push(phMap);
}
}
this._nestedTemplates.forEach(buildTemplateFn => buildTemplateFn());
this._nestedTemplateFns.forEach(buildTemplateFn => buildTemplateFn());
return o.fn(
// i.e. (rf: RenderFlags, ctx0: any, ctx: any)
[
new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), ...this.getNestedContexts(),
new o.FnParam(CONTEXT_NAME, null)
],
// i.e. (rf: RenderFlags, ctx: any)
[new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, null)],
[
// Temporary variable declarations for query refresh (i.e. let _t: any;)
...this._prefixCode,
@ -183,7 +229,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}
// LocalResolver
getLocal(name: string): o.Expression|null { return this._bindingScope.get(name); }
getLocal(name: string): o.Expression|null { return this._updateScope.get(name); }
visitContent(ngContent: t.Content) {
const slot = this.allocateDataSlot();
@ -225,18 +271,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this.creationInstruction(element.sourceSpan, nsInstruction);
}
getNestedContexts(): o.FnParam[] {
const nestedContexts = [];
let nestingLevel = this.level - 1;
while (nestingLevel >= 0) {
nestedContexts.push(new o.FnParam(`ctx${nestingLevel}`, null));
nestingLevel--;
}
return nestedContexts;
}
visitElement(element: t.Element) {
const elementIndex = this.allocateDataSlot();
const wasInI18nSection = this._inI18nSection;
@ -426,17 +460,19 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const references = flatten(element.references.map(reference => {
const slot = this.allocateDataSlot();
// Generate the update temporary.
const variableName = this._bindingScope.freshReferenceName();
// When the ref's binding is processed, we'll either generate a load() or a reference()
// instruction depending on the nesting level of the binding relative to the reference def.
const refLevel = this.level;
this._bindingScope.set(
reference.name, o.variable(variableName), undefined, (bindingLevel: number) => {
return bindingLevel === refLevel ?
o.importExpr(R3.load).callFn([o.literal(slot)]) :
o.importExpr(R3.reference).callFn([
o.literal(bindingLevel - refLevel), o.literal(slot)
]);
const variableName = this._updateScope.freshReferenceName();
const retrievalLevel = this.level;
const lhs = o.variable(variableName);
this._updateScope.set(
retrievalLevel, reference.name, lhs, DeclarationPriority.DEFAULT,
(scope: BindingScope, relativeLevel: number) => {
// e.g. x(2);
const nextContextStmt =
relativeLevel > 0 ? [generateNextContextExpr(relativeLevel).toStmt()] : [];
// e.g. const $foo$ = r(1);
const refExpr = lhs.set(o.importExpr(R3.reference).callFn([o.literal(slot)]));
return nextContextStmt.concat(refExpr.toConstDecl());
});
return [reference.name, reference.value];
}));
@ -515,17 +551,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const elName = sanitizeIdentifier(element.name);
const evName = sanitizeIdentifier(outputAst.name);
const functionName = `${this.templateName}_${elName}_${evName}_listener`;
const localVars: o.Statement[] = [];
const bindingScope = this._bindingScope.nestedScope(
this.level + 1, (lhsVar: o.ReadVarExpr, rhsExpression: o.Expression) => {
localVars.push(
lhsVar.set(rhsExpression).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
});
const bindingExpr = convertActionBinding(
bindingScope, implicit, outputAst.handler, 'b',
this._creationScope, implicit, outputAst.handler, 'b',
() => error('Unexpected interpolation'));
const handler = o.fn(
[new o.FnParam('$event', o.DYNAMIC_TYPE)], [...localVars, ...bindingExpr.render3Stmts],
[new o.FnParam('$event', o.DYNAMIC_TYPE)], [...bindingExpr.render3Stmts],
o.INFERRED_TYPE, null, functionName);
this.creationInstruction(
outputAst.sourceSpan, R3.listener, o.literal(outputAst.name), handler);
@ -668,8 +698,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const templateName =
contextName ? `${contextName}_Template_${templateIndex}` : `Template_${templateIndex}`;
const templateContext = `ctx${this.level}`;
const parameters: o.Expression[] = [
o.literal(templateIndex),
o.variable(templateName),
@ -713,15 +741,14 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Create the template function
const templateVisitor = new TemplateDefinitionBuilder(
this.constantPool, templateContext, this._bindingScope, this.level + 1, contextName,
templateName, [], this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes,
this._namespace);
this.constantPool, this._updateScope, this.level + 1, contextName, templateName, [],
this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes, this._namespace);
// Nested templates must not be visited until after their parent templates have completed
// processing, so they are queued here until after the initial pass. Otherwise, we wouldn't
// be able to support bindings in nested templates to local refs that occur after the
// template definition. e.g. <div *ngIf="showing"> {{ foo }} </div> <div #foo></div>
this._nestedTemplates.push(() => {
this._nestedTemplateFns.push(() => {
const templateFunctionExpr =
templateVisitor.buildTemplateFunction(template.children, template.variables);
this.constantPool.statements.push(templateFunctionExpr.toDeclStmt(templateName, null));
@ -790,7 +817,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// bindings. e.g. {{ foo }} <div #foo></div>
private updateInstruction(
span: ParseSourceSpan|null, reference: o.ExternalReference, paramsFn: () => o.Expression[]) {
this._bindingCode.push(() => { return this.instruction(span, reference, paramsFn()); });
this._updateCodeFns.push(() => { return this.instruction(span, reference, paramsFn()); });
}
private convertPropertyBinding(implicit: o.Expression, value: AST, skipBindFn?: boolean):
@ -800,7 +827,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const convertedPropertyBinding = convertPropertyBinding(
this, implicit, value, this.bindingContext(), BindingForm.TrySimple, interpolationFn);
this._variableCode.push(...convertedPropertyBinding.stmts);
this._tempVariables.push(...convertedPropertyBinding.stmts);
const valExpr = convertedPropertyBinding.currValExpr;
return value instanceof Interpolation || skipBindFn ? valExpr :
@ -888,6 +915,12 @@ function pureFunctionCallInfo(args: o.Expression[]) {
};
}
// e.g. x(2);
function generateNextContextExpr(relativeLevelDiff: number): o.Expression {
return o.importExpr(R3.nextContext)
.callFn(relativeLevelDiff > 1 ? [o.literal(relativeLevelDiff)] : []);
}
function getLiteralFactory(
constantPool: ConstantPool, literal: o.LiteralArrayExpr | o.LiteralMapExpr,
allocateSlots: (numSlots: number) => number): o.Expression {
@ -919,33 +952,44 @@ function getLiteralFactory(
*
* It is expected that the function creates the `const localName = expression`; statement.
*/
export type DeclareLocalVarCallback = (lhsVar: o.ReadVarExpr, rhsExpression: o.Expression) => void;
export type DeclareLocalVarCallback = (scope: BindingScope, relativeLevel: number) => o.Statement[];
/** The prefix used to get a shared context in BindingScope's map. */
const SHARED_CONTEXT_KEY = '$$shared_ctx$$';
/**
* This is used when one refers to variable such as: 'let abc = x(2).$implicit`.
* - key to the map is the string literal `"abc"`.
* - value `retrievalLevel` is the level from which this value can be retrieved, which is 2 levels
* up in example.
* - value `lhs` is the left hand side which is an AST representing `abc`.
* - value `declareLocalCallback` is a callback that is invoked when declaring the local.
* - value `declare` is true if this value needs to be declared.
* - value `priority` dictates the sorting priority of this var declaration compared
* to other var declarations on the same retrieval level. For example, if there is a
* context variable and a local ref accessing the same parent view, the context var
* declaration should always come before the local ref declaration.
*/
type BindingData = {
retrievalLevel: number; lhs: o.ReadVarExpr; declareLocalCallback?: DeclareLocalVarCallback;
declare: boolean;
priority: number;
};
/**
* The sorting priority of a local variable declaration. Higher numbers
* mean the declaration will appear first in the generated code.
*/
const enum DeclarationPriority { DEFAULT = 0, CONTEXT = 1, SHARED_CONTEXT = 2 }
export class BindingScope implements LocalResolver {
/**
* Keeps a map from local variables to their expressions.
*
* This is used when one refers to variable such as: 'let abc = a.b.c`.
* - key to the map is the string literal `"abc"`.
* - value `lhs` is the left hand side which is an AST representing `abc`.
* - value `rhs` is the right hand side which is an AST representing `a.b.c`.
* - value `declared` is true if the `declareLocalVarCallback` has been called for this scope
* already.
*/
private map = new Map < string, {
lhs: o.ReadVarExpr;
rhs: o.Expression|undefined;
declared: boolean;
rhsCallback?: (level: number) => o.Expression;
}
> ();
/** Keeps a map from local variables to their BindingData. */
private map = new Map<string, BindingData>();
private referenceNameIndex = 0;
static ROOT_SCOPE = new BindingScope().set('$event', o.variable('$event'));
static ROOT_SCOPE = new BindingScope().set(-1, '$event', o.variable('$event'));
private constructor(
private level: number = 0, private parent: BindingScope|null = null,
private declareLocalVarCallback: DeclareLocalVarCallback = noop) {}
private constructor(public bindingLevel: number = 0, private parent: BindingScope|null = null) {}
get(name: string): o.Expression|null {
let current: BindingScope|null = this;
@ -953,48 +997,118 @@ export class BindingScope implements LocalResolver {
let value = current.map.get(name);
if (value != null) {
if (current !== this) {
// make a local copy and reset the `declared` state.
value = {lhs: value.lhs, rhs: value.rhs, rhsCallback: value.rhsCallback, declared: false};
// make a local copy and reset the `declare` state
value = {
retrievalLevel: value.retrievalLevel,
lhs: value.lhs,
declareLocalCallback: value.declareLocalCallback,
declare: false,
priority: value.priority
};
// Cache the value locally.
this.map.set(name, value);
// Possibly generate a shared context var
this.maybeGenerateSharedContextVar(value);
}
const rhs = value.rhs || value.rhsCallback && value.rhsCallback(this.level);
if (rhs && !value.declared) {
// if it is first time we are referencing the variable in the scope
// then invoke the callback to insert variable declaration.
this.declareLocalVarCallback(value.lhs, rhs);
value.declared = true;
if (value.declareLocalCallback && !value.declare) {
value.declare = true;
}
return value.lhs;
}
current = current.parent;
}
return null;
// If we get to this point, we are looking for a property on the top level component
// - If level === 0, we are on the top and don't need to re-declare `ctx`.
// - If level > 0, we are in an embedded view. We need to retrieve the name of the
// local var we used to store the component context, e.g. const $comp$ = x();
return this.bindingLevel === 0 ? null : this.getComponentProperty(name);
}
/**
* Create a local variable for later reference.
*
* @param retrievalLevel The level from which this value can be retrieved
* @param name Name of the variable.
* @param lhs AST representing the left hand side of the `let lhs = rhs;`.
* @param rhs AST representing the right hand side of the `let lhs = rhs;`. The `rhs` can be
* `undefined` for variable that are ambient such as `$event` and which don't have `rhs`
* declaration.
* @param priority The sorting priority of this var
* @param declareLocalCallback The callback to invoke when declaring this local var
*/
set(name: string, lhs: o.ReadVarExpr, rhs?: o.Expression,
rhsCallback?: (level: number) => o.Expression): BindingScope {
set(retrievalLevel: number, name: string, lhs: o.ReadVarExpr,
priority: number = DeclarationPriority.DEFAULT,
declareLocalCallback?: DeclareLocalVarCallback): BindingScope {
!this.map.has(name) ||
error(`The name ${name} is already defined in scope to be ${this.map.get(name)}`);
this.map.set(name, {lhs: lhs, rhs: rhs, declared: false, rhsCallback: rhsCallback});
this.map.set(name, {
retrievalLevel: retrievalLevel,
lhs: lhs,
declare: false,
declareLocalCallback: declareLocalCallback,
priority: priority
});
return this;
}
getLocal(name: string): (o.Expression|null) { return this.get(name); }
nestedScope(level: number, declareCallback: DeclareLocalVarCallback): BindingScope {
return new BindingScope(level, this, declareCallback);
nestedScope(level: number): BindingScope {
const newScope = new BindingScope(level, this);
if (level > 0) newScope.generateSharedContextVar(0);
return newScope;
}
getSharedContextName(retrievalLevel: number): o.ReadVarExpr|null {
const sharedCtxObj = this.map.get(SHARED_CONTEXT_KEY + retrievalLevel);
return sharedCtxObj && sharedCtxObj.declare ? sharedCtxObj.lhs : null;
}
maybeGenerateSharedContextVar(value: BindingData) {
if (value.priority === DeclarationPriority.CONTEXT) {
const sharedCtxObj = this.map.get(SHARED_CONTEXT_KEY + value.retrievalLevel);
if (sharedCtxObj) {
sharedCtxObj.declare = true;
} else {
this.generateSharedContextVar(value.retrievalLevel);
}
}
}
generateSharedContextVar(retrievalLevel: number) {
const lhs = o.variable(CONTEXT_NAME + this.freshReferenceName());
this.map.set(SHARED_CONTEXT_KEY + retrievalLevel, {
retrievalLevel: retrievalLevel,
lhs: lhs,
declareLocalCallback: (scope: BindingScope, relativeLevel: number) => {
// const ctx_r0 = x(2);
return [lhs.set(generateNextContextExpr(relativeLevel)).toConstDecl()];
},
declare: false,
priority: DeclarationPriority.SHARED_CONTEXT
});
}
getComponentProperty(name: string): o.Expression {
const componentValue = this.map.get(SHARED_CONTEXT_KEY + 0) !;
componentValue.declare = true;
return componentValue.lhs.prop(name);
}
variableDeclarations(): o.Statement[] {
let currentContextLevel = 0;
return Array.from(this.map.values())
.filter(value => value.declare)
.sort((a, b) => b.retrievalLevel - a.retrievalLevel || b.priority - a.priority)
.reduce((stmts: o.Statement[], value: BindingData) => {
const levelDiff = this.bindingLevel - value.retrievalLevel;
const currStmts = value.declareLocalCallback !(this, levelDiff - currentContextLevel);
currentContextLevel = levelDiff;
return stmts.concat(currStmts);
}, []) as o.Statement[];
}
freshReferenceName(): string {
let current: BindingScope = this;
// Find the top scope as it maintains the global reference count