Skip to content
Please note that GitHub no longer supports Internet Explorer.

We recommend upgrading to the latest Microsoft Edge, Google Chrome, or Firefox.

Learn more
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control flow analysis for array construction #11432

Merged
merged 24 commits into from Oct 14, 2016
Merged

Control flow analysis for array construction #11432

merged 24 commits into from Oct 14, 2016

Conversation

@ahejlsberg
Copy link
Member

ahejlsberg commented Oct 6, 2016

This PR introduces control flow analysis for array construction patterns that originate in an empty array literal (x = []) followed by some number of x.push(value), x.unshift(value) or x[n] = value operations.

A let, const, or var variable declared with no type annotation and an initial value of [] is considered an implicit any[] variable. When an implicit any variable (see #11263) or implicit any[] variable is assigned an empty array literal [], each following x.push(value), x.unshift(value) or x[n] = value operation evolves the type of the variable in accordance with what elements are added to it.

function f1() {
    let x = [];
    x[0] = 5;
    x[1] = "hello";
    x[2] = true;
    return x;  // (string | number | boolean)[]
}

function f2() {
    let x = [];
    x.push(5);
    x.push("hello");
    x.push(true);
    return x;  // (string | number | boolean)[]
}

function f3() {
    let x = null;
    if (cond()) {
        x = [];
        while (cond()) {
            x.push("hello");
        }
    }
    return x;  // string[] | null
}

An array type is evolved only by operations on the variable in which the evolving array type originated. When an evolving array type variable is referenced in an expression, its type is "fixed" and cannot be further evolved.

function f4() {
    let x = [];       // x has evolving array type
    x.push(5);
    let y = x;        // y has fixed array type
    x.push("hello");  // Ok
    y.push("hello");  // Error
}

Similar to implicit any variables, control flow analysis is unable to determine the actual types of implicit any[] variables when they are referenced in nested functions. In such cases, the variables will appear to have type any[] in the nested functions and errors will be reported for the references if --noImplicitAny is enabled.

function f3() {
    let x = [];  // Error: Variable 'x' implicitly has type 'any[]' in some locations where its type cannot be determined.
    x.push(5);
    function g() {
        x;    // Error: Variable 'x' implicitly has an 'any[]' type.
    }
}
@zenmumbler

This comment has been minimized.

Copy link

zenmumbler commented Oct 11, 2016

Will this affect a problem (#11082) I submitted earlier? This sounds like its different enough from my problem but wanted to check if there is any overlap, thanks!

}
}
return result;
}

// Maps from T to T and avoids allocation of all elements map to themselves

This comment has been minimized.

Copy link
@sandersn

sandersn Oct 11, 2016

Member

typo: of → if

@ahejlsberg

This comment has been minimized.

Copy link
Member Author

ahejlsberg commented Oct 12, 2016

@zenmumbler No, this PR doesn't affect the issue in #11082.

@ahejlsberg

This comment has been minimized.

Copy link
Member Author

ahejlsberg commented Oct 12, 2016

@mhegazy Want to take a look before I merge this?

Copy link
Member

sandersn left a comment

Besides a few small changes, I also want to make sure we're OK with less localised errors in a series of functions with no return type annotations. In the example below, the error moves from the line with 'oops' on it to the last line.

function f() {
  let x = [];
  x.push(12);
  x.push('oops');
  return x;
}
function g() {
  let a = g();
  a.push('oops 2'); // shouldn't be allowed
  return a; // g now has type (string | number)[]
}
let sum = 0;
for (const n of g()) {
  sum += n; // error, can't add (string | number) to number
}

Right now, I think I'm on the side of fewer annotations, but I think we should know about the pitfalls of the new behaviour.



==== tests/cases/compiler/implicitAnyWidenToAny.ts (2 errors) ====
==== tests/cases/compiler/implicitAnyWidenToAny.ts (1 errors) ====
// these should be errors
var x = null; // error at "x"
var x1 = undefined; // error at "x1"
var widenArray = [null, undefined]; // error at "widenArray"
~~~~~~~~~~
!!! error TS7005: Variable 'widenArray' implicitly has an 'any[]' type.
var emptyArray = []; // error at "emptyArray"

This comment has been minimized.

Copy link
@sandersn

sandersn Oct 12, 2016

Member

Delete comment and consider deleting the entire line.

This comment has been minimized.

Copy link
@ahejlsberg

ahejlsberg Oct 13, 2016

Author Member

Yes, will fix.


declare function cond(): boolean;

function f1() {

This comment has been minimized.

Copy link
@sandersn

sandersn Oct 12, 2016

Member

It would be nice to have meaningful names for these test cases

This comment has been minimized.

Copy link
@ahejlsberg

ahejlsberg Oct 13, 2016

Author Member

I think we're fine with the short names.

x[1] = "hello";
x[2] = true;
return x; // (string | number | boolean)[]
}

This comment has been minimized.

Copy link
@sandersn

sandersn Oct 12, 2016

Member

what about a test case that case that does x[0] = 5; x[0] = "hello"; x[0] = true? It would still return (string | number | boolean)[], right?

This comment has been minimized.

Copy link
@ahejlsberg

ahejlsberg Oct 13, 2016

Author Member

Yes, you'd get the same type. We don't look at the value of the index expression, so nothing gained by an additional test.

