From ab0f9eeba5f4c602b9ab92cd5479d5fcab4109d5 Mon Sep 17 00:00:00 2001 From: Dylan Hunn Date: Sat, 26 Aug 2023 18:53:59 -0700 Subject: [PATCH] refactor(compiler): Implement `switch` blocks in template pipeline (#51518) `switch` blocks are part of the new control flow syntax. This commit adds support for processing them, and emitting the appropriate templates and conditional instructions. PR Close #51518 --- .../TEST_CASES.json | 53 +++++--- .../basic_switch_template.js | 4 +- .../src/template/pipeline/ir/src/enums.ts | 10 ++ .../template/pipeline/ir/src/expression.ts | 43 +++++- .../template/pipeline/ir/src/ops/update.ts | 64 ++++++++- .../src/template/pipeline/src/emit.ts | 2 + .../src/template/pipeline/src/ingest.ts | 126 ++++++++++-------- .../src/template/pipeline/src/instruction.ts | 24 ++-- .../pipeline/src/phases/conditionals.ts | 50 +++++++ .../src/template/pipeline/src/phases/reify.ts | 11 ++ 10 files changed, 299 insertions(+), 88 deletions(-) create mode 100644 packages/compiler/src/template/pipeline/src/phases/conditionals.ts diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/TEST_CASES.json index a25b70bb1db0c..873353687aafb 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/TEST_CASES.json @@ -4,7 +4,9 @@ { "description": "should generate a basic switch block", "angularCompilerOptions": { - "_enabledBlockTypes": ["switch"] + "_enabledBlockTypes": [ + "switch" + ] }, "inputFiles": [ "basic_switch.ts" @@ -19,13 +21,14 @@ ], "failureMessage": "Incorrect template" } - ], - "skipForTemplatePipeline": true + ] }, { "description": "should generate a switch block without a default block", "angularCompilerOptions": { - "_enabledBlockTypes": ["switch"] + "_enabledBlockTypes": [ + "switch" + ] }, "inputFiles": [ "switch_without_default.ts" @@ -46,7 +49,9 @@ { "description": "should generate nested switch blocks", "angularCompilerOptions": { - "_enabledBlockTypes": ["switch"] + "_enabledBlockTypes": [ + "switch" + ] }, "inputFiles": [ "nested_switch.ts" @@ -67,7 +72,9 @@ { "description": "should generate switch block with a pipe in its expression", "angularCompilerOptions": { - "_enabledBlockTypes": ["switch"] + "_enabledBlockTypes": [ + "switch" + ] }, "inputFiles": [ "switch_with_pipe.ts" @@ -88,7 +95,9 @@ { "description": "should generate a basic if block", "angularCompilerOptions": { - "_enabledBlockTypes": ["if"] + "_enabledBlockTypes": [ + "if" + ] }, "inputFiles": [ "basic_if.ts" @@ -109,7 +118,9 @@ { "description": "should generate a basic if/else block", "angularCompilerOptions": { - "_enabledBlockTypes": ["if"] + "_enabledBlockTypes": [ + "if" + ] }, "inputFiles": [ "basic_if_else.ts" @@ -130,7 +141,9 @@ { "description": "should generate a basic if/else if block", "angularCompilerOptions": { - "_enabledBlockTypes": ["if"] + "_enabledBlockTypes": [ + "if" + ] }, "inputFiles": [ "basic_if_else_if.ts" @@ -151,7 +164,9 @@ { "description": "should generate a nested if block", "angularCompilerOptions": { - "_enabledBlockTypes": ["if"] + "_enabledBlockTypes": [ + "if" + ] }, "inputFiles": [ "nested_if.ts" @@ -172,7 +187,9 @@ { "description": "should generate an if block using pipes in its conditions", "angularCompilerOptions": { - "_enabledBlockTypes": ["if"] + "_enabledBlockTypes": [ + "if" + ] }, "inputFiles": [ "if_with_pipe.ts" @@ -193,7 +210,9 @@ { "description": "should generate an if block with an aliased expression", "angularCompilerOptions": { - "_enabledBlockTypes": ["if"] + "_enabledBlockTypes": [ + "if" + ] }, "inputFiles": [ "if_with_alias.ts" @@ -214,7 +233,9 @@ { "description": "should expose the alias to nested conditional blocks", "angularCompilerOptions": { - "_enabledBlockTypes": ["if"] + "_enabledBlockTypes": [ + "if" + ] }, "inputFiles": [ "if_nested_alias.ts" @@ -235,7 +256,9 @@ { "description": "should expose the alias to nested event listeners", "angularCompilerOptions": { - "_enabledBlockTypes": ["if"] + "_enabledBlockTypes": [ + "if" + ] }, "inputFiles": [ "if_nested_alias_listeners.ts" @@ -254,4 +277,4 @@ "skipForTemplatePipeline": true } ] -} +} \ No newline at end of file diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_switch_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_switch_template.js index 307e4956efeb7..791f926c1e126 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_switch_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_switch_template.js @@ -33,10 +33,10 @@ function MyApp_Template(rf, ctx) { $r3$.ɵɵelementEnd(); } if (rf & 2) { - let MyApp_contFlowTmp; + let $MyApp_contFlowTmp$; $r3$.ɵɵadvance(1); $r3$.ɵɵtextInterpolate1(" ", ctx.message, " "); $r3$.ɵɵadvance(1); - $r3$.ɵɵconditional(2, (MyApp_contFlowTmp = ctx.value()) === 0 ? 2 : MyApp_contFlowTmp === 1 ? 3 : MyApp_contFlowTmp === 2 ? 4 : 5); + $r3$.ɵɵconditional(2, ($MyApp_contFlowTmp$ = ctx.value()) === 0 ? 2 : $MyApp_contFlowTmp$ === 1 ? 3 : $MyApp_contFlowTmp$ === 2 ? 4 : 5); } } diff --git a/packages/compiler/src/template/pipeline/ir/src/enums.ts b/packages/compiler/src/template/pipeline/ir/src/enums.ts index f5e07343177bd..8bd62caba52ad 100644 --- a/packages/compiler/src/template/pipeline/ir/src/enums.ts +++ b/packages/compiler/src/template/pipeline/ir/src/enums.ts @@ -69,6 +69,11 @@ export enum OpKind { */ DisableBindings, + /** + * An op to conditionally render a template. + */ + Conditional, + /** * An operation to re-enable binding, after it was previously disabled. */ @@ -276,6 +281,11 @@ export enum ExpressionKind { * An expression representing a sanitizer function. */ SanitizerExpr, + + /** + * An expression that will cause a literal slot index to be emitted. + */ + SlotLiteralExpr, } /** diff --git a/packages/compiler/src/template/pipeline/ir/src/expression.ts b/packages/compiler/src/template/pipeline/ir/src/expression.ts index 7a7077622acc5..3f3a28524c0a6 100644 --- a/packages/compiler/src/template/pipeline/ir/src/expression.ts +++ b/packages/compiler/src/template/pipeline/ir/src/expression.ts @@ -10,7 +10,7 @@ import * as o from '../../../../output/output_ast'; import type {ParseSourceSpan} from '../../../../parse_util'; import {ExpressionKind, OpKind, SanitizerFn} from './enums'; -import {ConsumesVarsTrait, UsesSlotIndex, UsesSlotIndexTrait, UsesVarOffset, UsesVarOffsetTrait} from './traits'; +import {ConsumesVarsTrait, TRAIT_USES_SLOT_INDEX, UsesSlotIndex, UsesSlotIndexTrait, UsesVarOffset, UsesVarOffsetTrait} from './traits'; import type {XrefId} from './operations'; import type {CreateOp} from './ops/create'; @@ -23,7 +23,7 @@ export type Expression = LexicalReadExpr|ReferenceExpr|ContextExpr|NextContextExpr|GetCurrentViewExpr|RestoreViewExpr| ResetViewExpr|ReadVariableExpr|PureFunctionExpr|PureFunctionParameterExpr|PipeBindingExpr| PipeBindingVariadicExpr|SafePropertyReadExpr|SafeKeyedReadExpr|SafeInvokeFunctionExpr|EmptyExpr| - AssignTemporaryExpr|ReadTemporaryExpr|SanitizerExpr; + AssignTemporaryExpr|ReadTemporaryExpr|SanitizerExpr|SlotLiteralExpr; /** * Transformer type which converts expressions into general `o.Expression`s (which may be an @@ -734,6 +734,33 @@ export class SanitizerExpr extends ExpressionBase { override transformInternalExpressions(): void {} } +export class SlotLiteralExpr extends ExpressionBase { + override readonly kind = ExpressionKind.SlotLiteralExpr; + readonly[UsesSlotIndex] = true; + + constructor(readonly target: XrefId) { + super(); + } + + slot: number|null = null; + + override visitExpression(visitor: o.ExpressionVisitor, context: any): any {} + + override isEquivalent(e: Expression): boolean { + return e instanceof SlotLiteralExpr && e.target === this.target && e.slot === this.slot; + } + + override isConstant() { + return true; + } + + override clone(): SlotLiteralExpr { + return new SlotLiteralExpr(this.target); + } + + override transformInternalExpressions(): void {} +} + /** * Visits all `Expression`s in the AST of `op` with the `visitor` function. */ @@ -798,6 +825,18 @@ export function transformExpressionsInOp( case OpKind.Variable: op.initializer = transformExpressionsInExpression(op.initializer, transform, flags); break; + case OpKind.Conditional: + for (const condition of op.conditions) { + if (condition[1] === null) { + // This is a default case. + continue; + } + condition[1] = transformExpressionsInExpression(condition[1]!, transform, flags); + } + if (op.processed !== null) { + op.processed = transformExpressionsInExpression(op.processed, transform, flags); + } + break; case OpKind.Listener: for (const innerOp of op.handlerOps) { transformExpressionsInOp(innerOp, transform, flags | VisitorContextFlag.InChildOperation); 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 736e302199c5b..8d31368aaa5af 100644 --- a/packages/compiler/src/template/pipeline/ir/src/ops/update.ts +++ b/packages/compiler/src/template/pipeline/ir/src/ops/update.ts @@ -11,7 +11,7 @@ import * as o from '../../../../../output/output_ast'; import {ParseSourceSpan} from '../../../../../parse_util'; import {BindingKind, OpKind} from '../enums'; import {Op, XrefId} from '../operations'; -import {ConsumesVarsTrait, DependsOnSlotContextOpTrait, TRAIT_CONSUMES_VARS, TRAIT_DEPENDS_ON_SLOT_CONTEXT} from '../traits'; +import {ConsumesVarsTrait, DependsOnSlotContextOpTrait, TRAIT_CONSUMES_VARS, TRAIT_DEPENDS_ON_SLOT_CONTEXT, TRAIT_USES_SLOT_INDEX, UsesSlotIndexTrait} from '../traits'; import type {HostPropertyOp} from './host'; import {ListEndOp, NEW_OP, StatementOp, VariableOp} from './shared'; @@ -20,9 +20,9 @@ import {ListEndOp, NEW_OP, StatementOp, VariableOp} from './shared'; /** * An operation usable on the update side of the IR. */ -export type UpdateOp = - ListEndOp|StatementOp|PropertyOp|AttributeOp|StylePropOp|ClassPropOp| - StyleMapOp|ClassMapOp|InterpolateTextOp|AdvanceOp|VariableOp|BindingOp|HostPropertyOp; +export type UpdateOp = ListEndOp|StatementOp|PropertyOp|AttributeOp|StylePropOp| + ClassPropOp|StyleMapOp|ClassMapOp|InterpolateTextOp|AdvanceOp|VariableOp|BindingOp| + HostPropertyOp|ConditionalOp; /** * A logical operation to perform string interpolation on a text node. @@ -457,3 +457,59 @@ export function createAdvanceOp(delta: number, sourceSpan: ParseSourceSpan): Adv ...NEW_OP, }; } + +/** + * Logical operation representing a conditional expression in the update IR. + */ +export interface ConditionalOp extends Op, DependsOnSlotContextOpTrait, + UsesSlotIndexTrait { + kind: OpKind.Conditional; + + /** + * The insertion point, which is the first template in the creation block belonging to this + * condition. + */ + target: XrefId; + + /** + * The slot of the target, to be populated during slot allocation. + */ + slot: number|null; + + /** + * The main test expression. + */ + test: o.Expression; + + /** + * Each possible embedded view that could be displayed has a condition (or is default). This + * structure maps each view xref to its corresponding condition. + */ + conditions: Array<[XrefId, o.Expression|null]>; + + /** + * After processing, this will be a single collapsed Joost-expression that evaluates to the right + * slot.. + */ + processed: o.Expression|null; + + sourceSpan: ParseSourceSpan; +} + +/** + * Create a conditional op, which will display an embedded view according to a condtion. + */ +export function createConditionalOp( + target: XrefId, test: o.Expression, sourceSpan: ParseSourceSpan): ConditionalOp { + return { + kind: OpKind.Conditional, + target, + test, + conditions: [], + processed: null, + sourceSpan, + ...NEW_OP, + ...TRAIT_USES_SLOT_INDEX, + ...TRAIT_DEPENDS_ON_SLOT_CONTEXT, + }; +} diff --git a/packages/compiler/src/template/pipeline/src/emit.ts b/packages/compiler/src/template/pipeline/src/emit.ts index 88053c8e1ee39..0f25dc8c5aa68 100644 --- a/packages/compiler/src/template/pipeline/src/emit.ts +++ b/packages/compiler/src/template/pipeline/src/emit.ts @@ -17,6 +17,7 @@ import {phaseFindAnyCasts} from './phases/any_cast'; import {phaseAttributeExtraction} from './phases/attribute_extraction'; import {phaseBindingSpecialization} from './phases/binding_specialization'; import {phaseChaining} from './phases/chaining'; +import {phaseConditionals} from './phases/conditionals'; import {phaseConstCollection} from './phases/const_collection'; import {phaseEmptyElements} from './phases/empty_elements'; import {phaseExpandSafeReads} from './phases/expand_safe_reads'; @@ -75,6 +76,7 @@ const phases: Phase[] = [ {kind: Kind.Both, fn: phaseAttributeExtraction}, {kind: Kind.Both, fn: phaseParseExtractedStyles}, {kind: Kind.Tmpl, fn: phaseRemoveEmptyBindings}, + {kind: Kind.Tmpl, fn: phaseConditionals}, {kind: Kind.Tmpl, fn: phaseNoListenersOnTemplates}, {kind: Kind.Tmpl, fn: phasePipeCreation}, {kind: Kind.Tmpl, fn: phasePipeVariadic}, diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts index c8c40676f28c5..5c66f4b2b4ce5 100644 --- a/packages/compiler/src/template/pipeline/src/ingest.ts +++ b/packages/compiler/src/template/pipeline/src/ingest.ts @@ -109,16 +109,18 @@ export function ingestHostEvent(job: HostBindingCompilationJob, event: e.ParsedE /** * Ingest the nodes of a template AST into the given `ViewCompilation`. */ -function ingestNodes(view: ViewCompilationUnit, template: t.Node[]): void { +function ingestNodes(unit: ViewCompilationUnit, template: t.Node[]): void { for (const node of template) { if (node instanceof t.Element) { - ingestElement(view, node); + ingestElement(unit, node); } else if (node instanceof t.Template) { - ingestTemplate(view, node); + ingestTemplate(unit, node); } else if (node instanceof t.Text) { - ingestText(view, node); + ingestText(unit, node); } else if (node instanceof t.BoundText) { - ingestBoundText(view, node); + ingestBoundText(unit, node); + } else if (node instanceof t.SwitchBlock) { + ingestSwitchBlock(unit, node); } else { throw new Error(`Unsupported template node: ${node.constructor.name}`); } @@ -130,31 +132,31 @@ function ingestNodes(view: ViewCompilationUnit, template: t.Node[]): void { /** * Ingest an element AST from the template into the given `ViewCompilation`. */ -function ingestElement(view: ViewCompilationUnit, element: t.Element): void { +function ingestElement(unit: ViewCompilationUnit, element: t.Element): void { const staticAttributes: Record = {}; for (const attr of element.attributes) { staticAttributes[attr.name] = attr.value; } - const id = view.job.allocateXrefId(); + const id = unit.job.allocateXrefId(); const [namespaceKey, elementName] = splitNsName(element.name); const startOp = ir.createElementStartOp( elementName, id, namespaceForKey(namespaceKey), element.i18n, element.startSourceSpan); - view.create.push(startOp); + unit.create.push(startOp); - ingestBindings(view, startOp, element); + ingestBindings(unit, startOp, element); ingestReferences(startOp, element); - ingestNodes(view, element.children); + ingestNodes(unit, element.children); - view.create.push(ir.createElementEndOp(id, element.endSourceSpan)); + unit.create.push(ir.createElementEndOp(id, element.endSourceSpan)); } /** * Ingest an `ng-template` node from the AST into the given `ViewCompilation`. */ -function ingestTemplate(view: ViewCompilationUnit, tmpl: t.Template): void { - const childView = view.job.allocateView(view.xref); +function ingestTemplate(unit: ViewCompilationUnit, tmpl: t.Template): void { + const childView = unit.job.allocateView(unit.xref); let tagNameWithoutNamespace = tmpl.tagName; @@ -167,9 +169,9 @@ function ingestTemplate(view: ViewCompilationUnit, tmpl: t.Template): void { const tplOp = ir.createTemplateOp( childView.xref, tagNameWithoutNamespace ?? 'ng-template', namespaceForKey(namespacePrefix), tmpl.i18n, tmpl.startSourceSpan); - view.create.push(tplOp); + unit.create.push(tplOp); - ingestBindings(view, tplOp, tmpl); + ingestBindings(unit, tplOp, tmpl); ingestReferences(tplOp, tmpl); ingestNodes(childView, tmpl.children); @@ -181,14 +183,14 @@ function ingestTemplate(view: ViewCompilationUnit, tmpl: t.Template): void { /** * Ingest a literal text node from the AST into the given `ViewCompilation`. */ -function ingestText(view: ViewCompilationUnit, text: t.Text): void { - view.create.push(ir.createTextOp(view.job.allocateXrefId(), text.value, text.sourceSpan)); +function ingestText(unit: ViewCompilationUnit, text: t.Text): void { + unit.create.push(ir.createTextOp(unit.job.allocateXrefId(), text.value, text.sourceSpan)); } /** * Ingest an interpolated text node from the AST into the given `ViewCompilation`. */ -function ingestBoundText(view: ViewCompilationUnit, text: t.BoundText): void { +function ingestBoundText(unit: ViewCompilationUnit, text: t.BoundText): void { let value = text.value; if (value instanceof e.ASTWithSource) { value = value.ast; @@ -198,41 +200,61 @@ function ingestBoundText(view: ViewCompilationUnit, text: t.BoundText): void { `AssertionError: expected Interpolation for BoundText node, got ${value.constructor.name}`); } - const textXref = view.job.allocateXrefId(); - view.create.push(ir.createTextOp(textXref, '', text.sourceSpan)); - view.update.push(ir.createInterpolateTextOp( + const textXref = unit.job.allocateXrefId(); + unit.create.push(ir.createTextOp(textXref, '', text.sourceSpan)); + unit.update.push(ir.createInterpolateTextOp( textXref, new ir.Interpolation( - value.strings, value.expressions.map(expr => convertAst(expr, view.job))), + value.strings, value.expressions.map(expr => convertAst(expr, unit.job))), text.sourceSpan)); } +/** + * Ingest a `{#switch}` block into the given `ViewCompilation`. + */ +function ingestSwitchBlock(unit: ViewCompilationUnit, switchBlock: t.SwitchBlock): void { + let firstXref: ir.XrefId|null = null; + let conditions: Array<[ir.XrefId, o.Expression | null]> = []; + for (const switchCase of switchBlock.cases) { + const cView = unit.job.allocateView(unit.xref); + if (!firstXref) firstXref = cView.xref; + unit.create.push(ir.createTemplateOp(cView.xref, 'Case', ir.Namespace.HTML, undefined, null!)); + const caseExpr = switchCase.expression ? convertAst(switchCase.expression, unit.job) : null; + conditions.push([cView.xref, caseExpr]); + ingestNodes(cView, switchCase.children); + } + const conditional = + ir.createConditionalOp(firstXref!, convertAst(switchBlock.expression, unit.job), null!); + conditional.conditions = conditions; + unit.update.push(conditional); +} + /** * Convert a template AST expression into an output AST expression. */ -function convertAst(ast: e.AST, cpl: CompilationJob): o.Expression { +function convertAst(ast: e.AST, job: CompilationJob): o.Expression { if (ast instanceof e.ASTWithSource) { - return convertAst(ast.ast, cpl); + return convertAst(ast.ast, job); } else if (ast instanceof e.PropertyRead) { if (ast.receiver instanceof e.ImplicitReceiver && !(ast.receiver instanceof e.ThisReceiver)) { return new ir.LexicalReadExpr(ast.name); } else { - return new o.ReadPropExpr(convertAst(ast.receiver, cpl), ast.name); + return new o.ReadPropExpr(convertAst(ast.receiver, job), ast.name); } } else if (ast instanceof e.PropertyWrite) { - return new o.WritePropExpr(convertAst(ast.receiver, cpl), ast.name, convertAst(ast.value, cpl)); + return new o.WritePropExpr(convertAst(ast.receiver, job), ast.name, convertAst(ast.value, job)); } else if (ast instanceof e.KeyedWrite) { return new o.WriteKeyExpr( - convertAst(ast.receiver, cpl), - convertAst(ast.key, cpl), - convertAst(ast.value, cpl), + convertAst(ast.receiver, job), + convertAst(ast.key, job), + convertAst(ast.value, job), ); } else if (ast instanceof e.Call) { if (ast.receiver instanceof e.ImplicitReceiver) { throw new Error(`Unexpected ImplicitReceiver`); } else { return new o.InvokeFunctionExpr( - convertAst(ast.receiver, cpl), ast.args.map(arg => convertAst(arg, cpl))); + convertAst(ast.receiver, job), ast.args.map(arg => convertAst(arg, job))); } } else if (ast instanceof e.LiteralPrimitive) { return o.literal(ast.value); @@ -242,46 +264,46 @@ function convertAst(ast: e.AST, cpl: CompilationJob): o.Expression { throw new Error(`AssertionError: unknown binary operator ${ast.operation}`); } return new o.BinaryOperatorExpr( - operator, convertAst(ast.left, cpl), convertAst(ast.right, cpl)); + operator, convertAst(ast.left, job), convertAst(ast.right, job)); } else if (ast instanceof e.ThisReceiver) { - return new ir.ContextExpr(cpl.root.xref); + return new ir.ContextExpr(job.root.xref); } else if (ast instanceof e.KeyedRead) { - return new o.ReadKeyExpr(convertAst(ast.receiver, cpl), convertAst(ast.key, cpl)); + return new o.ReadKeyExpr(convertAst(ast.receiver, job), convertAst(ast.key, job)); } else if (ast instanceof e.Chain) { throw new Error(`AssertionError: Chain in unknown context`); } else if (ast instanceof e.LiteralMap) { const entries = ast.keys.map((key, idx) => { const value = ast.values[idx]; - return new o.LiteralMapEntry(key.key, convertAst(value, cpl), key.quoted); + return new o.LiteralMapEntry(key.key, convertAst(value, job), key.quoted); }); return new o.LiteralMapExpr(entries); } else if (ast instanceof e.LiteralArray) { - return new o.LiteralArrayExpr(ast.expressions.map(expr => convertAst(expr, cpl))); + return new o.LiteralArrayExpr(ast.expressions.map(expr => convertAst(expr, job))); } else if (ast instanceof e.Conditional) { return new o.ConditionalExpr( - convertAst(ast.condition, cpl), - convertAst(ast.trueExp, cpl), - convertAst(ast.falseExp, cpl), + convertAst(ast.condition, job), + convertAst(ast.trueExp, job), + convertAst(ast.falseExp, job), ); } else if (ast instanceof e.NonNullAssert) { // A non-null assertion shouldn't impact generated instructions, so we can just drop it. - return convertAst(ast.expression, cpl); + return convertAst(ast.expression, job); } else if (ast instanceof e.BindingPipe) { return new ir.PipeBindingExpr( - cpl.allocateXrefId(), + job.allocateXrefId(), ast.name, [ - convertAst(ast.exp, cpl), - ...ast.args.map(arg => convertAst(arg, cpl)), + convertAst(ast.exp, job), + ...ast.args.map(arg => convertAst(arg, job)), ], ); } else if (ast instanceof e.SafeKeyedRead) { - return new ir.SafeKeyedReadExpr(convertAst(ast.receiver, cpl), convertAst(ast.key, cpl)); + return new ir.SafeKeyedReadExpr(convertAst(ast.receiver, job), convertAst(ast.key, job)); } else if (ast instanceof e.SafePropertyRead) { - return new ir.SafePropertyReadExpr(convertAst(ast.receiver, cpl), ast.name); + return new ir.SafePropertyReadExpr(convertAst(ast.receiver, job), ast.name); } else if (ast instanceof e.SafeCall) { return new ir.SafeInvokeFunctionExpr( - convertAst(ast.receiver, cpl), ast.args.map(a => convertAst(a, cpl))); + convertAst(ast.receiver, job), ast.args.map(a => convertAst(a, job))); } else if (ast instanceof e.EmptyExpr) { return new ir.EmptyExpr(); } else { @@ -294,16 +316,16 @@ function convertAst(ast: e.AST, cpl: CompilationJob): o.Expression { * to their IR representation. */ function ingestBindings( - view: ViewCompilationUnit, op: ir.ElementOpBase, element: t.Element|t.Template): void { + unit: ViewCompilationUnit, op: ir.ElementOpBase, element: t.Element|t.Template): void { if (element instanceof t.Template) { for (const attr of element.templateAttrs) { if (attr instanceof t.TextAttribute) { ingestBinding( - view, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null, + unit, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null, SecurityContext.NONE, attr.sourceSpan, true, true); } else { ingestBinding( - view, op.xref, attr.name, attr.value, attr.type, attr.unit, attr.securityContext, + unit, op.xref, attr.name, attr.value, attr.type, attr.unit, attr.securityContext, attr.sourceSpan, false, true); } } @@ -314,13 +336,13 @@ function ingestBindings( // `[attr.foo]="bar"` or `attr.foo="{{bar}}"`, both of which will be handled as inputs with // `BindingType.Attribute`. ingestBinding( - view, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null, + unit, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null, SecurityContext.NONE, attr.sourceSpan, true, false); } for (const input of element.inputs) { ingestBinding( - view, op.xref, input.name, input.value, input.type, input.unit, input.securityContext, + unit, op.xref, input.name, input.value, input.type, input.unit, input.securityContext, input.sourceSpan, false, false); } @@ -351,7 +373,7 @@ function ingestBindings( throw new Error('Expected listener to have non-empty expression list.'); } - const expressions = inputExprs.map(expr => convertAst(expr, view.job)); + const expressions = inputExprs.map(expr => convertAst(expr, unit.job)); const returnExpr = expressions.pop()!; for (const expr of expressions) { @@ -359,7 +381,7 @@ function ingestBindings( listenerOp.handlerOps.push(stmtOp); } listenerOp.handlerOps.push(ir.createStatementOp(new o.ReturnStatement(returnExpr))); - view.create.push(listenerOp); + unit.create.push(listenerOp); } } diff --git a/packages/compiler/src/template/pipeline/src/instruction.ts b/packages/compiler/src/template/pipeline/src/instruction.ts index 13f63d10ea32f..1c912ebd293c9 100644 --- a/packages/compiler/src/template/pipeline/src/instruction.ts +++ b/packages/compiler/src/template/pipeline/src/instruction.ts @@ -72,19 +72,13 @@ export function elementContainerEnd(): ir.CreateOp { } export function template( - slot: number, templateFnRef: o.Expression, decls: number, vars: number, tag: string, - constIndex: number, sourceSpan: ParseSourceSpan): ir.CreateOp { - return call( - Identifiers.templateCreate, - [ - o.literal(slot), - templateFnRef, - o.literal(decls), - o.literal(vars), - o.literal(tag), - o.literal(constIndex), - ], - sourceSpan); + slot: number, templateFnRef: o.Expression, decls: number, vars: number, tag: string|null, + constIndex: number|null, sourceSpan: ParseSourceSpan): ir.CreateOp { + const args = [o.literal(slot), templateFnRef, o.literal(decls), o.literal(vars)]; + if (tag != null && constIndex != null) { + args.push(o.literal(tag), o.literal(constIndex)); + } + return call(Identifiers.templateCreate, args, sourceSpan); } export function disableBindings(): ir.CreateOp { @@ -388,6 +382,10 @@ function call( return ir.createStatementOp(new o.ExpressionStatement(expr, sourceSpan)) as OpT; } +export function conditional(slot: number, condition: o.Expression): ir.UpdateOp { + return call(Identifiers.conditional, [o.literal(slot), condition], null); +} + /** * Describes a specific flavor of instruction used to represent variadic instructions, which * have some number of variants for specific argument counts. diff --git a/packages/compiler/src/template/pipeline/src/phases/conditionals.ts b/packages/compiler/src/template/pipeline/src/phases/conditionals.ts new file mode 100644 index 0000000000000..4607d7a45efde --- /dev/null +++ b/packages/compiler/src/template/pipeline/src/phases/conditionals.ts @@ -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 o from '../../../../output/output_ast'; +import * as ir from '../../ir'; +import {ComponentCompilationJob} from '../compilation'; + +/** + * Collapse the various conditions of conditional ops into a single test expression. + */ +export function phaseConditionals(job: ComponentCompilationJob): void { + for (const unit of job.units) { + for (const op of unit.ops()) { + if (op.kind !== ir.OpKind.Conditional) { + continue; + } + + let test: o.Expression; + + // Any case with a `null` condition is `default`. If one exists, default to it instead. + const defaultCase = op.conditions.findIndex(([xref, cond]) => cond === null); + if (defaultCase >= 0) { + const [xref, cond] = op.conditions.splice(defaultCase, 1)[0]; + test = new ir.SlotLiteralExpr(xref); + } else { + // By default, a switch evaluates to `-1`, causing no template to be displayed. + test = o.literal(-1); + } + + // Switch expressions assign their main test to a temporary, to avoid re-executing it. + let tmp = new ir.AssignTemporaryExpr(op.test, job.allocateXrefId()); + + // For each remaining condition, test whether the temporary satifies the check. + for (let i = op.conditions.length - 1; i >= 0; i--) { + const useTmp = i === 0 ? tmp : new ir.ReadTemporaryExpr(tmp.xref); + const [xref, check] = op.conditions[i]; + const comparison = new o.BinaryOperatorExpr(o.BinaryOperator.Identical, useTmp, check!); + test = new o.ConditionalExpr(comparison, new ir.SlotLiteralExpr(xref), test); + } + + // Save the resulting aggregate Joost-expression. + op.processed = test; + } + } +} diff --git a/packages/compiler/src/template/pipeline/src/phases/reify.ts b/packages/compiler/src/template/pipeline/src/phases/reify.ts index 82134e1e586da..407dfdf61b807 100644 --- a/packages/compiler/src/template/pipeline/src/phases/reify.ts +++ b/packages/compiler/src/template/pipeline/src/phases/reify.ts @@ -240,6 +240,15 @@ function reifyUpdateOperations(_unit: CompilationUnit, ops: ir.OpList