-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Simple control flow analysis #1287
Changes from 2 commits
3ec5eb1
d0da055
1d6bd0d
80cc8bb
147959f
20d0cd9
363a517
15e630c
68b135a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
/// <reference path="parser.ts"/> | ||
/// <reference path="binder.ts"/> | ||
/// <reference path="emitter.ts"/> | ||
/// <reference path="controlflow.ts"/> | ||
|
||
module ts { | ||
var nextSymbolId = 1; | ||
|
@@ -6111,7 +6112,8 @@ module ts { | |
|
||
function checkFunctionExpressionBody(node: FunctionExpression) { | ||
if (node.type) { | ||
checkIfNonVoidFunctionHasReturnExpressionsOrSingleThrowStatment(node, getTypeFromTypeNode(node.type)); | ||
checkControlFlowOfFunction(node, getTypeFromTypeNode(node.type) !== voidType, error); | ||
// checkIfNonVoidFunctionHasReturnExpressionsOrSingleThrowStatment(node, getTypeFromTypeNode(node.type)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove commented out lines. |
||
} | ||
if (node.body.kind === SyntaxKind.FunctionBlock) { | ||
checkSourceElement(node.body); | ||
|
@@ -7229,7 +7231,8 @@ module ts { | |
|
||
checkSourceElement(node.body); | ||
if (node.type && !isAccessor(node.kind)) { | ||
checkIfNonVoidFunctionHasReturnExpressionsOrSingleThrowStatment(node, getTypeFromTypeNode(node.type)); | ||
checkControlFlowOfFunction(node, getTypeFromTypeNode(node.type) !== voidType, error); | ||
// checkIfNonVoidFunctionHasReturnExpressionsOrSingleThrowStatment(node, getTypeFromTypeNode(node.type)); | ||
} | ||
|
||
// If there is no body and no explicit return type, then report an error. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,316 @@ | ||
/// <reference path="types.ts"/> | ||
|
||
module ts { | ||
export function checkControlFlowOfFunction(decl: FunctionLikeDeclaration, noImplicitReturns: boolean, error: (n: Node, message: DiagnosticMessage, arg0?: any) => void) { | ||
if (!decl.body || decl.body.kind !== SyntaxKind.FunctionBlock) { | ||
return; | ||
} | ||
|
||
var finalState = checkControlFlow(decl.body, error); | ||
if (noImplicitReturns && finalState === ControlFlowState.Reachable) { | ||
var errorNode: Node = decl.name || decl; | ||
error(errorNode, Diagnostics.Not_all_code_paths_return_a_value); | ||
} | ||
} | ||
|
||
export function checkControlFlowOfBlock(block: Block, error: (n: Node, message: DiagnosticMessage, arg0?: any) => void) { | ||
checkControlFlow(block, error); | ||
} | ||
|
||
function checkControlFlow(decl: Node, error: (n: Node, message: DiagnosticMessage, arg0?: any) => void): ControlFlowState { | ||
var currentState = ControlFlowState.Reachable; | ||
|
||
function setState(newState: ControlFlowState) { | ||
currentState = newState; | ||
} | ||
|
||
function or(s1: ControlFlowState, s2: ControlFlowState): ControlFlowState { | ||
if (s1 === ControlFlowState.Reachable || s2 === ControlFlowState.Reachable) { | ||
return ControlFlowState.Reachable; | ||
} | ||
if (s1 === ControlFlowState.ReportedUnreachable && s2 === ControlFlowState.ReportedUnreachable) { | ||
return ControlFlowState.ReportedUnreachable; | ||
} | ||
return ControlFlowState.Unreachable; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The latter one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then consider an assertion, but this looks fine. |
||
} | ||
|
||
function verifyReachable(n: Node): void { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. semantic of this function is: "check current state and report error if current state is unreachable" so I would say that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
if (currentState === ControlFlowState.Unreachable) { | ||
error(n, Diagnostics.Unreachable_code_detected); | ||
currentState = ControlFlowState.ReportedUnreachable; | ||
} | ||
} | ||
|
||
// label name -> index in 'labelStack' | ||
var labels: Map<number> = {}; | ||
// CF state at all seen labels | ||
var labelStack: ControlFlowState[] = []; | ||
// indices of implicit labels in 'labelStack' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure what this means. |
||
var implicitLabels: number[] = []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Explain what an implicit label is; right now I'm assuming it's the entry to point to something like a basic block. |
||
|
||
function pushNamedLabel(name: Identifier): boolean { | ||
if (hasProperty(labels, name.text)) { | ||
return false; | ||
} | ||
var newLen = labelStack.push(ControlFlowState.Uninitialized); | ||
labels[name.text] = newLen - 1; | ||
return true; | ||
} | ||
|
||
function pushImplicitLabel(): number { | ||
var newLen = labelStack.push(ControlFlowState.Uninitialized); | ||
implicitLabels.push(newLen - 1); | ||
return newLen - 1; | ||
} | ||
|
||
function setFinalStateAtLabel(mergedStates: ControlFlowState, outerState: ControlFlowState, name: Identifier): void { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leave a comment to explain that |
||
if (mergedStates === ControlFlowState.Uninitialized) { | ||
if (name) { | ||
error(name, Diagnostics.Unused_label); | ||
} | ||
setState(outerState); | ||
} | ||
else { | ||
setState(or(mergedStates, outerState)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you help explain why this is right? |
||
} | ||
} | ||
|
||
function popNamedLabel(name: Identifier, outerState: ControlFlowState): void { | ||
Debug.assert(hasProperty(labels, name.text)); | ||
var index = labels[name.text]; | ||
Debug.assert(labelStack.length === index + 1); | ||
labels[name.text] = undefined; | ||
var mergedStates = labelStack.pop(); | ||
setFinalStateAtLabel(mergedStates, outerState, name); | ||
} | ||
|
||
function popImplicitLabel(index: number, outerState: ControlFlowState): void { | ||
Debug.assert(labelStack.length === index + 1); | ||
var i = implicitLabels.pop(); | ||
Debug.assert(index === i); | ||
var mergedStates = labelStack.pop(); | ||
setFinalStateAtLabel(mergedStates, outerState, /*name*/ undefined); | ||
} | ||
|
||
function gotoLabel(label: Identifier, outerState: ControlFlowState): void { | ||
var stateIndex: number; | ||
if (label) { | ||
if (!hasProperty(labels, label.text)) { | ||
// reference to non-existing label | ||
return; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Either add a newline, or remove the one after the |
||
stateIndex = labels[label.text]; | ||
} | ||
else { | ||
if (implicitLabels.length === 0) { | ||
// non-labeled break\continue being used outside loops | ||
return; | ||
} | ||
|
||
stateIndex = implicitLabels[implicitLabels.length - 1]; | ||
} | ||
var stateAtLabel = labelStack[stateIndex]; | ||
labelStack[stateIndex] = stateAtLabel === ControlFlowState.Uninitialized ? outerState : or(outerState, stateAtLabel); | ||
} | ||
|
||
function checkWhileStatement(n: WhileStatement): void { | ||
verifyReachable(n); | ||
|
||
var preWhileState: ControlFlowState = n.expression.kind === SyntaxKind.FalseKeyword ? ControlFlowState.Unreachable : currentState; | ||
var postWhileState: ControlFlowState = n.expression.kind === SyntaxKind.TrueKeyword ? ControlFlowState.Unreachable : currentState; | ||
|
||
setState(preWhileState); | ||
|
||
var index = pushImplicitLabel(); | ||
check(n.statement); | ||
popImplicitLabel(index, postWhileState); | ||
} | ||
|
||
function checkDoStatement(n: DoStatement): void { | ||
verifyReachable(n); | ||
var preDoState = currentState; | ||
|
||
var index = pushImplicitLabel(); | ||
check(n.statement); | ||
|
||
var postDoState = n.expression.kind === SyntaxKind.TrueKeyword ? ControlFlowState.Unreachable : preDoState; | ||
popImplicitLabel(index, postDoState); | ||
} | ||
|
||
function checkForStatement(n: ForStatement): void { | ||
verifyReachable(n); | ||
|
||
var preForState = currentState; | ||
var index = pushImplicitLabel(); | ||
check(n.statement); | ||
var postForState = n.declarations || n.initializer || n.condition || n.iterator ? preForState : ControlFlowState.Unreachable; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, I can add it |
||
popImplicitLabel(index, postForState); | ||
} | ||
|
||
function checkForInStatement(n: ForInStatement): void { | ||
verifyReachable(n); | ||
var preForInState = currentState; | ||
var index = pushImplicitLabel(); | ||
check(n.statement); | ||
popImplicitLabel(index, preForInState); | ||
} | ||
|
||
function checkBlock(n: Block): void { | ||
forEach(n.statements, check); | ||
} | ||
|
||
function checkIfStatement(n: IfStatement): void { | ||
var ifTrueState: ControlFlowState = n.expression.kind === SyntaxKind.FalseKeyword ? ControlFlowState.Unreachable : currentState; | ||
var ifFalseState: ControlFlowState = n.expression.kind === SyntaxKind.TrueKeyword ? ControlFlowState.Unreachable : currentState; | ||
|
||
setState(ifTrueState); | ||
check(n.thenStatement); | ||
ifTrueState = currentState; | ||
|
||
setState(ifFalseState); | ||
check(n.elseStatement); | ||
|
||
currentState = or(currentState, ifTrueState); | ||
} | ||
|
||
function checkReturnOrThrow(n: Node): void { | ||
verifyReachable(n); | ||
setState(ControlFlowState.Unreachable); | ||
} | ||
|
||
function checkBreakOrContinueStatement(n: BreakOrContinueStatement): void { | ||
verifyReachable(n); | ||
if (n.kind === SyntaxKind.BreakStatement) { | ||
gotoLabel(n.label, currentState); | ||
} | ||
else { | ||
gotoLabel(n.label, ControlFlowState.Unreachable); // touch label so it will be marked a used | ||
} | ||
setState(ControlFlowState.Unreachable); | ||
} | ||
|
||
function checkTryStatement(n: TryStatement): void { | ||
verifyReachable(n); | ||
|
||
// catch\finally blocks has the same reachability as try block | ||
var startState = currentState; | ||
check(n.tryBlock); | ||
var postTryState = currentState; | ||
|
||
setState(startState); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there's some potential to be more accurate here, but I think we'd get diminishing returns. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In theory - yes, we might consider catch block unreachable if we can prove that try block either never throws or region that might throw is unreachable. In practice this will add a lot to the overall complexity and yield little value since it will capture a very small amount of very special cases. The same reasoning is applied to the initial reachability of finally blocks |
||
check(n.catchBlock); | ||
var postCatchState = currentState; | ||
|
||
setState(startState); | ||
check(n.finallyBlock); | ||
setState(or(postTryState, postCatchState)); | ||
} | ||
|
||
function checkSwitchStatement(n: SwitchStatement): void { | ||
verifyReachable(n); | ||
var startState = currentState; | ||
var hasDefault = false; | ||
|
||
var index = pushImplicitLabel(); | ||
|
||
forEach(n.clauses, (c: CaseOrDefaultClause) => { | ||
hasDefault = hasDefault || c.kind === SyntaxKind.DefaultClause; | ||
setState(startState); | ||
forEach(c.statements, check); | ||
if (c.statements.length && currentState === ControlFlowState.Reachable) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider c.statements.length > 0 |
||
error(c.expression, Diagnostics.Fallthrough_case_in_switch); | ||
} | ||
}); | ||
|
||
// post switch state is unreachable if switch is exaustive (has a default case ) and does not have fallthrough from the last case | ||
var postSwitchState = hasDefault && currentState !== ControlFlowState.Reachable ? ControlFlowState.Unreachable : startState; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about function f(x) {
switch (x) {
case 1:
case 2:
case 3:
break;
default:
return x;
}
return -x;
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will work correctly since break to a implicit label will make return statement reachable |
||
|
||
popImplicitLabel(index, postSwitchState); | ||
} | ||
|
||
function checkLabelledStatement(n: LabeledStatement): void { | ||
verifyReachable(n); | ||
var ok = pushNamedLabel(n.label); | ||
check(n.statement); | ||
if (ok) { | ||
popNamedLabel(n.label, currentState); | ||
} | ||
} | ||
|
||
function checkWithStatement(n: WithStatement): void { | ||
verifyReachable(n); | ||
check(n.statement); | ||
} | ||
|
||
// current assumption: only statements affect CF | ||
function check(n: Node): void { | ||
if (!n || currentState === ControlFlowState.ReportedUnreachable) { | ||
return; | ||
} | ||
switch (n.kind) { | ||
case SyntaxKind.WhileStatement: | ||
checkWhileStatement(<WhileStatement>n); | ||
break; | ||
case SyntaxKind.SourceFile: | ||
checkBlock(<SourceFile>n); | ||
break; | ||
case SyntaxKind.Block: | ||
case SyntaxKind.TryBlock: | ||
case SyntaxKind.CatchBlock: | ||
case SyntaxKind.FinallyBlock: | ||
case SyntaxKind.ModuleBlock: | ||
case SyntaxKind.FunctionBlock: | ||
checkBlock(<Block>n); | ||
break; | ||
case SyntaxKind.IfStatement: | ||
checkIfStatement(<IfStatement>n); | ||
break; | ||
case SyntaxKind.ReturnStatement: | ||
case SyntaxKind.ThrowStatement: | ||
checkReturnOrThrow(n); | ||
break; | ||
case SyntaxKind.BreakStatement: | ||
case SyntaxKind.ContinueStatement: | ||
checkBreakOrContinueStatement(<BreakOrContinueStatement>n); | ||
break; | ||
case SyntaxKind.VariableStatement: | ||
case SyntaxKind.EmptyStatement: | ||
case SyntaxKind.ExpressionStatement: | ||
case SyntaxKind.DebuggerStatement: | ||
verifyReachable(n); | ||
break; | ||
case SyntaxKind.DoStatement: | ||
checkDoStatement(<DoStatement>n); | ||
break; | ||
case SyntaxKind.ForInStatement: | ||
checkForInStatement(<ForInStatement>n); | ||
break; | ||
case SyntaxKind.ForStatement: | ||
checkForStatement(<ForStatement>n); | ||
break; | ||
case SyntaxKind.LabeledStatement: | ||
checkLabelledStatement(<LabeledStatement>n); | ||
break; | ||
case SyntaxKind.SwitchStatement: | ||
checkSwitchStatement(<SwitchStatement>n); | ||
break; | ||
case SyntaxKind.TryStatement: | ||
checkTryStatement(<TryStatement>n); | ||
break; | ||
case SyntaxKind.WithStatement: | ||
checkWithStatement(<WithStatement>n); | ||
break; | ||
} | ||
} | ||
|
||
check(decl); | ||
return currentState; | ||
} | ||
|
||
const enum ControlFlowState { | ||
Uninitialized = 0, | ||
Reachable = 1, | ||
Unreachable = 2, | ||
ReportedUnreachable = 3, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider
controlFlow.ts
, but I like this too.