diff --git a/packages/compiler/src/template/pipeline/ir/src/enums.ts b/packages/compiler/src/template/pipeline/ir/src/enums.ts index 9cec6f50788..e7d0442cc6e 100644 --- a/packages/compiler/src/template/pipeline/ir/src/enums.ts +++ b/packages/compiler/src/template/pipeline/ir/src/enums.ts @@ -336,6 +336,16 @@ export enum ExpressionKind { ConditionalCase, } +export enum VariableFlags { + None = 0b0000, + + /** + * Always inline this variable, regardless of the number of times it's used. + * An `AlwaysInline` variable may not depend on context, because doing so may cause side effects + * that are illegal when multi-inlined. (The optimizer will enforce this constraint.) + */ + AlwaysInline = 0b0001, +} /** * Distinguishes between different kinds of `SemanticVariable`s. */ 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 07ee7aa93c6..306c0b6780b 100644 --- a/packages/compiler/src/template/pipeline/ir/src/ops/shared.ts +++ b/packages/compiler/src/template/pipeline/ir/src/ops/shared.ts @@ -7,7 +7,7 @@ */ import * as o from '../../../../../output/output_ast'; -import {OpKind} from '../enums'; +import {OpKind, VariableFlags} from '../enums'; import {Op, XrefId} from '../operations'; import {SemanticVariable} from '../variable'; @@ -68,18 +68,22 @@ export interface VariableOp> extends Op { * Expression representing the value of the variable. */ initializer: o.Expression; + + flags: VariableFlags; } /** * Create a `VariableOp`. */ export function createVariableOp>( - xref: XrefId, variable: SemanticVariable, initializer: o.Expression): VariableOp { + xref: XrefId, variable: SemanticVariable, initializer: o.Expression, + flags: VariableFlags): VariableOp { return { kind: OpKind.Variable, xref, variable, initializer, + flags, ...NEW_OP, }; } diff --git a/packages/compiler/src/template/pipeline/src/phases/generate_variables.ts b/packages/compiler/src/template/pipeline/src/phases/generate_variables.ts index 78b9ce6fe94..bc28a5f3ad4 100644 --- a/packages/compiler/src/template/pipeline/src/phases/generate_variables.ts +++ b/packages/compiler/src/template/pipeline/src/phases/generate_variables.ts @@ -177,7 +177,8 @@ function generateVariablesInScopeForView( // 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.job.allocateXrefId(), scope.viewContextVariable, new ir.NextContextExpr())); + view.job.allocateXrefId(), scope.viewContextVariable, new ir.NextContextExpr(), + ir.VariableFlags.None)); } // Add variables for all context variables available in this scope's view. @@ -187,13 +188,15 @@ function generateVariablesInScopeForView( const variable = value === ir.CTX_REF ? context : new o.ReadPropExpr(context, value); // Add the variable declaration. newOps.push(ir.createVariableOp( - view.job.allocateXrefId(), scope.contextVariables.get(name)!, variable)); + view.job.allocateXrefId(), scope.contextVariables.get(name)!, variable, + ir.VariableFlags.None)); } // Add variables for all local references declared for elements in this scope. for (const ref of scope.references) { newOps.push(ir.createVariableOp( - view.job.allocateXrefId(), ref.variable, new ir.ReferenceExpr(ref.targetId, ref.offset))); + view.job.allocateXrefId(), ref.variable, new ir.ReferenceExpr(ref.targetId, ref.offset), + ir.VariableFlags.None)); } if (scope.parent !== null) { diff --git a/packages/compiler/src/template/pipeline/src/phases/variable_optimization.ts b/packages/compiler/src/template/pipeline/src/phases/variable_optimization.ts index 7ce5acb1b20..6803346f199 100644 --- a/packages/compiler/src/template/pipeline/src/phases/variable_optimization.ts +++ b/packages/compiler/src/template/pipeline/src/phases/variable_optimization.ts @@ -30,6 +30,15 @@ import {CompilationJob} from '../compilation'; */ export function phaseVariableOptimization(job: CompilationJob): void { for (const unit of job.units) { + inlineAlwaysInlineVariables(unit.create); + inlineAlwaysInlineVariables(unit.update); + + for (const op of unit.create) { + if (op.kind === ir.OpKind.Listener) { + inlineAlwaysInlineVariables(op.handlerOps); + } + } + optimizeVariablesInOpList(unit.create, job.compatibility); optimizeVariablesInOpList(unit.update, job.compatibility); @@ -95,6 +104,33 @@ interface OpInfo { fences: Fence; } +function inlineAlwaysInlineVariables(ops: ir.OpList): void { + const vars = new Map>(); + for (const op of ops) { + if (op.kind === ir.OpKind.Variable) { + ir.visitExpressionsInOp(op, expr => { + if (ir.isIrExpression(expr) && fencesForIrExpression(expr) !== Fence.None) { + throw new Error(`AssertionError: A context-sensitive variable was marked AlwaysInline`); + } + }); + vars.set(op.xref, op); + } + + ir.transformExpressionsInOp(op, expr => { + if (expr instanceof ir.ReadVariableExpr && vars.has(expr.xref)) { + const varOp = vars.get(expr.xref)!; + // Inline by cloning, because we might inline into multiple places. + return varOp.initializer.clone(); + } + return expr; + }, ir.VisitorContextFlag.None); + } + + for (const op of vars.values()) { + ir.OpList.remove(op as ir.CreateOp | ir.UpdateOp); + } +} + /** * Process a list of operations and optimize variables within that list. */ @@ -174,10 +210,15 @@ function optimizeVariablesInOpList( // Next, inline any remaining variables with exactly one usage. const toInline: ir.XrefId[] = []; for (const [id, count] of varUsages) { + const decl = varDecls.get(id)!; + const varInfo = opMap.get(decl as ir.CreateOp | ir.UpdateOp)!; // We can inline variables that: - // - are used once + // - are used exactly once, and // - are not used remotely - if (count !== 1) { + // OR + // - are marked for always inlining + const isAlwaysInline = !!(decl.flags & ir.VariableFlags.AlwaysInline); + if (count !== 1 || isAlwaysInline) { // We can't inline this variable as it's used more than once. continue; } @@ -196,6 +237,12 @@ function optimizeVariablesInOpList( // no future operation will make inlining legal. const decl = varDecls.get(candidate)!; const varInfo = opMap.get(decl as ir.CreateOp | ir.UpdateOp)!; + const isAlwaysInline = !!(decl.flags & ir.VariableFlags.AlwaysInline); + + if (isAlwaysInline) { + throw new Error( + `AssertionError: Found an 'AlwaysInline' variable after the always inlining pass.`); + } // Scan operations following the variable declaration and look for the point where that variable // is used. There should only be one usage given the precondition above.