Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor(compiler): support pipes in the template pipeline
This commit adds end-to-end support for pipes in the template pipeline. This
support works across multiple steps:

1. Pipes are first ingested as `ir.PipeBindingExpr`s during the ingest step.

2. A "pipe creation" phase inserts operations to instantiate each required
pipe, based on the presence of those `ir.PipeBindingExpr`s.

3. A "variadic pipe" phase transforms pipes with more than 4 arguments into
variadic pipe bindings, which use a literal array argument. This literal
array will be later memoized into a pure function invocation.

4. A special phase (`phaseAlignPipeVariadicVarOffset`) reconciles a
difference in variable slot assignment logic between the template pipeline
and the `TemplateDefinitionBuilder`, to ensure that the pipeline output can
pass the existing tests. This phase should not affect runtime semantics and
can be dropped once matching output is no longer necessary.

5. Reification emits pipe instructions based on the argument count.
  • Loading branch information
alxhub committed May 9, 2023
commit 34b42c10d52cefce8198c90a2a7484c7b565acb5
15 changes: 15 additions & 0 deletions packages/compiler/src/template/pipeline/ir/src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ export enum OpKind {
* An operation to advance the runtime's implicit slot context during the update phase of a view.
*/
Advance,

/**
* An operation to instantiate a pipe.
*/
Pipe,
}

/**
Expand Down Expand Up @@ -142,6 +147,16 @@ export enum ExpressionKind {
* Indicates a positional parameter to a pure function definition.
*/
PureFunctionParameterExpr,

/**
* Binding to a pipe transformation.
*/
PipeBinding,

/**
* Binding to a pipe transformation with a variable number of arguments.
*/
PipeBindingVariadic,
}

/**
Expand Down
81 changes: 77 additions & 4 deletions packages/compiler/src/template/pipeline/ir/src/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as o from '../../../../output/output_ast';
import type {ParseSourceSpan} from '../../../../parse_util';

import {ExpressionKind, OpKind} from './enums';
import {UsesSlotIndex, UsesSlotIndexTrait, UsesVarOffset, UsesVarOffsetTrait} from './traits';
import {ConsumesVarsTrait, UsesSlotIndex, UsesSlotIndexTrait, UsesVarOffset, UsesVarOffsetTrait} from './traits';

import type {XrefId} from './operations';
import type {CreateOp} from './ops/create';
Expand All @@ -19,9 +19,9 @@ import type {UpdateOp} from './ops/update';
/**
* An `o.Expression` subtype representing a logical expression in the intermediate representation.
*/
export type Expression =
LexicalReadExpr|ReferenceExpr|ContextExpr|NextContextExpr|GetCurrentViewExpr|RestoreViewExpr|
ResetViewExpr|ReadVariableExpr|PureFunctionExpr|PureFunctionParameterExpr;
export type Expression = LexicalReadExpr|ReferenceExpr|ContextExpr|NextContextExpr|
GetCurrentViewExpr|RestoreViewExpr|ResetViewExpr|ReadVariableExpr|PureFunctionExpr|
PureFunctionParameterExpr|PipeBindingExpr|PipeBindingVariadicExpr;

