diff --git a/packages/compiler/src/template/pipeline/ir/index.ts b/packages/compiler/src/template/pipeline/ir/index.ts index cddbdb64546..cb373cc2aa5 100644 --- a/packages/compiler/src/template/pipeline/ir/index.ts +++ b/packages/compiler/src/template/pipeline/ir/index.ts @@ -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'; diff --git a/packages/compiler/src/template/pipeline/ir/src/enums.ts b/packages/compiler/src/template/pipeline/ir/src/enums.ts index 3bc4b73b2a6..73c9aab1be5 100644 --- a/packages/compiler/src/template/pipeline/ir/src/enums.ts +++ b/packages/compiler/src/template/pipeline/ir/src/enums.ts @@ -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, } diff --git a/packages/compiler/src/template/pipeline/ir/src/expression.ts b/packages/compiler/src/template/pipeline/ir/src/expression.ts index 8fe91439528..cb6d40df1b7 100644 --- a/packages/compiler/src/template/pipeline/ir/src/expression.ts +++ b/packages/compiler/src/template/pipeline/ir/src/expression.ts @@ -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}`); + } +} diff --git a/packages/compiler/src/template/pipeline/ir/src/ops/create.ts b/packages/compiler/src/template/pipeline/ir/src/ops/create.ts index f3ffed7c04c..f6208d888a8 100644 --- a/packages/compiler/src/template/pipeline/ir/src/ops/create.ts +++ b/packages/compiler/src/template/pipeline/ir/src/ops/create.ts @@ -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|StatementOp|ElementOp|ElementStartOp| - ElementEndOp|TemplateOp|TextOp|ListenerOp; + ElementEndOp|TemplateOp|TextOp|ListenerOp|VariableOp; /** * Representation of a local reference on an element. diff --git a/packages/compiler/src/template/pipeline/ir/src/ops/shared.ts b/packages/compiler/src/template/pipeline/ir/src/ops/shared.ts index c70be98cffb..b0824eb405b 100644 --- a/packages/compiler/src/template/pipeline/ir/src/ops/shared.ts +++ b/packages/compiler/src/template/pipeline/ir/src/ops/shared.ts @@ -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>(statement: o.Statement): }; } +/** + * Operation which declares and initializes a `SemanticVariable`, that is valid either in create or + * update IR. + */ +export interface VariableOp> extends Op { + 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>( + xref: XrefId, variable: SemanticVariable, initializer: o.Expression): VariableOp { + return { + kind: OpKind.Variable, + xref, + name: null, + variable, + initializer, + ...NEW_OP, + }; +} + /** * Static structure shared by all operations. * diff --git a/packages/compiler/src/template/pipeline/ir/src/ops/update.ts b/packages/compiler/src/template/pipeline/ir/src/ops/update.ts index c8acbd0ae2b..9b554bb70c4 100644 --- a/packages/compiler/src/template/pipeline/ir/src/ops/update.ts +++ b/packages/compiler/src/template/pipeline/ir/src/ops/update.ts @@ -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|StatementOp|PropertyOp|InterpolateTextOp; +export type UpdateOp = + ListEndOp|StatementOp|PropertyOp|InterpolateTextOp|VariableOp; /** * A logical operation to perform string interpolation on a text node. diff --git a/packages/compiler/src/template/pipeline/ir/src/variable.ts b/packages/compiler/src/template/pipeline/ir/src/variable.ts new file mode 100644 index 00000000000..602bdb81474 --- /dev/null +++ b/packages/compiler/src/template/pipeline/ir/src/variable.ts @@ -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; +} diff --git a/packages/compiler/src/template/pipeline/src/phases/generate_variables.ts b/packages/compiler/src/template/pipeline/src/phases/generate_variables.ts new file mode 100644 index 00000000000..4c9df8666a3 --- /dev/null +++ b/packages/compiler/src/template/pipeline/src/phases/generate_variables.ts @@ -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( + 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( + 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[] { + const newOps: ir.VariableOp[] = []; + + 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; +}