Skip to content
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

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg 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
Copy link

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: of → if

@ahejlsberg
Copy link
Member Author

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

@ahejlsberg
Copy link
Member Author

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

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete comment and consider deleting the entire line.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, will fix.


declare function cond(): boolean;

function f1() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're fine with the short names.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

@sandersn sandersn Oct 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that makes sense.

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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo:determined

@ahejlsberg
Copy link
Member Author

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
Copy link
Member Author

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

# Conflicts:
#	src/compiler/checker.ts
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants