refactor(compiler): pipeline phase to generate variables (#48580)

This commit implements the first "phase" of the template pipeline. Phases are
individual steps in compilation that perform a transformation of the IR in order
to move closer to generating runtime code for the template.

This first phase is `phaseGenerateVariables()`. This phase introduces variable
definition operations into the IR to define variables in each view. These
variables either represent internal operations (saving/restoring the view
context for listeners, for example) or variables created from user-defined names
such as local references or template context properties.

Every view has all possibly-referenced variables generated, regardless of
whether they're actually referenced by other operations. A future phase will
optimize the variables in each view, inlining those which are only read once and
removing those which are not referenced at all.

PR Close #48580
This commit is contained in:
Alex Rickabaugh 2022-12-21 17:23:15 -05:00 committed by Andrew Kushnir
parent 99a2068a5b
commit 369fd7a540
8 changed files with 596 additions and 6 deletions

View file

@ -13,3 +13,4 @@ export * from './src/operations';
export * from './src/ops/create';
export * from './src/ops/shared';
export * from './src/ops/update';
export * from './src/variable';

View file

@ -23,6 +23,11 @@ export enum OpKind {
*/
Statement,
/**
* An operation which declares and initializes a `SemanticVariable`.
*/
Variable,
/**
* An operation to begin rendering of an element.
*/
@ -72,4 +77,59 @@ export enum ExpressionKind {
* Read of a variable in a lexical scope.
*/
LexicalRead,
/**
* A reference to the current view context.
*/
Context,
/**
* Read of a variable declared in a `VariableOp`.
*/
ReadVariable,
/**
* Runtime operation to navigate to the next view context in the view hierarchy.
*/
NextContext,
/**
* Runtime operation to retrieve the value of a local reference.
*/
Reference,
/**
* Runtime operation to snapshot the current view context.
*/
GetCurrentView,
/**
* Runtime operation to restore a snapshotted view.
*/
RestoreView,
/**
* Runtime operation to reset the current view context after `RestoreView`.
*/
ResetView,
}
/**
* Distinguishes between different kinds of `SemanticVariable`s.
*/
export enum SemanticVariableKind {
/**
* Represents the context of a particular view.
*/
Context,
/**
* Represents an identifier declared in the lexical scope of a view.
*/
Identifier,
/**
* Represents a saved state that can be used to restore a view in a listener handler function.
*/
SavedView,
}

View file

@ -10,11 +10,13 @@ import * as o from '../../../../output/output_ast';
import type {ParseSourceSpan} from '../../../../parse_util';
import {ExpressionKind} from './enums';
import {XrefId} from './operations';
/**
* An `o.Expression` subtype representing a logical expression in the intermediate representation.
*/
export type Expression = LexicalReadExpr;
export type Expression = LexicalReadExpr|ReferenceExpr|ContextExpr|NextContextExpr|
GetCurrentViewExpr|RestoreViewExpr|ResetViewExpr;
/**
* Transformer type which converts IR expressions into general `o.Expression`s (which may be an
@ -68,3 +70,213 @@ export class LexicalReadExpr extends ExpressionBase {
override transformInternalExpressions(): void {}
}
/**
* Runtime operation to retrieve the value of a local reference.
*/
export class ReferenceExpr extends ExpressionBase {
readonly kind = ExpressionKind.Reference;
constructor(readonly target: XrefId, readonly offset: number) {
super();
}
override visitExpression(): void {}
override isEquivalent(e: o.Expression): boolean {
return e instanceof ReferenceExpr && e.target === this.target;
}
override isConstant(): boolean {
return false;
}
override transformInternalExpressions(): void {}
}
/**
* A reference to the current view context (usually the `ctx` variable in a template function).
*/
export class ContextExpr extends ExpressionBase {
readonly kind = ExpressionKind.Context;
constructor(readonly view: XrefId) {
super();
}
override visitExpression(): void {}
override isEquivalent(e: o.Expression): boolean {
return e instanceof ContextExpr && e.view === this.view;
}
override isConstant(): boolean {
return false;
}
override transformInternalExpressions(): void {}
}
/**
* Runtime operation to navigate to the next view context in the view hierarchy.
*/
export class NextContextExpr extends ExpressionBase {
readonly kind = ExpressionKind.NextContext;
constructor() {
super();
}
override visitExpression(): void {}
override isEquivalent(e: o.Expression): boolean {
return e instanceof NextContextExpr;
}
override isConstant(): boolean {
return false;
}
override transformInternalExpressions(): void {}
}
/**
* Runtime operation to snapshot the current view context.
*
* The result of this operation can be stored in a variable and later used with the `RestoreView`
* operation.
*/
export class GetCurrentViewExpr extends ExpressionBase {
readonly kind = ExpressionKind.GetCurrentView;
constructor() {
super();
}
override visitExpression(): void {}
override isEquivalent(e: o.Expression): boolean {
return e instanceof GetCurrentViewExpr;
}
override isConstant(): boolean {
return false;
}
override transformInternalExpressions(): void {}
}
/**
* Runtime operation to restore a snapshotted view.
*/
export class RestoreViewExpr extends ExpressionBase {
readonly kind = ExpressionKind.RestoreView;
constructor(public view: XrefId|o.Expression) {
super();
}
override visitExpression(visitor: o.ExpressionVisitor, context: any): void {
if (typeof this.view !== 'number') {
this.view.visitExpression(visitor, context);
}
}
override isEquivalent(e: o.Expression): boolean {
if (!(e instanceof RestoreViewExpr) || typeof e.view !== typeof this.view) {
return false;
}
if (typeof this.view === 'number') {
return this.view === e.view;
} else {
return this.view.isEquivalent(e.view as o.Expression);
}
}
override isConstant(): boolean {
return false;
}
override transformInternalExpressions(transform: ExpressionTransform): void {
if (typeof this.view !== 'number') {
this.view = transformExpressionsInExpression(this.view, transform);
}
}
}
/**
* Runtime operation to reset the current view context after `RestoreView`.
*/
export class ResetViewExpr extends ExpressionBase {
readonly kind = ExpressionKind.ResetView;
constructor(public expr: o.Expression) {
super();
}
override visitExpression(visitor: o.ExpressionVisitor, context: any): any {
this.expr.visitExpression(visitor, context);
}
override isEquivalent(e: o.Expression): boolean {
return e instanceof ResetViewExpr && this.expr.isEquivalent(e.expr);
}
override isConstant(): boolean {
return false;
}
override transformInternalExpressions(transform: ExpressionTransform): void {
this.expr = transformExpressionsInExpression(this.expr, transform);
}
}
/**
* Transform all `Expression`s in the AST of `expr` with the `transform` function.
*
* All such operations will be replaced with the result of applying `transform`, which may be an
* identity transformation.
*/
export function transformExpressionsInExpression(
expr: o.Expression, transform: ExpressionTransform): o.Expression {
if (expr instanceof ExpressionBase) {
expr.transformInternalExpressions(transform);
return transform(expr as Expression);
} else if (expr instanceof o.BinaryOperatorExpr) {
expr.lhs = transformExpressionsInExpression(expr.lhs, transform);
expr.rhs = transformExpressionsInExpression(expr.rhs, transform);
} else if (expr instanceof o.ReadPropExpr) {
expr.receiver = transformExpressionsInExpression(expr.receiver, transform);
} else if (expr instanceof o.InvokeFunctionExpr) {
expr.fn = transformExpressionsInExpression(expr.fn, transform);
for (let i = 0; i < expr.args.length; i++) {
expr.args[i] = transformExpressionsInExpression(expr.args[i], transform);
}
} else if (
expr instanceof o.ReadVarExpr || expr instanceof o.ExternalExpr ||
expr instanceof o.LiteralExpr) {
// No action for these types.
} else {
throw new Error(`Unhandled expression kind: ${expr.constructor.name}`);
}
return expr;
}
/**
* Transform all `Expression`s in the AST of `stmt` with the `transform` function.
*
* All such operations will be replaced with the result of applying `transform`, which may be an
* identity transformation.
*/
export function transformExpressionsInStatement(
stmt: o.Statement, transform: ExpressionTransform): void {
if (stmt instanceof o.ExpressionStatement) {
stmt.expr = transformExpressionsInExpression(stmt.expr, transform);
} else if (stmt instanceof o.ReturnStatement) {
stmt.value = transformExpressionsInExpression(stmt.value, transform);
} else {
throw new Error(`Unhandled statement kind: ${stmt.constructor.name}`);
}
}

View file

@ -10,14 +10,14 @@ import {ElementAttributes} from '../element';
import {OpKind} from '../enums';
import {Op, OpList, XrefId} from '../operations';
import {ListEndOp, NEW_OP, StatementOp} from './shared';
import {ListEndOp, NEW_OP, StatementOp, VariableOp} from './shared';
import type {UpdateOp} from './update';
/**
* An operation usable on the creation side of the IR.
*/
export type CreateOp = ListEndOp<CreateOp>|StatementOp<CreateOp>|ElementOp|ElementStartOp|
ElementEndOp|TemplateOp|TextOp|ListenerOp;
ElementEndOp|TemplateOp|TextOp|ListenerOp|VariableOp<CreateOp>;
/**
* Representation of a local reference on an element.

View file

@ -8,7 +8,8 @@
import * as o from '../../../../../output/output_ast';
import {OpKind} from '../enums';
import {Op} from '../operations';
import {Op, XrefId} from '../operations';
import {SemanticVariable} from '../variable';
/**
* A special `Op` which is used internally in the `OpList` linked list to represent the head and
@ -45,6 +46,50 @@ export function createStatementOp<OpT extends Op<OpT>>(statement: o.Statement):
};
}
/**
* Operation which declares and initializes a `SemanticVariable`, that is valid either in create or
* update IR.
*/
export interface VariableOp<OpT extends Op<OpT>> extends Op<OpT> {
kind: OpKind.Variable;
/**
* `XrefId` which identifies this specific variable, and is used to reference this variable from
* other parts of the IR.
*/
xref: XrefId;
/**
* Name assigned to this variable in generated code, or `null` if not yet assigned.
*/
name: string|null;
/**
* The `SemanticVariable` which describes the meaning behind this variable.
*/
variable: SemanticVariable;
/**
* Expression representing the value of the variable.
*/
initializer: o.Expression;
}
/**
* Create a `VariableOp`.
*/
export function createVariableOp<OpT extends Op<OpT>>(
xref: XrefId, variable: SemanticVariable, initializer: o.Expression): VariableOp<OpT> {
return {
kind: OpKind.Variable,
xref,
name: null,
variable,
initializer,
...NEW_OP,
};
}
/**
* Static structure shared by all operations.
*

View file

@ -10,12 +10,13 @@ import * as o from '../../../../../output/output_ast';
import {OpKind} from '../enums';
import {Op, XrefId} from '../operations';
import {ListEndOp, NEW_OP, StatementOp} from './shared';
import {ListEndOp, NEW_OP, StatementOp, VariableOp} from './shared';
/**
* An operation usable on the update side of the IR.
*/
export type UpdateOp = ListEndOp<UpdateOp>|StatementOp<UpdateOp>|PropertyOp|InterpolateTextOp;
export type UpdateOp =
ListEndOp<UpdateOp>|StatementOp<UpdateOp>|PropertyOp|InterpolateTextOp|VariableOp<UpdateOp>;
/**
* A logical operation to perform string interpolation on a text node.

View file

@ -0,0 +1,51 @@
/**
* @license
* Copyright Google LLC 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 type {SemanticVariableKind} from './enums';
import type {XrefId} from './operations';
/**
* Union type for the different kinds of variables.
*/
export type SemanticVariable = ContextVariable|IdentifierVariable|SavedViewVariable;
/**
* A variable that represents the context of a particular view.
*/
export interface ContextVariable {
kind: SemanticVariableKind.Context;
/**
* `XrefId` of the view that this variable represents.
*/
view: XrefId;
}
/**
* A variable that represents a specific identifier within a template.
*/
export interface IdentifierVariable {
kind: SemanticVariableKind.Identifier;
/**
* The identifier whose value in the template is tracked in this variable.
*/
name: string;
}
/**
* A variable that represents a saved view context.
*/
export interface SavedViewVariable {
kind: SemanticVariableKind.SavedView;
/**
* The view context saved in this variable.
*/
view: XrefId;
}

View file

@ -0,0 +1,220 @@
/**
* @license
* Copyright Google LLC 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 * as o from '../../../../output/output_ast';
import * as ir from '../../ir';
import type {ComponentCompilation, ViewCompilation} from '../compilation';
/**
* Generate a preamble sequence for each view creation block and listener function which declares
* any variables that be referenced in other operations in the block.
*
* Variables generated include:
* * a saved view context to be used to restore the current view in event listeners.
* * the context of the restored view within event listener handlers.
* * context variables from the current view as well as all parent views (including the root
* context if needed).
* * local references from elements within the current view and any lexical parents.
*
* Variables are generated here unconditionally, and may optimized away in future operations if it
* turns out their values (and any side effects) are unused.
*/
export function phaseGenerateVariables(cpl: ComponentCompilation): void {
recursivelyProcessView(cpl.root, /* there is no parent scope for the root view */ null);
}
/**
* Process the given `ViewCompilation` and generate preambles for it and any listeners that it
* declares.
*
* @param `parentScope` a scope extracted from the parent view which captures any variables which
* should be inherited by this view. `null` if the current view is the root view.
*/
function recursivelyProcessView(view: ViewCompilation, parentScope: Scope|null): void {
// Extract a `Scope` from this view.
const scope = getScopeForView(view, parentScope);
// Start the view creation block with an operation to save the current view context. This may be
// used to restore the view context in any listeners that may be present.
view.create.prepend([
ir.createVariableOp<ir.CreateOp>(
view.tpl.allocateXrefId(), {
kind: ir.SemanticVariableKind.SavedView,
view: view.xref,
},
new ir.GetCurrentViewExpr()),
]);
for (const op of view.create) {
switch (op.kind) {
case ir.OpKind.Template:
// Descend into child embedded views.
recursivelyProcessView(view.tpl.views.get(op.xref)!, scope);
break;
case ir.OpKind.Listener:
// Listeners get a preamble which starts with a call to restore the view.
const preambleOps = [
ir.createVariableOp<ir.UpdateOp>(
view.tpl.allocateXrefId(), {
kind: ir.SemanticVariableKind.Context,
view: view.xref,
},
new ir.RestoreViewExpr(view.xref)),
// And includes all variables available to this view.
...generateVariablesInScopeForView(view, scope)
];
op.handlerOps.prepend(preambleOps);
// The "restore view" operation in listeners requires a call to `resetView` to reset the
// context prior to returning from the listener operation. Find any `return` statements in
// the listener body and wrap them in a call to reset the view.
for (const handlerOp of op.handlerOps) {
if (handlerOp.kind === ir.OpKind.Statement &&
handlerOp.statement instanceof o.ReturnStatement) {
handlerOp.statement.value = new ir.ResetViewExpr(handlerOp.statement.value);
}
}
break;
}
}
// Prepend the declarations for all available variables in scope to the `update` block.
const preambleOps = generateVariablesInScopeForView(view, scope);
view.update.prepend(preambleOps);
}
/**
* Lexical scope of a view, including a reference to its parent view's scope, if any.
*/
interface Scope {
/**
* `XrefId` of the view to which this scope corresponds.
*/
view: ir.XrefId;
/**
* Local references collected from elements within the view.
*/
references: Reference[];
/**
* `Scope` of the parent view, if any.
*/
parent: Scope|null;
}
/**
* Information needed about a local reference collected from an element within a view.
*/
interface Reference {
/**
* Name given to the local reference variable within the template.
*
* This is not the name which will be used for the variable declaration in the generated
* template code.
*/
name: string;
/**
* `XrefId` of the element-like node which this reference targets.
*
* The reference may be either to the element (or template) itself, or to a directive on it.
*/
targetId: ir.XrefId;
/**
* A generated offset of this reference among all the references on a specific element.
*/
offset: number;
}
/**
* Process a view and generate a `Scope` representing the variables available for reference within
* that view.
*/
function getScopeForView(view: ViewCompilation, parent: Scope|null): Scope {
const scope: Scope = {
view: view.xref,
references: [],
parent,
};
for (const op of view.create) {
switch (op.kind) {
case ir.OpKind.Element:
case ir.OpKind.ElementStart:
case ir.OpKind.Template:
if (!Array.isArray(op.localRefs)) {
throw new Error(`AssertionError: expected localRefs to be an array`);
}
// Record available local references from this element.
for (let offset = 0; offset < op.localRefs.length; offset++) {
scope.references.push({
name: op.localRefs[offset].name,
targetId: op.xref,
offset,
});
}
break;
}
}
return scope;
}
/**
* Generate declarations for all variables that are in scope for a given view.
*
* This is a recursive process, as views inherit variables available from their parent view, which
* itself may have inherited variables, etc.
*/
function generateVariablesInScopeForView(
view: ViewCompilation, scope: Scope): ir.VariableOp<ir.UpdateOp>[] {
const newOps: ir.VariableOp<ir.UpdateOp>[] = [];
if (scope.view !== view.xref) {
// Before generating variables for a parent view, we need to switch to the context of the parent
// view with a `nextContext` expression. This context switching operation itself declares a
// variable, because the context of the view may be referenced directly.
newOps.push(ir.createVariableOp(
view.tpl.allocateXrefId(), {
kind: ir.SemanticVariableKind.Context,
view: scope.view,
},
new ir.NextContextExpr()));
}
// Add variables for all context variables available in this scope's view.
for (const [name, value] of view.tpl.views.get(scope.view)!.contextVariables) {
newOps.push(ir.createVariableOp(
view.tpl.allocateXrefId(), {
kind: ir.SemanticVariableKind.Identifier,
name,
},
new o.ReadPropExpr(new ir.ContextExpr(view.xref), value)));
}
// Add variables for all local references declared for elements in this scope.
for (const ref of scope.references) {
newOps.push(ir.createVariableOp(
view.tpl.allocateXrefId(), {
kind: ir.SemanticVariableKind.Identifier,
name: ref.name,
},
new ir.ReferenceExpr(ref.targetId, ref.offset)));
}
if (scope.parent !== null) {
// Recursively add variables from the parent scope.
newOps.push(...generateVariablesInScopeForView(view, scope.parent));
}
return newOps;
}