From 49f074f61dc4967027e129d3e47c2d2f8fe9909f Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Wed, 14 Feb 2018 17:12:05 -0800 Subject: [PATCH] feat(ivy): support array and object literals in binding expressions (#22336) PR Close #22336 --- .../src/compiler_util/expression_converter.ts | 6 +- packages/compiler/src/constant_pool.ts | 93 +++++- packages/compiler/src/output/output_ast.ts | 85 ++++++ .../compiler/src/render3/r3_identifiers.ts | 11 + .../compiler/src/render3/r3_view_compiler.ts | 59 +++- .../render3/r3_compiler_compliance_spec.ts | 267 ++++++++++++++++++ 6 files changed, 503 insertions(+), 18 deletions(-) diff --git a/packages/compiler/src/compiler_util/expression_converter.ts b/packages/compiler/src/compiler_util/expression_converter.ts index 0b942328e7..99fe8f6570 100644 --- a/packages/compiler/src/compiler_util/expression_converter.ts +++ b/packages/compiler/src/compiler_util/expression_converter.ts @@ -97,7 +97,7 @@ export enum BindingForm { General, // Try to generate a simple binding (no temporaries or statements) - // otherise generate a general binding + // otherwise generate a general binding TrySimple, } @@ -341,7 +341,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { } visitLiteralPrimitive(ast: cdAst.LiteralPrimitive, mode: _Mode): any { - // For literal values of null, undefined, true, or false allow type inteference + // For literal values of null, undefined, true, or false allow type interference // to infer the type. const type = ast.value === null || ast.value === undefined || ast.value === true || ast.value === true ? @@ -648,7 +648,7 @@ function convertStmtIntoExpression(stmt: o.Statement): o.Expression|null { return null; } -class BuiltinFunctionCall extends cdAst.FunctionCall { +export class BuiltinFunctionCall extends cdAst.FunctionCall { constructor(span: cdAst.ParseSpan, public args: cdAst.AST[], public converter: BuiltinConverter) { super(span, null, args); } diff --git a/packages/compiler/src/constant_pool.ts b/packages/compiler/src/constant_pool.ts index 00835f00b2..90b54d9a38 100644 --- a/packages/compiler/src/constant_pool.ts +++ b/packages/compiler/src/constant_pool.ts @@ -13,6 +13,14 @@ const CONSTANT_PREFIX = '_c'; export const enum DefinitionKind {Injector, Directive, Component, Pipe} +/** + * Context to use when producing a key. + * + * This ensures we see the constant not the reference variable when producing + * a key. + */ +const KEY_CONTEXT = {}; + /** * A node that is a place-holder that allows the node to be replaced when the actual * node is known. @@ -22,18 +30,31 @@ export const enum DefinitionKind {Injector, Directive, Component, Pipe} * change the referenced expression. */ class FixupExpression extends o.Expression { - constructor(public resolved: o.Expression) { super(resolved.type); } + private original: o.Expression; shared: boolean; + constructor(public resolved: o.Expression) { + super(resolved.type); + this.original = resolved; + } + visitExpression(visitor: o.ExpressionVisitor, context: any): any { - return this.resolved.visitExpression(visitor, context); + if (context === KEY_CONTEXT) { + // When producing a key we want to traverse the constant not the + // variable used to refer to it. + return this.original.visitExpression(visitor, context); + } else { + return this.resolved.visitExpression(visitor, context); + } } isEquivalent(e: o.Expression): boolean { return e instanceof FixupExpression && this.resolved.isEquivalent(e.resolved); } + isConstant() { return true; } + fixup(expression: o.Expression) { this.resolved = expression; this.shared = true; @@ -48,6 +69,7 @@ class FixupExpression extends o.Expression { export class ConstantPool { statements: o.Statement[] = []; private literals = new Map(); + private literalFactories = new Map(); private injectorDefinitions = new Map(); private directiveDefinitions = new Map(); private componentDefinitions = new Map(); @@ -56,6 +78,11 @@ export class ConstantPool { private nextNameIndex = 0; getConstLiteral(literal: o.Expression, forceShared?: boolean): o.Expression { + if (literal instanceof o.LiteralExpr || literal instanceof FixupExpression) { + // Do no put simple literals into the constant pool or try to produce a constant for a + // reference to a constant. + return literal; + } const key = this.keyOf(literal); let fixup = this.literals.get(key); let newValue = false; @@ -97,6 +124,54 @@ export class ConstantPool { return fixup; } + getLiteralFactory(literal: o.LiteralArrayExpr|o.LiteralMapExpr): + {literalFactory: o.Expression, literalFactoryArguments: o.Expression[]} { + // Create a pure function that builds an array of a mix of constant and variable expressions + if (literal instanceof o.LiteralArrayExpr) { + const argumentsForKey = literal.entries.map(e => e.isConstant() ? e : o.literal(null)); + const key = this.keyOf(o.literalArr(argumentsForKey)); + return this._getLiteralFactory(key, literal.entries, entries => o.literalArr(entries)); + } else { + const expressionForKey = o.literalMap( + literal.entries.map(e => ({ + key: e.key, + value: e.value.isConstant() ? e.value : o.literal(null), + quoted: e.quoted + }))); + const key = this.keyOf(expressionForKey); + return this._getLiteralFactory( + key, literal.entries.map(e => e.value), + entries => o.literalMap(entries.map((value, index) => ({ + key: literal.entries[index].key, + value, + quoted: literal.entries[index].quoted + })))); + } + } + + private _getLiteralFactory( + key: string, values: o.Expression[], resultMap: (parameters: o.Expression[]) => o.Expression): + {literalFactory: o.Expression, literalFactoryArguments: o.Expression[]} { + let literalFactory = this.literalFactories.get(key); + const literalFactoryArguments = values.filter((e => !e.isConstant())); + if (!literalFactory) { + const resultExpressions = values.map( + (e, index) => e.isConstant() ? this.getConstLiteral(e, true) : o.variable(`a${index}`)); + const parameters = + resultExpressions.filter(isVariable).map(e => new o.FnParam(e.name !, o.DYNAMIC_TYPE)); + const pureFunctionDeclaration = + o.fn(parameters, [new o.ReturnStatement(resultMap(resultExpressions))], o.INFERRED_TYPE); + const name = this.freshName(); + this.statements.push( + o.variable(name).set(pureFunctionDeclaration).toDeclStmt(o.INFERRED_TYPE, [ + o.StmtModifier.Final + ])); + literalFactory = o.variable(name); + this.literalFactories.set(key, literalFactory); + } + return {literalFactory, literalFactoryArguments}; + } + /** * Produce a unique name. * @@ -139,7 +214,7 @@ export class ConstantPool { private freshName(): string { return this.uniqueName(CONSTANT_PREFIX); } private keyOf(expression: o.Expression) { - return expression.visitExpression(new KeyVisitor(), null); + return expression.visitExpression(new KeyVisitor(), KEY_CONTEXT); } } @@ -147,13 +222,13 @@ class KeyVisitor implements o.ExpressionVisitor { visitLiteralExpr(ast: o.LiteralExpr): string { return `${typeof ast.value === 'string' ? '"' + ast.value + '"' : ast.value}`; } - visitLiteralArrayExpr(ast: o.LiteralArrayExpr): string { - return `[${ast.entries.map(entry => entry.visitExpression(this, null)).join(',')}]`; + visitLiteralArrayExpr(ast: o.LiteralArrayExpr, context: object): string { + return `[${ast.entries.map(entry => entry.visitExpression(this, context)).join(',')}]`; } - visitLiteralMapExpr(ast: o.LiteralMapExpr): string { + visitLiteralMapExpr(ast: o.LiteralMapExpr, context: object): string { const mapEntry = (entry: o.LiteralMapEntry) => - `${entry.key}:${entry.value.visitExpression(this, null)}`; + `${entry.key}:${entry.value.visitExpression(this, context)}`; return `{${ast.entries.map(mapEntry).join(',')}`; } @@ -184,3 +259,7 @@ function invalid(arg: o.Expression | o.Statement): never { throw new Error( `Invalid state: Visitor ${this.constructor.name} doesn't handle ${o.constructor.name}`); } + +function isVariable(e: o.Expression): e is o.ReadVarExpr { + return e instanceof o.ReadVarExpr; +} \ No newline at end of file diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index e0a672fbc9..f5b930dce6 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -144,6 +144,11 @@ export abstract class Expression { */ abstract isEquivalent(e: Expression): boolean; + /** + * Return true if the expression is constant. + */ + abstract isConstant(): boolean; + prop(name: string, sourceSpan?: ParseSourceSpan|null): ReadPropExpr { return new ReadPropExpr(this, name, null, sourceSpan); } @@ -250,10 +255,13 @@ export class ReadVarExpr extends Expression { this.builtin = name; } } + isEquivalent(e: Expression): boolean { return e instanceof ReadVarExpr && this.name === e.name && this.builtin === e.builtin; } + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitReadVarExpr(this, context); } @@ -274,10 +282,13 @@ export class WriteVarExpr extends Expression { super(type || value.type, sourceSpan); this.value = value; } + isEquivalent(e: Expression): boolean { return e instanceof WriteVarExpr && this.name === e.name && this.value.isEquivalent(e.value); } + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitWriteVarExpr(this, context); } @@ -296,10 +307,14 @@ export class WriteKeyExpr extends Expression { super(type || value.type, sourceSpan); this.value = value; } + isEquivalent(e: Expression): boolean { return e instanceof WriteKeyExpr && this.receiver.isEquivalent(e.receiver) && this.index.isEquivalent(e.index) && this.value.isEquivalent(e.value); } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitWriteKeyExpr(this, context); } @@ -314,10 +329,14 @@ export class WritePropExpr extends Expression { super(type || value.type, sourceSpan); this.value = value; } + isEquivalent(e: Expression): boolean { return e instanceof WritePropExpr && this.receiver.isEquivalent(e.receiver) && this.name === e.name && this.value.isEquivalent(e.value); } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitWritePropExpr(this, context); } @@ -344,10 +363,14 @@ export class InvokeMethodExpr extends Expression { this.builtin = method; } } + isEquivalent(e: Expression): boolean { return e instanceof InvokeMethodExpr && this.receiver.isEquivalent(e.receiver) && this.name === e.name && this.builtin === e.builtin && areAllEquivalent(this.args, e.args); } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitInvokeMethodExpr(this, context); } @@ -360,10 +383,14 @@ export class InvokeFunctionExpr extends Expression { sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { return e instanceof InvokeFunctionExpr && this.fn.isEquivalent(e.fn) && areAllEquivalent(this.args, e.args); } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitInvokeFunctionExpr(this, context); } @@ -376,10 +403,14 @@ export class InstantiateExpr extends Expression { sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { return e instanceof InstantiateExpr && this.classExpr.isEquivalent(e.classExpr) && areAllEquivalent(this.args, e.args); } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitInstantiateExpr(this, context); } @@ -392,9 +423,13 @@ export class LiteralExpr extends Expression { sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { return e instanceof LiteralExpr && this.value === e.value; } + + isConstant() { return true; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitLiteralExpr(this, context); } @@ -407,10 +442,14 @@ export class ExternalExpr extends Expression { sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { return e instanceof ExternalExpr && this.value.name === e.value.name && this.value.moduleName === e.value.moduleName && this.value.runtime === e.value.runtime; } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitExternalExpr(this, context); } @@ -424,16 +463,21 @@ export class ExternalReference { export class ConditionalExpr extends Expression { public trueCase: Expression; + constructor( public condition: Expression, trueCase: Expression, public falseCase: Expression|null = null, type?: Type|null, sourceSpan?: ParseSourceSpan|null) { super(type || trueCase.type, sourceSpan); this.trueCase = trueCase; } + isEquivalent(e: Expression): boolean { return e instanceof ConditionalExpr && this.condition.isEquivalent(e.condition) && this.trueCase.isEquivalent(e.trueCase) && nullSafeIsEquivalent(this.falseCase, e.falseCase); } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitConditionalExpr(this, context); } @@ -444,9 +488,13 @@ export class NotExpr extends Expression { constructor(public condition: Expression, sourceSpan?: ParseSourceSpan|null) { super(BOOL_TYPE, sourceSpan); } + isEquivalent(e: Expression): boolean { return e instanceof NotExpr && this.condition.isEquivalent(e.condition); } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitNotExpr(this, context); } @@ -456,9 +504,13 @@ export class AssertNotNull extends Expression { constructor(public condition: Expression, sourceSpan?: ParseSourceSpan|null) { super(condition.type, sourceSpan); } + isEquivalent(e: Expression): boolean { return e instanceof AssertNotNull && this.condition.isEquivalent(e.condition); } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitAssertNotNullExpr(this, context); } @@ -468,9 +520,13 @@ export class CastExpr extends Expression { constructor(public value: Expression, type?: Type|null, sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { return e instanceof CastExpr && this.value.isEquivalent(e.value); } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitCastExpr(this, context); } @@ -490,10 +546,14 @@ export class FunctionExpr extends Expression { sourceSpan?: ParseSourceSpan|null, public name?: string|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { return e instanceof FunctionExpr && areAllEquivalent(this.params, e.params) && areAllEquivalent(this.statements, e.statements); } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitFunctionExpr(this, context); } @@ -513,10 +573,14 @@ export class BinaryOperatorExpr extends Expression { super(type || lhs.type, sourceSpan); this.lhs = lhs; } + isEquivalent(e: Expression): boolean { return e instanceof BinaryOperatorExpr && this.operator === e.operator && this.lhs.isEquivalent(e.lhs) && this.rhs.isEquivalent(e.rhs); } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitBinaryOperatorExpr(this, context); } @@ -529,13 +593,18 @@ export class ReadPropExpr extends Expression { sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { return e instanceof ReadPropExpr && this.receiver.isEquivalent(e.receiver) && this.name === e.name; } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitReadPropExpr(this, context); } + set(value: Expression): WritePropExpr { return new WritePropExpr(this.receiver, this.name, value, null, this.sourceSpan); } @@ -548,13 +617,18 @@ export class ReadKeyExpr extends Expression { sourceSpan?: ParseSourceSpan|null) { super(type, sourceSpan); } + isEquivalent(e: Expression): boolean { return e instanceof ReadKeyExpr && this.receiver.isEquivalent(e.receiver) && this.index.isEquivalent(e.index); } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitReadKeyExpr(this, context); } + set(value: Expression): WriteKeyExpr { return new WriteKeyExpr(this.receiver, this.index, value, null, this.sourceSpan); } @@ -567,6 +641,9 @@ export class LiteralArrayExpr extends Expression { super(type, sourceSpan); this.entries = entries; } + + isConstant() { return this.entries.every(e => e.isConstant()); } + isEquivalent(e: Expression): boolean { return e instanceof LiteralArrayExpr && areAllEquivalent(this.entries, e.entries); } @@ -591,9 +668,13 @@ export class LiteralMapExpr extends Expression { this.valueType = type.valueType; } } + isEquivalent(e: Expression): boolean { return e instanceof LiteralMapExpr && areAllEquivalent(this.entries, e.entries); } + + isConstant() { return this.entries.every(e => e.value.isConstant()); } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitLiteralMapExpr(this, context); } @@ -603,9 +684,13 @@ export class CommaExpr extends Expression { constructor(public parts: Expression[], sourceSpan?: ParseSourceSpan|null) { super(parts[parts.length - 1].type, sourceSpan); } + isEquivalent(e: Expression): boolean { return e instanceof CommaExpr && areAllEquivalent(this.parts, e.parts); } + + isConstant() { return false; } + visitExpression(visitor: ExpressionVisitor, context: any): any { return visitor.visitCommaExpr(this, context); } diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 4fbe0e5954..253fdd1f63 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -64,6 +64,17 @@ export class Identifiers { static interpolation8: o.ExternalReference = {name: 'ɵi8', moduleName: CORE}; static interpolationV: o.ExternalReference = {name: 'ɵiV', moduleName: CORE}; + static pureFunction0: o.ExternalReference = {name: 'ɵf0', moduleName: CORE}; + static pureFunction1: o.ExternalReference = {name: 'ɵf1', moduleName: CORE}; + static pureFunction2: o.ExternalReference = {name: 'ɵf2', moduleName: CORE}; + static pureFunction3: o.ExternalReference = {name: 'ɵf3', moduleName: CORE}; + static pureFunction4: o.ExternalReference = {name: 'ɵf4', moduleName: CORE}; + static pureFunction5: o.ExternalReference = {name: 'ɵf5', moduleName: CORE}; + static pureFunction6: o.ExternalReference = {name: 'ɵf6', moduleName: CORE}; + static pureFunction7: o.ExternalReference = {name: 'ɵf7', moduleName: CORE}; + static pureFunction8: o.ExternalReference = {name: 'ɵf8', moduleName: CORE}; + static pureFunctionV: o.ExternalReference = {name: 'ɵfV', moduleName: CORE}; + static pipeBind1: o.ExternalReference = {name: 'ɵpb1', moduleName: CORE}; static pipeBind2: o.ExternalReference = {name: 'ɵpb2', moduleName: CORE}; static pipeBind3: o.ExternalReference = {name: 'ɵpb3', moduleName: CORE}; diff --git a/packages/compiler/src/render3/r3_view_compiler.ts b/packages/compiler/src/render3/r3_view_compiler.ts index f127880733..84df4f2319 100644 --- a/packages/compiler/src/render3/r3_view_compiler.ts +++ b/packages/compiler/src/render3/r3_view_compiler.ts @@ -8,9 +8,9 @@ import {CompileDirectiveMetadata, CompilePipeSummary, CompileTokenMetadata, CompileTypeMetadata, flatten, identifierName, rendererTypeName, tokenReference, viewClassName} from '../compile_metadata'; import {CompileReflector} from '../compile_reflector'; -import {BindingForm, BuiltinConverter, ConvertPropertyBindingResult, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter'; +import {BindingForm, BuiltinConverter, BuiltinFunctionCall, ConvertPropertyBindingResult, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter'; import {ConstantPool, DefinitionKind} from '../constant_pool'; -import {AST, AstMemoryEfficientTransformer, AstTransformer, BindingPipe, FunctionCall, ImplicitReceiver, LiteralPrimitive, MethodCall, ParseSpan, PropertyRead} from '../expression_parser/ast'; +import {AST, AstMemoryEfficientTransformer, AstTransformer, BindingPipe, FunctionCall, ImplicitReceiver, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, PropertyRead} from '../expression_parser/ast'; import {Identifiers} from '../identifiers'; import {LifecycleHooks} from '../lifecycle_reflector'; import * as o from '../output/output_ast'; @@ -22,6 +22,7 @@ import {OutputContext, error} from '../util'; import {Identifiers as R3} from './r3_identifiers'; + /** Name of the context parameter passed into a template function */ const CONTEXT_NAME = 'ctx'; @@ -217,6 +218,23 @@ function pipeBinding(args: o.Expression[]): o.ExternalReference { } } +const pureFunctionIdentifiers = [ + R3.pureFunction0, R3.pureFunction1, R3.pureFunction2, R3.pureFunction3, R3.pureFunction4, + R3.pureFunction5, R3.pureFunction6, R3.pureFunction7, R3.pureFunction8 +]; +function getLiteralFactory( + outputContext: OutputContext, literal: o.LiteralArrayExpr | o.LiteralMapExpr): o.Expression { + const {literalFactory, literalFactoryArguments} = + outputContext.constantPool.getLiteralFactory(literal); + 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]); +} + class BindingScope { private map = new Map(); private referenceNameIndex = 0; @@ -269,7 +287,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { private _postfix: o.Statement[] = []; private _contentProjections: Map; private _projectionDefinitionIndex = 0; - private _pipeConverter: PipeConverter; + private _valueConverter: ValueConverter; private unsupported = unsupported; private invalid = invalid; @@ -279,8 +297,8 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { private bindingScope: BindingScope, private level = 0, private ngContentSelectors: string[], private contextName: string|null, private templateName: string|null, private pipes: Map) { - this._pipeConverter = - new PipeConverter(() => this.allocateDataSlot(), (name, localName, slot, value) => { + this._valueConverter = new ValueConverter( + outputCtx, () => this.allocateDataSlot(), (name, localName, slot, value) => { bindingScope.set(localName, value); const pipe = pipes.get(name) !; pipe || error(`Could not find pipe ${name}`); @@ -634,7 +652,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { } private convertPropertyBinding(implicit: o.Expression, value: AST): o.Expression { - const pipesConvertedValue = value.visit(this._pipeConverter); + const pipesConvertedValue = value.visit(this._valueConverter); const convertedPropertyBinding = convertPropertyBinding( this, implicit, pipesConvertedValue, this.bindingContext(), BindingForm.TrySimple, interpolate); @@ -688,10 +706,10 @@ export function createFactory( o.INFERRED_TYPE, null, type.reference.name ? `${type.reference.name}_Factory` : null); } -class PipeConverter extends AstMemoryEfficientTransformer { +class ValueConverter extends AstMemoryEfficientTransformer { private pipeSlots = new Map(); constructor( - private allocateSlot: () => number, + private outputCtx: OutputContext, private allocateSlot: () => number, private definePipe: (name: string, localName: string, slot: number, value: o.Expression) => void) { super(); @@ -715,6 +733,31 @@ class PipeConverter extends AstMemoryEfficientTransformer { return new FunctionCall( ast.span, target, [new LiteralPrimitive(ast.span, slot), value, ...args]); } + + visitLiteralArray(ast: LiteralArray, context: any): AST { + return new BuiltinFunctionCall(ast.span, this.visitAll(ast.expressions), values => { + // If the literal has calculated (non-literal) elements transform it into + // 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.outputCtx.constantPool.getConstLiteral(literal, true) : + getLiteralFactory(this.outputCtx, literal); + }); + } + + visitLiteralMap(ast: LiteralMap, context: any): AST { + return new BuiltinFunctionCall(ast.span, this.visitAll(ast.values), values => { + // If the literal has calculated (non-literal) elements transform it into + // 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.literalMap(values.map( + (value, index) => ({key: ast.keys[index].key, value, quoted: ast.keys[index].quoted}))); + return values.every(a => a.isConstant()) ? + this.outputCtx.constantPool.getConstLiteral(literal, true) : + getLiteralFactory(this.outputCtx, literal); + }); + } } function invalid(arg: o.Expression | o.Statement | TemplateAst): never { diff --git a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts index 5935766ddf..f2149d2f48 100644 --- a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts +++ b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts @@ -209,6 +209,273 @@ describe('compiler compliance', () => { expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef'); }); + describe('value composition', () => { + + it('should support array literals', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, Input, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-comp', + template: \` +

{{ names[0] }}

+

{{ names[1] }}

+ \` + }) + export class MyComp { + @Input() names: string[]; + } + + @Component({ + selector: 'my-app', + template: \` + + \` + }) + export class MyApp { + customName = 'Bess'; + } + + @NgModule({declarations: [MyComp, MyApp]}) + export class MyModule { } + ` + } + }; + + const MyAppDeclaration = ` + const $e0_ff$ = ($v$: any) => { return ['Nancy', $v$]; }; + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyApp, + tag: 'my-app', + factory: function MyApp_Factory() { return new MyApp(); }, + template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) { + if (cm) { + $r3$.ɵE(0, MyComp); + $r3$.ɵe(); + } + $r3$.ɵp(0, 'names', $r3$.ɵb($r3$.ɵf1($e0_ff$, ctx.customName))); + MyComp.ngComponentDef.h(1, 0); + $r3$.ɵr(1, 0); + } + }); + `; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, MyAppDeclaration, 'Invalid array emit'); + }); + + it('should support 9+ bindings in array literals', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, Input, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-comp', + template: \` + {{ names[0] }} + {{ names[1] }} + {{ names[3] }} + {{ names[4] }} + {{ names[5] }} + {{ names[6] }} + {{ names[7] }} + {{ names[8] }} + {{ names[9] }} + {{ names[10] }} + {{ names[11] }} + \` + }) + export class MyComp { + @Input() names: string[]; + } + + @Component({ + selector: 'my-app', + template: \` + + + \` + }) + export class MyApp { + n0 = 'a'; + n1 = 'b'; + n2 = 'c'; + n3 = 'd'; + n4 = 'e'; + n5 = 'f'; + n6 = 'g'; + n7 = 'h'; + n8 = 'i'; + } + + @NgModule({declarations: [MyComp, MyApp]}) + export class MyModule {} + ` + } + }; + + const MyAppDefinition = ` + const $e0_ff$ = ($v0$: $any$, $v1$: $any$, $v2$: $any$, $v3$: $any$, $v4$: $any$, $v5$: $any$, $v6$: $any$, $v7$: $any$, $v8$: $any$) => { + return ['start-', $v0$, $v1$, $v2$, $v3$, $v4$, '-middle-', $v5$, $v6$, $v7$, $v8$, '-end']; + } + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyApp, + tag: 'my-app', + factory: function MyApp_Factory() { return new MyApp(); }, + template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) { + if (cm) { + $r3$.ɵE(0, MyComp); + $r3$.ɵe(); + } + $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))); + MyComp.ngComponentDef.h(1, 0); + $r3$.ɵr(1, 0); + } + }); + `; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, MyAppDefinition, 'Invalid array binding'); + }); + + it('should support object literals', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, Input, NgModule} from '@angular/core'; + + @Component({ + selector: 'object-comp', + template: \` +

{{ config['duration'] }}

+

{{ config.animation }}

+ \` + }) + export class ObjectComp { + @Input() config: {[key: string]: any}; + } + + @Component({ + selector: 'my-app', + template: \` + + \` + }) + export class MyApp { + name = 'slide'; + } + + @NgModule({declarations: [ObjectComp, MyApp]}) + export class MyModule {} + ` + } + }; + + const MyAppDefinition = ` + const $e0_ff$ = ($v$: any) => { return {'duration': 500, animation: $v$}; }; + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyApp, + tag: 'my-app', + factory: function MyApp_Factory() { return new MyApp(); }, + template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) { + if (cm) { + $r3$.ɵE(0, ObjectComp); + $r3$.ɵe(); + } + $r3$.ɵp(0, 'config', $r3$.ɵb($r3$.ɵf1($e0_ff$, ctx.name))); + ObjectComp.ngComponentDef.h(1, 0); + $r3$.ɵr(1, 0); + } + }); + `; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, MyAppDefinition, 'Invalid object literal binding'); + }); + + it('should support expressions nested deeply in object/array literals', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, Input, NgModule} from '@angular/core'; + + @Component({ + selector: 'nested-comp', + template: \` +

{{ config.animation }}

+

{{config.actions[0].opacity }}

+

{{config.actions[1].duration }}

+ \` + }) + export class NestedComp { + @Input() config: {[key: string]: any}; + } + + @Component({ + selector: 'my-app', + template: \` + + + \` + }) + export class MyApp { + name = 'slide'; + duration = 100; + } + + @NgModule({declarations: [NestedComp, MyApp]}) + export class MyModule {} + ` + } + }; + + const MyAppDefinition = ` + const $c0$ = {opacity: 0, duration: 0}; + const $e0_ff$ = ($v$: any) => { return {opacity: 1, duration: $v$}; }; + const $e0_ff_1$ = ($v$: any) => { return [$c0$, $v$]; }; + const $e0_ff_2$ = ($v1$: any, $v2$: any) => { return {animation: $v1$, actions: $v2$}; }; + … + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyApp, + tag: 'my-app', + factory: function MyApp_Factory() { return new MyApp(); }, + template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) { + if (cm) { + $r3$.ɵE(0, NestedComp); + $r3$.ɵe(); + } + $r3$.ɵp( + 0, 'config', + $r3$.ɵb($r3$.ɵf2( + $e0_ff_2$, ctx.name, $r3$.ɵf1($e0_ff_1$, $r3$.ɵf1($e0_ff$, ctx.duration))))); + NestedComp.ngComponentDef.h(1, 0); + $r3$.ɵr(1, 0); + } + }); + `; + + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, MyAppDefinition, 'Invalid array/object literal binding'); + }); + }); + it('should support content projection', () => { const files = { app: {