/**
* Transformer type which converts expressions into general `o.Expression`s (which may be an
Expand Down Expand Up @@ -359,6 +359,78 @@ export class PureFunctionParameterExpr extends ExpressionBase {
override transformInternalExpressions(): void {}
}

export class PipeBindingExpr extends ExpressionBase implements UsesSlotIndexTrait,
ConsumesVarsTrait,
UsesVarOffsetTrait {
override readonly kind = ExpressionKind.PipeBinding;
readonly[UsesSlotIndex] = true;
readonly[ConsumesVarsTrait] = true;
readonly[UsesVarOffset] = true;

slot: number|null = null;
varOffset: number|null = null;

constructor(readonly target: XrefId, readonly name: string, readonly args: o.Expression[]) {
super();
}

override visitExpression(visitor: o.ExpressionVisitor, context: any): void {
for (const arg of this.args) {
arg.visitExpression(visitor, context);
}
}

override isEquivalent(): boolean {
return false;
}

override isConstant(): boolean {
return false;
}

override transformInternalExpressions(transform: ExpressionTransform, flags: VisitorContextFlag):
void {
for (let idx = 0; idx < this.args.length; idx++) {
this.args[idx] = transformExpressionsInExpression(this.args[idx], transform, flags);
}
}
}

export class PipeBindingVariadicExpr extends ExpressionBase implements UsesSlotIndexTrait,
ConsumesVarsTrait,
UsesVarOffsetTrait {
override readonly kind = ExpressionKind.PipeBindingVariadic;
readonly[UsesSlotIndex] = true;
readonly[ConsumesVarsTrait] = true;
readonly[UsesVarOffset] = true;

slot: number|null = null;
varOffset: number|null = null;

constructor(
readonly target: XrefId, readonly name: string, public args: o.Expression,
public numArgs: number) {
super();
}

override visitExpression(visitor: o.ExpressionVisitor, context: any): void {
this.args.visitExpression(visitor, context);
}

override isEquivalent(): boolean {
return false;
}

override isConstant(): boolean {
return false;
}

override transformInternalExpressions(transform: ExpressionTransform, flags: VisitorContextFlag):
void {
this.args = transformExpressionsInExpression(this.args, transform, flags);
}
}

/**
* Visits all `Expression`s in the AST of `op` with the `visitor` function.
*/
Expand Down Expand Up @@ -411,6 +483,7 @@ export function transformExpressionsInOp(
case OpKind.ContainerEnd:
case OpKind.Template:
case OpKind.Text:
case OpKind.Pipe:
case OpKind.Advance:
// These operations contain no expressions.
break;
Expand Down
18 changes: 17 additions & 1 deletion packages/compiler/src/template/pipeline/ir/src/ops/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {UpdateOp} from './update';
*/
export type CreateOp =
ListEndOp<CreateOp>|StatementOp<CreateOp>|ElementOp|ElementStartOp|ElementEndOp|ContainerOp|
ContainerStartOp|ContainerEndOp|TemplateOp|TextOp|ListenerOp|VariableOp<CreateOp>;
ContainerStartOp|ContainerEndOp|TemplateOp|TextOp|ListenerOp|PipeOp|VariableOp<CreateOp>;

/**
* Representation of a local reference on an element.
Expand Down Expand Up @@ -272,6 +272,22 @@ export function createListenerOp(target: XrefId, name: string, tag: string): Lis
};
}

export interface PipeOp extends Op<CreateOp>, ConsumesSlotOpTrait {
kind: OpKind.Pipe;
xref: XrefId;
name: string;
}

export function createPipeOp(xref: XrefId, name: string): PipeOp {
return {
kind: OpKind.Pipe,
xref,
name,
...NEW_OP,
...TRAIT_CONSUMES_SLOT,
};
}

/**
* An index into the `consts` array which is shared across the compilation of all views in a
* component.
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler/src/template/pipeline/src/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@ import {phaseMergeNextContext} from './phases/next_context_merging';
import {phaseNgContainer} from './phases/ng_container';
import {phaseSaveRestoreView} from './phases/save_restore_view';
import {phasePureFunctionExtraction} from './phases/pure_function_extraction';
import {phasePipeCreation} from './phases/pipe_creation';
import {phasePipeVariadic} from './phases/pipe_variadic';
import {phasePureLiteralStructures} from './phases/pure_literal_structures';
import {phaseAlignPipeVariadicVarOffset} from './phases/align_pipe_variadic_var_offset';

/**
* Run all transformation phases in the correct order against a `ComponentCompilation`. After this
* processing, the compilation should be in a state where it can be emitted via `emitTemplateFn`.s
*/
export function transformTemplate(cpl: ComponentCompilation): void {
phasePipeCreation(cpl);
phasePipeVariadic(cpl);
phasePureLiteralStructures(cpl);
phaseGenerateVariables(cpl);
phaseSaveRestoreView(cpl);
Expand All @@ -52,6 +57,7 @@ export function transformTemplate(cpl: ComponentCompilation): void {
phaseNgContainer(cpl);
phaseEmptyElements(cpl);
phasePureFunctionExtraction(cpl);
phaseAlignPipeVariadicVarOffset(cpl);
phaseReify(cpl);
phaseChaining(cpl);
}
Expand Down
9 changes: 9 additions & 0 deletions packages/compiler/src/template/pipeline/src/ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,15 @@ function convertAst(ast: e.AST, cpl: ComponentCompilation): o.Expression {
convertAst(ast.trueExp, cpl),
convertAst(ast.falseExp, cpl),
);
} else if (ast instanceof e.BindingPipe) {
return new ir.PipeBindingExpr(
cpl.allocateXrefId(),
ast.name,
[
convertAst(ast.exp, cpl),
...ast.args.map(arg => convertAst(arg, cpl)),
],
);
} else {
throw new Error(`Unhandled expression type: ${ast.constructor.name}`);
}
Expand Down
36 changes: 35 additions & 1 deletion packages/compiler/src/template/pipeline/src/instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ export function listener(name: string, handlerFn: o.Expression): ir.CreateOp {
]);
}

export function pipe(slot: number, name: string): ir.CreateOp {
return call(Identifiers.pipe, [
o.literal(slot),
o.literal(name),
]);
}

