diff --git a/packages/compiler/src/template/pipeline/ir/src/expression.ts b/packages/compiler/src/template/pipeline/ir/src/expression.ts index a1a5f874cca..2870975e794 100644 --- a/packages/compiler/src/template/pipeline/ir/src/expression.ts +++ b/packages/compiler/src/template/pipeline/ir/src/expression.ts @@ -132,6 +132,8 @@ export class ContextExpr extends ExpressionBase { export class NextContextExpr extends ExpressionBase { override readonly kind = ExpressionKind.NextContext; + steps = 1; + constructor() { super(); } @@ -139,7 +141,7 @@ export class NextContextExpr extends ExpressionBase { override visitExpression(): void {} override isEquivalent(e: o.Expression): boolean { - return e instanceof NextContextExpr; + return e instanceof NextContextExpr && e.steps === this.steps; } override isConstant(): boolean { diff --git a/packages/compiler/src/template/pipeline/src/emit.ts b/packages/compiler/src/template/pipeline/src/emit.ts index 7a985f23970..0ac73662aad 100644 --- a/packages/compiler/src/template/pipeline/src/emit.ts +++ b/packages/compiler/src/template/pipeline/src/emit.ts @@ -25,6 +25,7 @@ import {phaseResolveNames} from './phases/resolve_names'; import {phaseResolveContexts} from './phases/resolve_contexts'; import {phaseVariableOptimization} from './phases/variable_optimization'; import {phaseChaining} from './phases/chaining'; +import {phaseMergeNextContext} from './phases/next_context_merging'; /** * Run all transformation phases in the correct order against a `ComponentCompilation`. After this @@ -42,6 +43,7 @@ export function transformTemplate(cpl: ComponentCompilation): void { phaseGenerateAdvance(cpl); phaseNaming(cpl); phaseVariableOptimization(cpl, {conservative: true}); + phaseMergeNextContext(cpl); phaseReify(cpl); phaseChaining(cpl); } diff --git a/packages/compiler/src/template/pipeline/src/instruction.ts b/packages/compiler/src/template/pipeline/src/instruction.ts index 258311c551e..5222be1dbd8 100644 --- a/packages/compiler/src/template/pipeline/src/instruction.ts +++ b/packages/compiler/src/template/pipeline/src/instruction.ts @@ -80,8 +80,8 @@ export function reference(slot: number): o.Expression { ]); } -export function nextContext(): o.Expression { - return o.importExpr(Identifiers.nextContext).callFn([]); +export function nextContext(steps: number): o.Expression { + return o.importExpr(Identifiers.nextContext).callFn(steps === 1 ? [] : [o.literal(steps)]); } diff --git a/packages/compiler/src/template/pipeline/src/phases/next_context_merging.ts b/packages/compiler/src/template/pipeline/src/phases/next_context_merging.ts new file mode 100644 index 00000000000..803e599f9fb --- /dev/null +++ b/packages/compiler/src/template/pipeline/src/phases/next_context_merging.ts @@ -0,0 +1,78 @@ +/** + * @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} from '../compilation'; + +/** + * Merges logically sequential `NextContextExpr` operations. + * + * `NextContextExpr` can be referenced repeatedly, "popping" the runtime's context stack each time. + * When two such expressions appear back-to-back, it's possible to merge them together into a single + * `NextContextExpr` that steps multiple contexts. This merging is possible if all conditions are + * met: + * + * * The result of the `NextContextExpr` that's folded into the subsequent one is not stored (that + * is, the call is purely side-effectful). + * * No operations in between them uses the implicit context. + */ +export function phaseMergeNextContext(cpl: ComponentCompilation): void { + for (const view of cpl.views.values()) { + for (const op of view.create) { + if (op.kind === ir.OpKind.Listener) { + mergeNextContextsInOps(op.handlerOps); + } + } + mergeNextContextsInOps(view.update); + } +} + +function mergeNextContextsInOps(ops: ir.OpList): void { + for (const op of ops) { + // Look for a candidate operation to maybe merge. + if (op.kind !== ir.OpKind.Statement || !(op.statement instanceof o.ExpressionStatement) || + !(op.statement.expr instanceof ir.NextContextExpr)) { + continue; + } + + const mergeSteps = op.statement.expr.steps; + + // Try to merge this `ir.NextContextExpr`. + let tryToMerge = true; + for (let candidate = op.next!; candidate.kind !== ir.OpKind.ListEnd && tryToMerge; + candidate = candidate.next!) { + ir.visitExpressionsInOp(candidate, (expr, flags) => { + if (!tryToMerge) { + // Either we've already merged, or failed to merge. + return; + } + + if (flags & ir.VisitorContextFlag.InChildOperation) { + // We cannot merge into child operations. + return; + } + + switch (expr.kind) { + case ir.ExpressionKind.NextContext: + // Merge the previous `ir.NextContextExpr` into this one. + expr.steps += mergeSteps; + ir.OpList.remove(op as ir.UpdateOp); + tryToMerge = false; + break; + case ir.ExpressionKind.GetCurrentView: + case ir.ExpressionKind.Reference: + // Can't merge past a dependency on the context. + tryToMerge = false; + break; + } + }); + } + } +} diff --git a/packages/compiler/src/template/pipeline/src/phases/reify.ts b/packages/compiler/src/template/pipeline/src/phases/reify.ts index 18b18031ea8..e6834a8daa7 100644 --- a/packages/compiler/src/template/pipeline/src/phases/reify.ts +++ b/packages/compiler/src/template/pipeline/src/phases/reify.ts @@ -128,7 +128,7 @@ function reifyUpdateOperations(_view: ViewCompilation, ops: ir.OpList