@@ -1200,6 +1215,12 @@ namespace ts {
else {
forEachChild(node, bind);
}
if (node.expression.kind === SyntaxKind.PropertyAccessExpression) {
const propertyAccess = <PropertyAccessExpression>node.expression;
if (isNarrowableOperand(propertyAccess.expression) && propertyAccess.name.text === "push") {

This comment has been minimized.

Copy link
@sandersn

sandersn Oct 12, 2016

Member

what about unshift? I used unshift briefly when writing spread types. :)

This comment has been minimized.

Copy link
@ahejlsberg

ahejlsberg Oct 13, 2016

Author Member

Yup, we should support unshift, uncommon as it may be.

@@ -8391,6 +8404,11 @@ namespace ts {
getAssignedType(<Expression>node);
}

function isEmptyArrayAssignment(node: VariableDeclaration | BindingElement | Expression) {
return node.kind === SyntaxKind.VariableDeclaration && (<VariableDeclaration>node).initializer && isEmptyArrayLiteral((<VariableDeclaration>node).initializer) ||

This comment has been minimized.

Copy link
@sandersn

sandersn Oct 12, 2016

Member

can you break these lines up a bit more? They are really hard to read on github.

This comment has been minimized.

Copy link
@ahejlsberg

ahejlsberg Oct 13, 2016

Author Member

Sure.

@@ -8469,21 +8495,115 @@ namespace ts {
return incomplete ? { flags: 0, type } : type;
}

// An evolving array type tracks the element types that have so far been seen in an

This comment has been minimized.

Copy link
@sandersn

sandersn Oct 12, 2016

Member

comment should be jsdoc formatted

@@ -2748,6 +2756,8 @@ namespace ts {
export interface AnonymousType extends ObjectType {
target?: AnonymousType; // Instantiation target
mapper?: TypeMapper; // Instantiation mapper
elementType?: Type; // Element expressions of evolving array type

This comment has been minimized.

Copy link
@sandersn

sandersn Oct 12, 2016

Member

why isn't AutoArrayType a new subtype of AnonymousType? I think it would make it easier to track the property that auto array types don't escape the dynamic scope of getFlowTypeOfReference.

This comment has been minimized.

Copy link
@ahejlsberg

ahejlsberg Oct 13, 2016

Author Member

I don't follow. autoArrayType is the declared type of an auto-inferred any[] and it's already used outside of getFlowTypeOfReference.

This comment has been minimized.

Copy link
@sandersn

sandersn Oct 13, 2016

Member

It's separate from autoArrayType, whose type is actually TypeReference.

I'm talking about the comment at the top of the new code that says

Evolving array types are ultimately converted into manifest array types
and never escape the getFlowTypeOfReference function."

I was suggesting something like:

export interface AutoArrayType extends AnonymousType {
  elementType?: Type;
  finalArrayType?: Type;
}

And then having the new code that returns AnonymousType today return AutoArrayType instead.

This comment has been minimized.

Copy link
@ahejlsberg

ahejlsberg Oct 13, 2016

Author Member

Oh, I see. The issue with doing it that way is that we don't have a TypeFlags.EvolvingArrayType that would indicate an evolving array type (because we're out of flag bits). Instead, we distinguish by looking for the elementType property on AnonymousType, so it has to be part of AnonymousType.

This comment has been minimized.

Copy link
@sandersn

sandersn Oct 13, 2016

Member

Ok, that makes sense.

getUnionType(sameMap(types, finalizeEvolvingArrayType), subtypeReduction);
}

// Return true if the given node is 'x' in an 'x.push(value)' operation.

This comment has been minimized.

Copy link
@sandersn

sandersn Oct 12, 2016

Member

should support unshift here too

visitedFlowCount = visitedFlowStart;
if (reference.parent.kind === SyntaxKind.NonNullExpression && getTypeWithFacts(result, TypeFacts.NEUndefinedOrNull).flags & TypeFlags.Never) {
// When the reference is 'x' in an 'x.push(value)' or 'x[n] = value' operation, we give type
// 'any[]' to 'x' instead of using the type determed by control flow analysis such that new

This comment has been minimized.

Copy link
@sandersn

sandersn Oct 12, 2016

Member

typo:determined

@ahejlsberg

This comment has been minimized.

Copy link
Member Author

ahejlsberg commented Oct 13, 2016

With the latest commits, when an evolving array has no x.push(value), x.unshift(value) or x[n] = value operations in any control flow path leading to the current location, its type is implicitly any[]. Thus, any reference to such an empty evolving array will produce an error with --noImplicitAny:

function f1() {
    let a = [];
    if (cond()) {
        a.push("hello");
    }
    a;  // string[]
}

function f2() {
    let a = [];
    a;  // any[], error with --noImplicitAny
}

Note that references to the length property are always allowed since they don't depend on the element type of the array:

function f3() {
    let a = [];
    while (a.length < 10) {
        a.push("test");
    }
    return a;  // string[]
}
@ahejlsberg

This comment has been minimized.

Copy link
Member Author

ahejlsberg commented Oct 13, 2016

@mhegazy Latest commits should solve the issue we discussed yesterday.

# Conflicts:
#	src/compiler/checker.ts
@ahejlsberg ahejlsberg merged commit b5d1e4c into master Oct 14, 2016
1 check was pending
1 check was pending
continuous-integration/travis-ci/pr The Travis CI build is in progress
Details
@ahejlsberg ahejlsberg deleted the controlFlowArrays branch Oct 14, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
4 participants
You can’t perform that action at this time.