From 86e00f67905c51c660699756dc05d583ae8ae34b Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 11 Sep 2021 21:37:16 -0400 Subject: [PATCH 1/2] Experiment: narrow falsy strings to '' and falsy numbers to 0 --- src/compiler/checker.ts | 50 +++++++++++++++++-- .../reference/classStaticBlock8.types | 2 +- .../controlFlowBinaryAndExpression.types | 2 +- .../controlFlowDoWhileStatement.types | 2 +- .../reference/controlFlowTruthiness.types | 14 +++--- tests/baselines/reference/expr.types | 4 +- .../logicalOrOperatorWithEveryType.types | 4 +- ...peGuardsInRightOperandOfOrOrOperator.types | 24 ++++----- 8 files changed, 71 insertions(+), 31 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 30d9ad9021327..798c005d1f8d1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -22560,6 +22560,28 @@ namespace ts { return filterType(type, t => (getTypeFacts(t) & include) !== 0); } + function truthyTypeFilter(type: Type, truthiness: boolean): Type | undefined { + if (!truthiness) { + if (type.flags & TypeFlags.BigInt) { + return zeroBigIntType; + } + else if (type.flags & TypeFlags.Boolean) { + return falseType; + } + else if (type.flags & TypeFlags.String) { + return emptyStringType; + } + else if (type.flags & TypeFlags.Number) { + return zeroType; + } + } + + const facts = getTypeFacts(type); + const include = truthiness ? TypeFacts.Truthy : TypeFacts.Falsy; + + return ((facts & include) === 0) ? undefined : type; + } + function getTypeWithDefault(type: Type, defaultExpression: Expression) { return defaultExpression ? getUnionType([getNonUndefinedType(type), getTypeOfExpression(defaultExpression)]) : @@ -22790,10 +22812,19 @@ namespace ts { return type.flags & TypeFlags.UnionOrIntersection ? every((type as UnionOrIntersectionType).types, f) : f(type); } - function filterType(type: Type, f: (t: Type) => boolean): Type { + // TODO: make this actually performant + function compactMap(array: T[], f: (item: T) => T | undefined) { + const result = compact(map(array, f)); + + return result.length === array.length && every(array, (item, i) => item === result[i]) + ? array + : result; + } + + function filterAndNarrowType(type: Type, narrow: (t: Type) => Type | undefined) { if (type.flags & TypeFlags.Union) { const types = (type as UnionType).types; - const filtered = filter(types, f); + const filtered = compactMap(types, narrow); if (filtered === types) { return type; } @@ -22806,7 +22837,7 @@ namespace ts { // Otherwise, if we have exactly one type left in the origin set, return that as the filtered type. // Otherwise, construct a new filtered origin type. const originTypes = (origin as UnionType).types; - const originFiltered = filter(originTypes, t => !!(t.flags & TypeFlags.Union) || f(t)); + const originFiltered = compactMap(originTypes, t=> !!(t.flags & TypeFlags.Union) ? t : narrow(t)); if (originTypes.length - originFiltered.length === types.length - filtered.length) { if (originFiltered.length === 1) { return originFiltered[0]; @@ -22816,7 +22847,16 @@ namespace ts { } return getUnionTypeFromSortedList(filtered, (type as UnionType).objectFlags, /*aliasSymbol*/ undefined, /*aliasTypeArguments*/ undefined, newOrigin); } - return type.flags & TypeFlags.Never || f(type) ? type : neverType; + + if (type.flags & TypeFlags.Never) { + return type; + } + + return narrow(type) || neverType; + } + + function filterType(type: Type, f: (t: Type) => boolean): Type { + return filterAndNarrowType(type, (t) => f(t) ? t : undefined); } function removeType(type: Type, targetType: Type) { @@ -23756,7 +23796,7 @@ namespace ts { function narrowTypeByTruthiness(type: Type, expr: Expression, assumeTrue: boolean): Type { if (isMatchingReference(reference, expr)) { return type.flags & TypeFlags.Unknown && assumeTrue ? nonNullUnknownType : - getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy); + filterAndNarrowType(type, t => truthyTypeFilter(t, assumeTrue)); } if (strictNullChecks && assumeTrue && optionalChainContainsReference(expr, reference)) { type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); diff --git a/tests/baselines/reference/classStaticBlock8.types b/tests/baselines/reference/classStaticBlock8.types index 7651bd003aa6e..3b7288dfe9739 100644 --- a/tests/baselines/reference/classStaticBlock8.types +++ b/tests/baselines/reference/classStaticBlock8.types @@ -111,7 +111,7 @@ function foo (v: number) { >4 : 4 } switch (v) { ->v : number +>v : 0 | 1 | 3 default: break; // valid } diff --git a/tests/baselines/reference/controlFlowBinaryAndExpression.types b/tests/baselines/reference/controlFlowBinaryAndExpression.types index 1e7f4e4484818..26694f89e94f8 100644 --- a/tests/baselines/reference/controlFlowBinaryAndExpression.types +++ b/tests/baselines/reference/controlFlowBinaryAndExpression.types @@ -17,7 +17,7 @@ let cond: boolean; >0 : 0 x; // string | number ->x : string | number +>x : number | "" x = ""; >x = "" : "" diff --git a/tests/baselines/reference/controlFlowDoWhileStatement.types b/tests/baselines/reference/controlFlowDoWhileStatement.types index 94431485fe545..d3539a229b355 100644 --- a/tests/baselines/reference/controlFlowDoWhileStatement.types +++ b/tests/baselines/reference/controlFlowDoWhileStatement.types @@ -102,7 +102,7 @@ function d() { >length : number x; // number ->x : number +>x : 0 } function e() { >e : () => void diff --git a/tests/baselines/reference/controlFlowTruthiness.types b/tests/baselines/reference/controlFlowTruthiness.types index 17f497267ee6e..87b8d14cc1b3d 100644 --- a/tests/baselines/reference/controlFlowTruthiness.types +++ b/tests/baselines/reference/controlFlowTruthiness.types @@ -18,7 +18,7 @@ function f1() { } else { x; // string | undefined ->x : string | undefined +>x : "" | undefined } } @@ -42,7 +42,7 @@ function f2() { } else { x; // string | undefined ->x : string | undefined +>x : "" | undefined } } @@ -63,7 +63,7 @@ function f3() { } else { x; // string | undefined ->x : string | undefined +>x : "" | undefined } } @@ -82,7 +82,7 @@ function f4() { >foo : () => string | undefined x; // string | undefined ->x : string | undefined +>x : "" | undefined } else { x; // string @@ -115,10 +115,10 @@ function f5() { } else { x; // string | undefined ->x : string | undefined +>x : "" | undefined y; // string | undefined ->y : string | undefined +>y : "" | undefined } } @@ -153,7 +153,7 @@ function f6() { >x : string | undefined y; // string | undefined ->y : string | undefined +>y : "" | undefined } } diff --git a/tests/baselines/reference/expr.types b/tests/baselines/reference/expr.types index eaa94113b310d..2840b36231a5b 100644 --- a/tests/baselines/reference/expr.types +++ b/tests/baselines/reference/expr.types @@ -208,7 +208,7 @@ function f() { n||n; >n||n : number >n : number ->n : number +>n : 0 n||e; >n||e : number @@ -238,7 +238,7 @@ function f() { s||s; >s||s : string >s : string ->s : string +>s : "" s||e; >s||e : string | E diff --git a/tests/baselines/reference/logicalOrOperatorWithEveryType.types b/tests/baselines/reference/logicalOrOperatorWithEveryType.types index e4b36c988bb56..fa187fb232b87 100644 --- a/tests/baselines/reference/logicalOrOperatorWithEveryType.types +++ b/tests/baselines/reference/logicalOrOperatorWithEveryType.types @@ -171,7 +171,7 @@ var rc3 = a3 || a3; // number || number is number >rc3 : number >a3 || a3 : number >a3 : number ->a3 : number +>a3 : 0 var rc4 = a4 || a3; // string || number is string | number >rc4 : string | number @@ -237,7 +237,7 @@ var rd4 = a4 || a4; // string || string is string >rd4 : string >a4 || a4 : string >a4 : string ->a4 : string +>a4 : "" var rd5 = a5 || a4; // void || string is void | string >rd5 : string diff --git a/tests/baselines/reference/typeGuardsInRightOperandOfOrOrOperator.types b/tests/baselines/reference/typeGuardsInRightOperandOfOrOrOperator.types index af10a3ff9dc8f..2835216ef467c 100644 --- a/tests/baselines/reference/typeGuardsInRightOperandOfOrOrOperator.types +++ b/tests/baselines/reference/typeGuardsInRightOperandOfOrOrOperator.types @@ -19,42 +19,42 @@ function foo(x: number | string) { >10 : 10 } function foo2(x: number | string) { ->foo2 : (x: number | string) => number | true +>foo2 : (x: number | string) => true | 0 | 10 >x : string | number // modify x in right hand operand return typeof x !== "string" || ((x = 10) || x); // string | number ->typeof x !== "string" || ((x = 10) || x) : number | true +>typeof x !== "string" || ((x = 10) || x) : true | 0 | 10 >typeof x !== "string" : boolean >typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" >x : string | number >"string" : "string" ->((x = 10) || x) : number ->(x = 10) || x : number +>((x = 10) || x) : 0 | 10 +>(x = 10) || x : 0 | 10 >(x = 10) : 10 >x = 10 : 10 >x : string | number >10 : 10 ->x : number +>x : 0 } function foo3(x: number | string) { ->foo3 : (x: number | string) => string | true +>foo3 : (x: number | string) => true | "" | "hello" >x : string | number // modify x in right hand operand with string type itself return typeof x !== "string" || ((x = "hello") || x); // string | number ->typeof x !== "string" || ((x = "hello") || x) : string | true +>typeof x !== "string" || ((x = "hello") || x) : true | "" | "hello" >typeof x !== "string" : boolean >typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" >x : string | number >"string" : "string" ->((x = "hello") || x) : string ->(x = "hello") || x : string +>((x = "hello") || x) : "" | "hello" +>(x = "hello") || x : "" | "hello" >(x = "hello") : "hello" >x = "hello" : "hello" >x : string | number >"hello" : "hello" ->x : string +>x : "" } function foo4(x: number | string | boolean) { >foo4 : (x: number | string | boolean) => boolean @@ -103,7 +103,7 @@ function foo5(x: number | string | boolean) { >typeof x === "number" // number | boolean || x : boolean >typeof x === "number" : boolean >typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" ->x : number | boolean +>x : 0 | boolean >"number" : "number" || x)); // boolean @@ -168,7 +168,7 @@ function foo7(x: number | string | boolean) { >typeof x === "number" // change value of x ? ((x = 10) && x.toString()) // number | boolean | string // do not change value : ((y = x) && x.toString()) : string >typeof x === "number" : boolean >typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" ->x : number | boolean +>x : 0 | boolean >"number" : "number" // change value of x From c6df9fc20844e8f1879eb8e9ae8a67b200cb7611 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 17 Oct 2021 21:22:14 -0400 Subject: [PATCH 2/2] Used more optimized compactMap --- src/compiler/checker.ts | 9 --------- src/compiler/core.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 798c005d1f8d1..f7a947abd8afd 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -22812,15 +22812,6 @@ namespace ts { return type.flags & TypeFlags.UnionOrIntersection ? every((type as UnionOrIntersectionType).types, f) : f(type); } - // TODO: make this actually performant - function compactMap(array: T[], f: (item: T) => T | undefined) { - const result = compact(map(array, f)); - - return result.length === array.length && every(array, (item, i) => item === result[i]) - ? array - : result; - } - function filterAndNarrowType(type: Type, narrow: (t: Type) => Type | undefined) { if (type.flags & TypeFlags.Union) { const types = (type as UnionType).types; diff --git a/src/compiler/core.ts b/src/compiler/core.ts index eb07a11cbac83..4dbf609bb7d78 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -399,6 +399,32 @@ namespace ts { return array; } + /** + * Maps, filters for truthiness, and avoids allocation if all elements map to themselves. + */ + export function compactMap(array: T[], f: (item: T) => T | undefined): T[] { + for (let i = 0; i < array.length; i++) { + const mapped = f(array[i]); + if (mapped && mapped === array[i]) { + continue; + } + + const sliced = array.slice(0, i); + const compacted = mapped ? [...sliced, mapped] : sliced; + + for (let j = i + 1; j < array.length; j++) { + const nextMapped = f(array[j]); + if (nextMapped) { + compacted.push(nextMapped); + } + } + + return compacted; + } + + return array; + } + /** * Flattens an array containing a mix of array or non-array elements. *