feat(ivy): add support for short-circuiting (#24039)

Short-circuitable expressions (using ternary & binary operators) could not use
the regular binding mechanism as it relies on the bindings being checked every
single time - the index is incremented as part of checking the bindings.

Then for pure function kind of bindings we use a different mechanism with a
fixed index. As such short circuiting a binding check does not mess with the
expected binding index.

Note that all pure function bindings are handled the same wether or not they
actually are short-circuitable. This allows to keep the compiler and compiled
code simple - and there is no runtime perf cost anyway.

PR Close #24039
This commit is contained in:
Victor Berchet
2018-05-21 15:59:25 -07:00
committed by Matias Niemelä
parent 83bb5d1922
commit 4f36340de7
13 changed files with 362 additions and 116 deletions

View File

@ -121,4 +121,7 @@ export class Identifiers {
static NgOnChangesFeature: o.ExternalReference = {name: 'ɵNgOnChangesFeature', moduleName: CORE};
static listener: o.ExternalReference = {name: 'ɵL', moduleName: CORE};
// Reserve slots for pure functions
static reserveSlots: o.ExternalReference = {name: 'ɵrS', moduleName: CORE};
}

View File

@ -57,6 +57,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Maps of placeholder to node indexes for each of the i18n section
private _phToNodeIdxes: {[phName: string]: number[]}[] = [{}];
// Number of slots to reserve for pureFunctions
private _pureFunctionSlots = 0;
constructor(
private constantPool: ConstantPool, private contextParameter: string,
parentBindingScope: BindingScope, private level = 0, private contextName: string|null,
@ -70,6 +73,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
});
this._valueConverter = new ValueConverter(
constantPool, () => this.allocateDataSlot(),
(numSlots: number): number => this._pureFunctionSlots += numSlots,
(name, localName, slot, value: o.ReadVarExpr) => {
const pipeType = pipeTypeByName.get(name);
if (pipeType) {
@ -139,6 +143,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
t.visitAll(this, nodes);
if (this._pureFunctionSlots > 0) {
this.instruction(
this._creationCode, null, R3.reserveSlots, o.literal(this._pureFunctionSlots));
}
const creationCode = this._creationCode.length > 0 ?
[o.ifStmt(
o.variable(RENDER_FLAGS).bitwiseAnd(o.literal(core.RenderFlags.Create), null, false),
@ -501,6 +510,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
class ValueConverter extends AstMemoryEfficientTransformer {
constructor(
private constantPool: ConstantPool, private allocateSlot: () => number,
private allocatePureFunctionSlots: (numSlots: number) => number,
private definePipe:
(name: string, localName: string, slot: number, value: o.Expression) => void) {
super();
@ -511,14 +521,20 @@ class ValueConverter extends AstMemoryEfficientTransformer {
// Allocate a slot to create the pipe
const slot = this.allocateSlot();
const slotPseudoLocal = `PIPE:${slot}`;
// Allocate one slot for the result plus one slot per pipe argument
const pureFunctionSlot = this.allocatePureFunctionSlots(2 + pipe.args.length);
const target = new PropertyRead(pipe.span, new ImplicitReceiver(pipe.span), slotPseudoLocal);
const bindingId = pipeBinding(pipe.args);
this.definePipe(pipe.name, slotPseudoLocal, slot, o.importExpr(bindingId));
const value = pipe.exp.visit(this);
const args = this.visitAll(pipe.args);
return new FunctionCall(
pipe.span, target, [new LiteralPrimitive(pipe.span, slot), value, ...args]);
return new FunctionCall(pipe.span, target, [
new LiteralPrimitive(pipe.span, slot),
new LiteralPrimitive(pipe.span, pureFunctionSlot),
value,
...args,
]);
}
visitLiteralArray(array: LiteralArray, context: any): AST {
@ -527,8 +543,9 @@ class ValueConverter extends AstMemoryEfficientTransformer {
// calls to literal factories that compose the literal and will cache intermediate
// values. Otherwise, just return an literal array that contains the values.
const literal = o.literalArr(values);
return values.every(a => a.isConstant()) ? this.constantPool.getConstLiteral(literal, true) :
getLiteralFactory(this.constantPool, literal);
return values.every(a => a.isConstant()) ?
this.constantPool.getConstLiteral(literal, true) :
getLiteralFactory(this.constantPool, literal, this.allocatePureFunctionSlots);
});
}
@ -539,14 +556,13 @@ class ValueConverter extends AstMemoryEfficientTransformer {
// values. Otherwise, just return an literal array that contains the values.
const literal = o.literalMap(values.map(
(value, index) => ({key: map.keys[index].key, value, quoted: map.keys[index].quoted})));
return values.every(a => a.isConstant()) ? this.constantPool.getConstLiteral(literal, true) :
getLiteralFactory(this.constantPool, literal);
return values.every(a => a.isConstant()) ?
this.constantPool.getConstLiteral(literal, true) :
getLiteralFactory(this.constantPool, literal, this.allocatePureFunctionSlots);
});
}
}
// Pipes always have at least one parameter, the value they operate on
const pipeBindingIdentifiers = [R3.pipeBind1, R3.pipeBind2, R3.pipeBind3, R3.pipeBind4];
@ -559,15 +575,20 @@ const pureFunctionIdentifiers = [
R3.pureFunction5, R3.pureFunction6, R3.pureFunction7, R3.pureFunction8
];
function getLiteralFactory(
constantPool: ConstantPool, literal: o.LiteralArrayExpr | o.LiteralMapExpr): o.Expression {
constantPool: ConstantPool, literal: o.LiteralArrayExpr | o.LiteralMapExpr,
allocateSlots: (numSlots: number) => number): o.Expression {
const {literalFactory, literalFactoryArguments} = constantPool.getLiteralFactory(literal);
// Allocate 1 slot for the result plus 1 per argument
const startSlot = allocateSlots(1 + literalFactoryArguments.length);
literalFactoryArguments.length > 0 || error(`Expected arguments to a literal factory function`);
let pureFunctionIdent =
pureFunctionIdentifiers[literalFactoryArguments.length] || R3.pureFunctionV;
// Literal factories are pure functions that only need to be re-invoked when the parameters
// change.
return o.importExpr(pureFunctionIdent).callFn([literalFactory, ...literalFactoryArguments]);
return o.importExpr(pureFunctionIdent).callFn([
o.literal(startSlot), literalFactory, ...literalFactoryArguments
]);
}
/**

View File

@ -107,6 +107,57 @@ describe('compiler compliance', () => {
expectEmit(result.source, template, 'Incorrect template');
});
it('should reserve slots for pure functions', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div
[ternary]="cond ? [a] : [0]"
[pipe]="value | pipe:1:2"
[and]="cond && [b]"
[or]="cond || [c]"
></div>\`
})
export class MyComponent {
id = 'one';
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }';
const template = `
template: function MyComponent_Template(rf: IDENT, ctx: IDENT) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵPp(1,'pipe');
$r3$.ɵe();
$r3$.ɵrS(10);
}
if (rf & 2) {
$r3$.ɵp(0, 'ternary', $r3$.ɵb((ctx.cond ? $r3$.ɵf1(2, _c0, ctx.a): _c1)));
$r3$.ɵp(0, 'pipe', $r3$.ɵb($r3$.ɵpb3(6, 1, ctx.value, 1, 2)));
$r3$.ɵp(0, 'and', $r3$.ɵb((ctx.cond && $r3$.ɵf1(4, _c0, ctx.b))));
$r3$.ɵp(0, 'or', $r3$.ɵb((ctx.cond || $r3$.ɵf1(6, _c0, ctx.c))));
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, factory, 'Incorrect factory');
expectEmit(result.source, template, 'Incorrect template');
});
it('should bind to class and style names', () => {
const files = {
app: {
@ -415,9 +466,10 @@ describe('compiler compliance', () => {
if (rf & 1) {
$r3$.ɵE(0, 'my-comp');
$r3$.ɵe();
$r3$.ɵrS(2);
}
if (rf & 2) {
$r3$.ɵp(0, 'names', $r3$.ɵb($r3$.ɵf1($e0_ff$, ctx.customName)));
$r3$.ɵp(0, 'names', $r3$.ɵb($r3$.ɵf1(2, $e0_ff$, ctx.customName)));
}
},
directives: [MyComp]
@ -494,11 +546,12 @@ describe('compiler compliance', () => {
if (rf & 1) {
$r3$.ɵE(0, 'my-comp');
$r3$.ɵe();
$r3$.ɵrS(10);
}
if (rf & 2) {
$r3$.ɵp(
0, 'names',
$r3$.ɵb($r3$.ɵfV($e0_ff$, ctx.n0, ctx.n1, ctx.n2, ctx.n3, ctx.n4, ctx.n5, ctx.n6, ctx.n7, ctx.n8)));
$r3$.ɵb($r3$.ɵfV(10, $e0_ff$, ctx.n0, ctx.n1, ctx.n2, ctx.n3, ctx.n4, ctx.n5, ctx.n6, ctx.n7, ctx.n8)));
}
},
directives: [MyComp]
@ -555,9 +608,10 @@ describe('compiler compliance', () => {
if (rf & 1) {
$r3$.ɵE(0, 'object-comp');
$r3$.ɵe();
$r3$.ɵrS(2);
}
if (rf & 2) {
$r3$.ɵp(0, 'config', $r3$.ɵb($r3$.ɵf1($e0_ff$, ctx.name)));
$r3$.ɵp(0, 'config', $r3$.ɵb($r3$.ɵf1(2, $e0_ff$, ctx.name)));
}
},
directives: [ObjectComp]
@ -620,12 +674,12 @@ describe('compiler compliance', () => {
if (rf & 1) {
$r3$.ɵE(0, 'nested-comp');
$r3$.ɵe();
$r3$.ɵrS(7);
}
if (rf & 2) {
$r3$.ɵp(
0, 'config',
$r3$.ɵb($r3$.ɵf2(
$e0_ff_2$, ctx.name, $r3$.ɵf1($e0_ff_1$, $r3$.ɵf1($e0_ff$, ctx.duration)))));
$r3$.ɵb($r3$.ɵf2(7, $e0_ff_2$, ctx.name, $r3$.ɵf1(4, $e0_ff_1$, $r3$.ɵf1(2, $e0_ff$, ctx.duration)))));
}
},
directives: [NestedComp]
@ -912,10 +966,11 @@ describe('compiler compliance', () => {
$r3$.ɵT(4);
$r3$.ɵPp(5, 'myPurePipe');
$r3$.ɵe();
$r3$.ɵrS(9);
}
if (rf & 2) {
$r3$.ɵt(0, $r3$.ɵi1('', $r3$.ɵpb2(1, $r3$.ɵpb2(2,ctx.name, ctx.size), ctx.size), ''));
$r3$.ɵt(4, $r3$.ɵi1('', $r3$.ɵpb2(5, ctx.name, ctx.size), ''));
$r3$.ɵt(0, $r3$.ɵi1('', $r3$.ɵpb2(1, 3, $r3$.ɵpb2(2, 6, ctx.name, ctx.size), ctx.size), ''));
$r3$.ɵt(4, $r3$.ɵi1('', $r3$.ɵpb2(5, 9, ctx.name, ctx.size), ''));
}
},
pipes: [MyPurePipe, MyPipe]