refactor(compiler): next context merging in the template pipeline (#49797)

This commit adds a phase to the template pipeline to merge `nextContext()`
instructions that follow each other without context reads in between. That
is, the sequence:

```typescript
nextContext();
var v1 = nextContext();
```

becomes:

```typescript
var v1 = nextContext(2);
```

PR Close #49797
This commit is contained in:
Alex Rickabaugh
2023-04-14 15:48:54 -07:00
committed by Jessica Janiuk
parent 84af3e302d
commit ead10aff41
5 changed files with 86 additions and 4 deletions
@@ -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 {
@@ -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);
}
@@ -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)]);
}
@@ -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<ir.UpdateOp>): 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;
}
});
}
}
}
@@ -128,7 +128,7 @@ function reifyUpdateOperations(_view: ViewCompilation, ops: ir.OpList<ir.UpdateO
function reifyIrExpression(expr: ir.Expression): o.Expression {
switch (expr.kind) {
case ir.ExpressionKind.NextContext:
return ng.nextContext();
return ng.nextContext(expr.steps);
case ir.ExpressionKind.Reference:
return ng.reference(expr.slot! + 1 + expr.offset);
case ir.ExpressionKind.LexicalRead: