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:

committed by
Matias Niemelä

parent
83bb5d1922
commit
4f36340de7
@ -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};
|
||||
}
|
||||
|
@ -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
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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]
|
||||
|
Reference in New Issue
Block a user