export function advance(delta: number): ir.UpdateOp {
return call(Identifiers.advance, [
o.literal(delta),
Expand Down Expand Up @@ -133,6 +140,34 @@ export function property(name: string, expression: o.Expression): ir.UpdateOp {
]);
}

const PIPE_BINDINGS: o.ExternalReference[] = [
Identifiers.pipeBind1,
Identifiers.pipeBind2,
Identifiers.pipeBind3,
Identifiers.pipeBind4,
];

export function pipeBind(slot: number, varOffset: number, args: o.Expression[]): o.Expression {
if (args.length < 1 || args.length > PIPE_BINDINGS.length) {
throw new Error(`pipeBind() argument count out of bounds`);
}

const instruction = PIPE_BINDINGS[args.length - 1];
return o.importExpr(instruction).callFn([
o.literal(slot),
o.literal(varOffset),
...args,
]);
}

export function pipeBindV(slot: number, varOffset: number, args: o.Expression): o.Expression {
return o.importExpr(Identifiers.pipeBindV).callFn([
o.literal(slot),
o.literal(varOffset),
args,
]);
}

export function textInterpolate(strings: string[], expressions: o.Expression[]): ir.UpdateOp {
if (strings.length < 1 || expressions.length !== strings.length - 1) {
throw new Error(
Expand Down Expand Up @@ -166,7 +201,6 @@ export function pureFunction(
);
}


function call<OpT extends ir.CreateOp|ir.UpdateOp>(
instruction: o.ExternalReference, args: o.Expression[]): OpT {
return ir.createStatementOp(o.importExpr(instruction).callFn(args).toStmt()) as OpT;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* @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 ir from '../../ir';

import type {ComponentCompilation} from '../compilation';
import {varsUsedByIrExpression} from './var_counting';

export function phaseAlignPipeVariadicVarOffset(cpl: ComponentCompilation): void {
for (const view of cpl.views.values()) {
for (const op of view.update) {
ir.visitExpressionsInOp(op, expr => {
if (!(expr instanceof ir.PipeBindingVariadicExpr)) {
return expr;
}

if (!(expr.args instanceof ir.PureFunctionExpr)) {
return expr;
}

if (expr.varOffset === null || expr.args.varOffset === null) {
throw new Error(`Must run after variable counting`);
}

// The structure of this variadic pipe expression is:
// PipeBindingVariadic(#, Y, PureFunction(X, ...ARGS))
// Where X and Y are the slot offsets for the variables used by these operations, and Y > X.

// In `TemplateDefinitionBuilder` the PipeBindingVariadic variable slots are allocated
// before the PureFunction slots, which is unusually out-of-order.
//
// To maintain identical output for the tests in question, we adjust the variable offsets of
// these two calls to emulate TDB's behavior. This is not perfect, because the ARGS of the
// PureFunction call may also allocate slots which by TDB's ordering would come after X, and
// we don't account for that. Still, this should be enough to pass the existing pipe tests.

// Put the PipeBindingVariadic vars where the PureFunction vars were previously allocated.
expr.varOffset = expr.args.varOffset;

// Put the PureFunction vars following the PipeBindingVariadic vars.
expr.args.varOffset = expr.varOffset + varsUsedByIrExpression(expr);
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @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 ir from '../../ir';
import type {ComponentCompilation, ViewCompilation} from '../compilation';

export function phasePipeCreation(cpl: ComponentCompilation): void {
for (const view of cpl.views.values()) {
processPipeBindingsInView(view);
}
}

function processPipeBindingsInView(view: ViewCompilation): void {
for (const updateOp of view.update) {
ir.visitExpressionsInOp(updateOp, (expr, flags) => {
if (!ir.isIrExpression(expr)) {
return;
}

if (expr.kind !== ir.ExpressionKind.PipeBinding) {
return;
}

if (flags & ir.VisitorContextFlag.InChildOperation) {
throw new Error(`AssertionError: pipe bindings should not appear in child expressions`);
}

if (!ir.hasDependsOnSlotContextTrait(updateOp)) {
throw new Error(`AssertionError: pipe binding associated with non-slot operation ${
ir.OpKind[updateOp.kind]}`);
}

addPipeToCreationBlock(view, updateOp.target, expr);
});
}
}

function addPipeToCreationBlock(
view: ViewCompilation, afterTargetXref: ir.XrefId, binding: ir.PipeBindingExpr): void {
// Find the appropriate point to insert the Pipe creation operation.
// We're looking for `afterTargetXref` (and also want to insert after any other pipe operations
// which might be beyond it).
for (let op = view.create.head.next!; op.kind !== ir.OpKind.ListEnd; op = op.next!) {
if (!ir.hasConsumesSlotTrait<ir.CreateOp>(op)) {
continue;
}

if (op.xref !== afterTargetXref) {
continue;
}

// We've found a tentative insertion point; however, we also want to skip past any _other_ pipe
// operations present.
while (op.next!.kind === ir.OpKind.Pipe) {
op = op.next!;
}

const pipe = ir.createPipeOp(binding.target, binding.name) as ir.CreateOp;
ir.OpList.insertBefore(pipe, op.next!);

// This completes adding the pipe to the creation block.
return;
}

// At this point, we've failed to add the pipe to the creation block.
throw new Error(`AssertionError: unable to find insertion point for pipe ${binding.name}`);
}
Loading