From 0c4c773fca2e950983e1b2bdff27914512e13d96 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 15 Aug 2023 14:27:30 +0200 Subject: [PATCH] refactor(compiler): generate switch block instructions (#51380) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the logic to generate the instructions for `switch` instructions. For the following block: ```html {#switch value()} {:case 0} case 0 {:case 1} case 1 {:case 2} case 2 {:default} default {/switch} ``` The compiler will produce the following output: ```ts function App_Template(rf, ctx) { if (rf & 1) { ɵɵtemplate(0, App_Case_0_Template, 1, 0); ɵɵtemplate(1, App_Case_1_Template, 1, 0); ɵɵtemplate(2, App_Case_2_Template, 1, 0); ɵɵtemplate(3, App_Case_3_Template, 1, 0); } if (rf & 2) { let App_contFlowTmp; ɵɵconditional(0, (App_contFlowTmp = ctx.value()) === 0 ? 0 : App_contFlowTmp === 1 ? 1 : App_contFlowTmp === 2 ? 2 : 3); } } ``` PR Close #51380 --- .../GOLDEN_PARTIAL.js | 230 ++++++++++++++++++ .../TEST_CASES.json | 89 +++++++ .../basic_switch.ts | 22 ++ .../basic_switch_template.js | 42 ++++ .../nested_switch.ts | 24 ++ .../nested_switch_template.js | 60 +++++ .../switch_with_pipe.ts | 27 ++ .../switch_with_pipe_template.js | 18 ++ .../switch_without_default.ts | 18 ++ .../switch_without_default_template.js | 35 +++ .../compiler/src/render3/r3_identifiers.ts | 2 + .../compiler/src/render3/view/template.ts | 87 ++++++- packages/core/src/render3/jit/environment.ts | 1 + 13 files changed, 653 insertions(+), 2 deletions(-) create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/GOLDEN_PARTIAL.js create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/TEST_CASES.json create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_switch.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_switch_template.js create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_switch.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_switch_template.js create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_with_pipe.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_with_pipe_template.js create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_without_default.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_without_default_template.js diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/GOLDEN_PARTIAL.js new file mode 100644 index 00000000000..5df9fa36496 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/GOLDEN_PARTIAL.js @@ -0,0 +1,230 @@ +/**************************************************************************************************** + * PARTIAL FILE: basic_switch.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyApp { + constructor() { + this.message = 'hello'; + } + value() { + return 1; + } +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: ` +
+ {{message}} + {#switch value()} + {:case 0} case 0 + {:case 1} case 1 + {:case 2} case 2 + {:default} default + {/switch} +
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + template: ` +
+ {{message}} + {#switch value()} + {:case 0} case 0 + {:case 1} case 1 + {:case 2} case 2 + {:default} default + {/switch} +
+ `, + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: basic_switch.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyApp { + message: string; + value(): number; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: switch_without_default.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyApp { + constructor() { + this.message = 'hello'; + this.value = () => 1; + } +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: ` +
+ {{message}} + {#switch value()} + {:case 0} case 0 + {:case 1} case 1 + {:case 2} case 2 + {/switch} +
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + template: ` +
+ {{message}} + {#switch value()} + {:case 0} case 0 + {:case 1} case 1 + {:case 2} case 2 + {/switch} +
+ `, + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: switch_without_default.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyApp { + message: string; + value: () => number; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: nested_switch.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyApp { + constructor() { + this.message = 'hello'; + this.value = () => 1; + this.nestedValue = () => 2; + } +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "ng-component", ngImport: i0, template: ` +
+ {{message}} + {#switch value()} + {:case 0} case 0 + {:case 1} + {#switch nestedValue()} + {:case 0} nested case 0 + {:case 1} nested case 1 + {:case 2} nested case 2 + {/switch} + {:case 2} case 2 + {/switch} +
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + template: ` +
+ {{message}} + {#switch value()} + {:case 0} case 0 + {:case 1} + {#switch nestedValue()} + {:case 0} nested case 0 + {:case 1} nested case 1 + {:case 2} nested case 2 + {/switch} + {:case 2} case 2 + {/switch} +
+ `, + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: nested_switch.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyApp { + message: string; + value: () => number; + nestedValue: () => number; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: switch_with_pipe.js + ****************************************************************************************************/ +import { Component, Pipe } from '@angular/core'; +import * as i0 from "@angular/core"; +export class TestPipe { + tranform(value) { + return value; + } +} +TestPipe.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); +TestPipe.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestPipe, isStandalone: true, name: "test" }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestPipe, decorators: [{ + type: Pipe, + args: [{ standalone: true, name: 'test' }] + }] }); +export class MyApp { + constructor() { + this.message = 'hello'; + this.value = () => 1; + } +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "ng-component", ngImport: i0, template: ` +
+ {{message}} + {#switch value() | test} + {:case 0} case 0 + {:case 1} case 1 + {:default} default + {/switch} +
+ `, isInline: true, dependencies: [{ kind: "pipe", type: TestPipe, name: "test" }] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + template: ` +
+ {{message}} + {#switch value() | test} + {:case 0} case 0 + {:case 1} case 1 + {:default} default + {/switch} +
+ `, + standalone: true, + imports: [TestPipe] + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: switch_with_pipe.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class TestPipe { + tranform(value: unknown): unknown; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵpipe: i0.ɵɵPipeDeclaration; +} +export declare class MyApp { + message: string; + value: () => number; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + 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 new file mode 100644 index 00000000000..3649a3f04b2 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/TEST_CASES.json @@ -0,0 +1,89 @@ +{ + "$schema": "../test_case_schema.json", + "cases": [ + { + "description": "should generate a basic switch block", + "angularCompilerOptions": { + "_enabledBlockTypes": ["switch"] + }, + "inputFiles": [ + "basic_switch.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "basic_switch_template.js", + "generated": "basic_switch.js" + } + ], + "failureMessage": "Incorrect template" + } + ], + "skipForTemplatePipeline": true + }, + { + "description": "should generate a switch block without a default block", + "angularCompilerOptions": { + "_enabledBlockTypes": ["switch"] + }, + "inputFiles": [ + "switch_without_default.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "switch_without_default_template.js", + "generated": "switch_without_default.js" + } + ], + "failureMessage": "Incorrect template" + } + ], + "skipForTemplatePipeline": true + }, + { + "description": "should generate nested switch blocks", + "angularCompilerOptions": { + "_enabledBlockTypes": ["switch"] + }, + "inputFiles": [ + "nested_switch.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "nested_switch_template.js", + "generated": "nested_switch.js" + } + ], + "failureMessage": "Incorrect template" + } + ], + "skipForTemplatePipeline": true + }, + { + "description": "should generate switch block with a pipe in its expression", + "angularCompilerOptions": { + "_enabledBlockTypes": ["switch"] + }, + "inputFiles": [ + "switch_with_pipe.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "switch_with_pipe_template.js", + "generated": "switch_with_pipe.js" + } + ], + "failureMessage": "Incorrect template" + } + ], + "skipForTemplatePipeline": true + } + ] +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_switch.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_switch.ts new file mode 100644 index 00000000000..55b22b05f88 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_switch.ts @@ -0,0 +1,22 @@ +import {Component} from '@angular/core'; + +@Component({ + template: ` +
+ {{message}} + {#switch value()} + {:case 0} case 0 + {:case 1} case 1 + {:case 2} case 2 + {:default} default + {/switch} +
+ `, +}) +export class MyApp { + message = 'hello'; + + value() { + return 1; + } +} 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 new file mode 100644 index 00000000000..307e4956efe --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/basic_switch_template.js @@ -0,0 +1,42 @@ +function MyApp_Case_2_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtext(0, " case 0 "); + } +} + +function MyApp_Case_3_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtext(0, " case 1 "); + } +} + +function MyApp_Case_4_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtext(0, " case 2 "); + } +} + +function MyApp_Case_5_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtext(0, " default "); + } +} +… +function MyApp_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵtext(1); + $r3$.ɵɵtemplate(2, MyApp_Case_2_Template, 1, 0); + $r3$.ɵɵtemplate(3, MyApp_Case_3_Template, 1, 0); + $r3$.ɵɵtemplate(4, MyApp_Case_4_Template, 1, 0); + $r3$.ɵɵtemplate(5, MyApp_Case_5_Template, 1, 0); + $r3$.ɵɵelementEnd(); + } + if (rf & 2) { + 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); + } +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_switch.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_switch.ts new file mode 100644 index 00000000000..92951b90ea0 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_switch.ts @@ -0,0 +1,24 @@ +import {Component} from '@angular/core'; + +@Component({ + template: ` +
+ {{message}} + {#switch value()} + {:case 0} case 0 + {:case 1} + {#switch nestedValue()} + {:case 0} nested case 0 + {:case 1} nested case 1 + {:case 2} nested case 2 + {/switch} + {:case 2} case 2 + {/switch} +
+ `, +}) +export class MyApp { + message = 'hello'; + value = () => 1; + nestedValue = () => 2; +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_switch_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_switch_template.js new file mode 100644 index 00000000000..f9038d29339 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/nested_switch_template.js @@ -0,0 +1,60 @@ +function MyApp_Case_2_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtext(0, " case 0 "); + } +} + +function MyApp_Case_3_Case_0_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtext(0, " nested case 0 "); + } +} + +function MyApp_Case_3_Case_1_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtext(0, " nested case 1 "); + } +} + +function MyApp_Case_3_Case_2_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtext(0, " nested case 2 "); + } +} + +function MyApp_Case_3_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtemplate(0, MyApp_Case_3_Case_0_Template, 1, 0); + $r3$.ɵɵtemplate(1, MyApp_Case_3_Case_1_Template, 1, 0); + $r3$.ɵɵtemplate(2, MyApp_Case_3_Case_2_Template, 1, 0); + } + if (rf & 2) { + const ctx_r1 = $r3$.ɵɵnextContext(); + let MyApp_Case_3_contFlowTmp; + $r3$.ɵɵconditional(0, (MyApp_Case_3_contFlowTmp = ctx_r1.nestedValue()) === 0 ? 0 : MyApp_Case_3_contFlowTmp === 1 ? 1 : MyApp_Case_3_contFlowTmp === 2 ? 2 : -1); + } +} + +function MyApp_Case_4_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtext(0, " case 2 "); + } +} +… +function MyApp_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵtext(1); + $r3$.ɵɵtemplate(2, MyApp_Case_2_Template, 1, 0); + $r3$.ɵɵtemplate(3, MyApp_Case_3_Template, 3, 4); + $r3$.ɵɵtemplate(4, MyApp_Case_4_Template, 1, 0); + $r3$.ɵɵelementEnd(); + } + if (rf & 2) { + 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 : -1); + } +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_with_pipe.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_with_pipe.ts new file mode 100644 index 00000000000..6397ddc1a4e --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_with_pipe.ts @@ -0,0 +1,27 @@ +import {Component, Pipe} from '@angular/core'; + +@Pipe({standalone: true, name: 'test'}) +export class TestPipe { + tranform(value: unknown) { + return value; + } +} + +@Component({ + template: ` +
+ {{message}} + {#switch value() | test} + {:case 0} case 0 + {:case 1} case 1 + {:default} default + {/switch} +
+ `, + standalone: true, + imports: [TestPipe] +}) +export class MyApp { + message = 'hello'; + value = () => 1; +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_with_pipe_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_with_pipe_template.js new file mode 100644 index 00000000000..6cb85e28ae5 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_with_pipe_template.js @@ -0,0 +1,18 @@ +function MyApp_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵtext(1); + $r3$.ɵɵpipe(2, "test"); + $r3$.ɵɵtemplate(3, MyApp_Case_3_Template, 1, 0); + $r3$.ɵɵtemplate(4, MyApp_Case_4_Template, 1, 0); + $r3$.ɵɵtemplate(5, MyApp_Case_5_Template, 1, 0); + $r3$.ɵɵelementEnd(); + } + if (rf & 2) { + let MyApp_contFlowTmp; + $r3$.ɵɵadvance(1); + $r3$.ɵɵtextInterpolate1(" ", ctx.message, " "); + $r3$.ɵɵadvance(2); + $r3$.ɵɵconditional(3, (MyApp_contFlowTmp = $r3$.ɵɵpipeBind1(2, 4, ctx.value())) === 0 ? 3 : MyApp_contFlowTmp === 1 ? 4 : 5); + } +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_without_default.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_without_default.ts new file mode 100644 index 00000000000..658528f9dbc --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_without_default.ts @@ -0,0 +1,18 @@ +import {Component} from '@angular/core'; + +@Component({ + template: ` +
+ {{message}} + {#switch value()} + {:case 0} case 0 + {:case 1} case 1 + {:case 2} case 2 + {/switch} +
+ `, +}) +export class MyApp { + message = 'hello'; + value = () => 1; +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_without_default_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_without_default_template.js new file mode 100644 index 00000000000..cb0d268165d --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_control_flow/switch_without_default_template.js @@ -0,0 +1,35 @@ +function MyApp_Case_2_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtext(0, " case 0 "); + } +} + +function MyApp_Case_3_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtext(0, " case 1 "); + } +} + +function MyApp_Case_4_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtext(0, " case 2 "); + } +} +… +function MyApp_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵtext(1); + $r3$.ɵɵtemplate(2, MyApp_Case_2_Template, 1, 0); + $r3$.ɵɵtemplate(3, MyApp_Case_3_Template, 1, 0); + $r3$.ɵɵtemplate(4, MyApp_Case_4_Template, 1, 0); + $r3$.ɵɵelementEnd(); + } + if (rf & 2) { + 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 : -1); + } +} diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index c4a5b081d08..e3e885b886e 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -161,6 +161,8 @@ export class Identifiers { static deferPrefetchOnViewport: o.ExternalReference = {name: 'ɵɵdeferPrefetchOnViewport', moduleName: CORE}; + static conditional: o.ExternalReference = {name: 'ɵɵconditional', moduleName: CORE}; + static text: o.ExternalReference = {name: 'ɵɵtext', moduleName: CORE}; static enableBindings: o.ExternalReference = {name: 'ɵɵenableBindings', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index e2c2f90106c..214ed10e303 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -178,6 +178,13 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver /** Temporary variable declarations generated from visiting pipes, literals, etc. */ private _tempVariables: o.Statement[] = []; + + /** + * Temporary variable used to store state between control flow instructions. + * Should be accessed via the `allocateControlFlowTempVariable` method. + */ + private _controlFlowTempVariable: o.ReadVarExpr|null = null; + /** * List of callbacks to build nested templates. Nested templates must not be visited until * after the parent template has finished visiting all of its nodes. This ensures that all @@ -1001,6 +1008,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver readonly visitDeferredBlockError = invalid; readonly visitDeferredBlockLoading = invalid; readonly visitDeferredBlockPlaceholder = invalid; + readonly visitSwitchBlockCase = invalid; visitBoundText(text: t.BoundText) { if (this.i18n) { @@ -1088,6 +1096,66 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver return null; } + visitSwitchBlock(block: t.SwitchBlock): void { + // Allocate slots for the primary block expression. + const blockExpression = block.expression.visit(this._valueConverter); + this.allocateBindingSlots(blockExpression); + + // We have to process the block in two steps: once here and again in the update instruction + // callback in order to generate the correct expressions when pipes or pure functions are used. + const caseData = block.cases.map(currentCase => { + const index = this.createEmbeddedTemplateFn( + null, currentCase.children, '_Case', currentCase.sourceSpan); + let expression: AST|null = null; + + if (currentCase.expression !== null) { + expression = currentCase.expression.visit(this._valueConverter); + this.allocateBindingSlots(expression); + } + + return {index, expression}; + }); + + // Use the index of the first block as the index for the entire container. + const containerIndex = caseData[0].index; + + this.updateInstructionWithAdvance(containerIndex, block.sourceSpan, R3.conditional, () => { + const generateCases = (caseIndex: number): o.Expression => { + // If we've gone beyond the last branch, return the special -1 + // value which means that no view will be rendered. + if (caseIndex > caseData.length - 1) { + return o.literal(-1); + } + + const {index, expression} = caseData[caseIndex]; + + // If the case has no expression, it means that it's the `default` case. + // Return its index and stop the recursion. Assumes that there's only one + // `default` condition and that it's defined last. + if (expression === null) { + return o.literal(index); + } + + // If this is the very first comparison, we need to assign the value of the primary + // expression as a part of the comparison so the remaining cases can reuse it. In practice + // this looks as follows: + // ``` + // let temp; + // conditional(1, (temp = ctx.foo) === 1 ? 1 : temp === 2 ? 2 : temp === 3 ? 3 : 4); + // ``` + const comparisonTarget = caseIndex === 0 ? + this.allocateControlFlowTempVariable().set( + this.convertPropertyBinding(blockExpression)) : + this.allocateControlFlowTempVariable(); + + return comparisonTarget.identical(this.convertPropertyBinding(expression)) + .conditional(o.literal(index), generateCases(caseIndex + 1)); + }; + + return [o.literal(containerIndex), generateCases(0)]; + }); + } + visitDeferredBlock(deferred: t.DeferredBlock): void { const {loading, placeholder, error, triggers, prefetchTriggers} = deferred; const primaryTemplateIndex = @@ -1233,8 +1301,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } // TODO: implement control flow instructions - visitSwitchBlock(block: t.SwitchBlock): void {} - visitSwitchBlockCase(block: t.SwitchBlockCase): void {} visitForLoopBlock(block: t.ForLoopBlock): void {} visitForLoopBlockEmpty(block: t.ForLoopBlockEmpty): void {} visitIfBlock(block: t.IfBlock): void {} @@ -1406,6 +1472,23 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver return args; } + /** + * Creates and returns a variable that can be used to + * store the state between control flow instructions. + */ + private allocateControlFlowTempVariable(): o.ReadVarExpr { + // Note: the assumption here is that we'll only need one temporary variable for all control + // flow instructions. It's expected that any instructions will overwrite it before passing it + // into the parameters. + if (this._controlFlowTempVariable === null) { + const name = `${this.contextName}_contFlowTmp`; + this._tempVariables.push(new o.DeclareVarStmt(name)); + this._controlFlowTempVariable = o.variable(name); + } + + return this._controlFlowTempVariable; + } + /** * Prepares all attribute expression values for the `TAttributes` array. * diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 5f5251f2618..4ad0d739874 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -138,6 +138,7 @@ export const angularCoreEnv: {[name: string]: Function} = 'ɵɵclassProp': r3.ɵɵclassProp, 'ɵɵadvance': r3.ɵɵadvance, 'ɵɵtemplate': r3.ɵɵtemplate, + 'ɵɵconditional': r3.ɵɵconditional, 'ɵɵdefer': r3.ɵɵdefer, 'ɵɵdeferWhen': r3.ɵɵdeferWhen, 'ɵɵdeferOnIdle': r3.ɵɵdeferOnIdle,