Detecting tail calls through CFA

This commit is contained in:
Andrey Breslav
2013-12-06 00:00:01 +04:00
parent 9f319e8b24
commit 97319808b6
16 changed files with 483 additions and 7 deletions
@@ -24,19 +24,23 @@ import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jet.lang.cfg.pseudocode.*;
import org.jetbrains.jet.lang.cfg.PseudocodeTraverser.*;
import org.jetbrains.jet.lang.cfg.PseudocodeTraverser.Edges;
import org.jetbrains.jet.lang.cfg.PseudocodeTraverser.InstructionAnalyzeStrategy;
import org.jetbrains.jet.lang.cfg.PseudocodeTraverser.InstructionDataAnalyzeStrategy;
import org.jetbrains.jet.lang.cfg.PseudocodeVariablesData.VariableInitState;
import org.jetbrains.jet.lang.cfg.PseudocodeVariablesData.VariableUseState;
import org.jetbrains.jet.lang.cfg.pseudocode.*;
import org.jetbrains.jet.lang.descriptors.*;
import org.jetbrains.jet.lang.diagnostics.DiagnosticFactory;
import org.jetbrains.jet.lang.diagnostics.Diagnostic;
import org.jetbrains.jet.lang.diagnostics.DiagnosticFactory;
import org.jetbrains.jet.lang.diagnostics.Errors;
import org.jetbrains.jet.lang.psi.*;
import org.jetbrains.jet.lang.resolve.BindingContext;
import org.jetbrains.jet.lang.resolve.BindingContextUtils;
import org.jetbrains.jet.lang.resolve.BindingTrace;
import org.jetbrains.jet.lang.resolve.DescriptorUtils;
import org.jetbrains.jet.lang.resolve.calls.model.ResolvedCall;
import org.jetbrains.jet.lang.resolve.calls.tail.TailRecursionKind;
import org.jetbrains.jet.lang.types.JetType;
import org.jetbrains.jet.lang.types.lang.KotlinBuiltIns;
import org.jetbrains.jet.lexer.JetTokens;
@@ -50,6 +54,10 @@ import static org.jetbrains.jet.lang.cfg.PseudocodeVariablesData.VariableUseStat
import static org.jetbrains.jet.lang.diagnostics.Errors.*;
import static org.jetbrains.jet.lang.resolve.BindingContext.CAPTURED_IN_CLOSURE;
import static org.jetbrains.jet.lang.types.TypeUtils.NO_EXPECTED_TYPE;
import static org.jetbrains.jet.lang.resolve.BindingContext.*;
import static org.jetbrains.jet.lang.resolve.calls.tail.TailRecursionKind.IN_TRY;
import static org.jetbrains.jet.lang.resolve.calls.tail.TailRecursionKind.MIGHT_BE;
import static org.jetbrains.jet.lang.resolve.calls.tail.TailRecursionKind.NON_TAIL;
import static org.jetbrains.jet.lang.types.TypeUtils.noExpectedType;
public class JetFlowInformationProvider {
@@ -64,7 +72,7 @@ public class JetFlowInformationProvider {
@NotNull BindingTrace trace,
@NotNull Pseudocode pseudocode
) {
subroutine = declaration;
this.subroutine = declaration;
this.trace = trace;
this.pseudocode = pseudocode;
}
@@ -94,7 +102,7 @@ public class JetFlowInformationProvider {
}
checkDefiniteReturn(expectedReturnType);
checkDefiniteReturnInLocalFunctions();
checkLocalFunctions();
if (isLocalObject) return;
@@ -106,6 +114,8 @@ public class JetFlowInformationProvider {
markUnusedVariables();
markUnusedLiteralsInBlock();
markTailCalls();
}
private void collectReturnExpressions(@NotNull final Collection<JetElement> returnedExpressions) {
@@ -168,7 +178,7 @@ public class JetFlowInformationProvider {
}
}
private void checkDefiniteReturnInLocalFunctions() {
private void checkLocalFunctions() {
for (LocalFunctionDeclarationInstruction localDeclarationInstruction : pseudocode.getLocalDeclarations()) {
JetElement element = localDeclarationInstruction.getElement();
if (element instanceof JetNamedFunction) {
@@ -178,7 +188,9 @@ public class JetFlowInformationProvider {
JetFlowInformationProvider providerForLocalDeclaration =
new JetFlowInformationProvider(localFunction, trace, localDeclarationInstruction.getBody());
providerForLocalDeclaration.checkDefiniteReturn(expectedType != null ? expectedType : NO_EXPECTED_TYPE);
providerForLocalDeclaration.markTailCalls();
}
}
}
@@ -610,6 +622,117 @@ public class JetFlowInformationProvider {
}
////////////////////////////////////////////////////////////////////////////////
// Tail calls
public void markTailCalls() {
final DeclarationDescriptor subroutineDescriptor = trace.get(BindingContext.DECLARATION_TO_DESCRIPTOR, subroutine);
if (!(subroutineDescriptor instanceof FunctionDescriptor)) return;
if (!KotlinBuiltIns.getInstance().isTailRecursive(subroutineDescriptor)) return;
// finally blocks are copied which leads to multiple diagnostics reported on one instruction
class KindAndCall {
TailRecursionKind kind;
ResolvedCall<?> call;
KindAndCall(TailRecursionKind kind, ResolvedCall<?> call) {
this.kind = kind;
this.call = call;
}
}
final Map<JetElement, KindAndCall> calls = new HashMap<JetElement, KindAndCall>();
PseudocodeTraverser.traverse(
pseudocode,
FORWARD,
new InstructionAnalyzeStrategy() {
@Override
public void execute(@NotNull Instruction instruction) {
if (!(instruction instanceof CallInstruction)) return;
CallInstruction callInstruction = (CallInstruction) instruction;
ResolvedCall<?> resolvedCall = trace.get(RESOLVED_CALL, callInstruction.getElement());
if (resolvedCall == null) return;
// is this a recursive call?
if (!resolvedCall.getResultingDescriptor().getOriginal().equals(subroutineDescriptor)) return;
JetElement element = callInstruction.getElement();
//noinspection unchecked
JetExpression parent = PsiTreeUtil.getParentOfType(
element,
JetTryExpression.class, JetFunction.class, JetClassInitializer.class
);
if (parent instanceof JetTryExpression) {
// We do not support tail calls Collections.singletonMap() try-catch-finally, for simplicity of the mental model
// very few cases there would be real tail-calls, and it's often not so easy for the user to see why
calls.put(element, new KindAndCall(IN_TRY, resolvedCall));
return;
}
boolean isTail = PseudocodeTraverser.traverseFollowingInstructions(
callInstruction,
new HashSet<Instruction>(),
FORWARD,
new TailRecursionDetector(subroutine, callInstruction)
);
TailRecursionKind kind = isTail ? MIGHT_BE : NON_TAIL;
KindAndCall kindAndCall = calls.get(element);
calls.put(element,
new KindAndCall(
combineKinds(kind, kindAndCall == null ? null : kindAndCall.kind),
resolvedCall
)
);
}
}
);
for (Map.Entry<JetElement, KindAndCall> entry : calls.entrySet()) {
JetElement element = entry.getKey();
KindAndCall kindAndCall = entry.getValue();
switch (kindAndCall.kind) {
case MIGHT_BE:
case IN_RETURN:
trace.record(TAIL_RECURSION_CALL, kindAndCall.call, TailRecursionKind.IN_RETURN);
trace.record(BindingContext.HAS_TAIL_CALLS, (FunctionDescriptor) subroutineDescriptor);
break;
case IN_TRY:
trace.report(Errors.TAIL_RECURSION_IN_TRY_IS_NOT_SUPPORTED.on(element));
break;
case NON_TAIL:
trace.report(Errors.NON_TAIL_RECURSIVE_CALL.on(element));
break;
}
}
}
private static TailRecursionKind combineKinds(TailRecursionKind kind, @Nullable TailRecursionKind existingKind) {
TailRecursionKind resultingKind;
if (existingKind == null || existingKind == kind) {
resultingKind = kind;
}
else {
if (check(kind, existingKind, IN_TRY, MIGHT_BE)) {
resultingKind = IN_TRY;
}
else if (check(kind, existingKind, IN_TRY, NON_TAIL)) {
resultingKind = IN_TRY;
}
else {
// MIGHT_BE, NON_TAIL
resultingKind = NON_TAIL;
}
}
return resultingKind;
}
private static boolean check(Object a, Object b, Object x, Object y) {
return (a == x && b == y) || (a == y && b == x);
}
////////////////////////////////////////////////////////////////////////////////
// Utility classes and methods
/**
@@ -0,0 +1,66 @@
/*
* Copyright 2010-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.jet.lang.cfg;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jet.lang.cfg.pseudocode.*;
import org.jetbrains.jet.lang.psi.JetElement;
public class TailRecursionDetector extends InstructionVisitorWithResult<Boolean> implements PseudocodeTraverser.InstructionHandler {
private final JetElement subroutine;
private final Instruction start;
public TailRecursionDetector(@NotNull JetElement subroutine, @NotNull Instruction start) {
this.subroutine = subroutine;
this.start = start;
}
@Override
public boolean handle(@NotNull Instruction instruction) {
return instruction == start || instruction.accept(this);
}
@Override
public Boolean visitInstruction(Instruction instruction) {
return false;
}
@Override
public Boolean visitSubroutineExit(SubroutineExitInstruction instruction) {
return !instruction.isError() && instruction.getSubroutine() == subroutine;
}
@Override
public Boolean visitSubroutineSink(SubroutineSinkInstruction instruction) {
return instruction.getSubroutine() == subroutine;
}
@Override
public Boolean visitJump(AbstractJumpInstruction instruction) {
return true;
}
@Override
public Boolean visitThrowExceptionInstruction(ThrowExceptionInstruction instruction) {
return false;
}
@Override
public Boolean visitMarkInstruction(MarkInstruction instruction) {
return true;
}
}
@@ -0,0 +1,35 @@
== test ==
tailRecursive fun test() : Int {
try {
// do nothing
} finally {
test()
}
}
---------------------
L0:
<START>
mark({ try { // do nothing } finally { test() } })
mark(try { // do nothing } finally { test() })
jmp?(L2 [onExceptionToFinallyBlock]) NEXT:[mark({ test() }), mark({ // do nothing })]
mark({ // do nothing })
read (Unit)
jmp(L3 [skipFinallyToErrorBlock]) NEXT:[mark({ test() })]
L2 [onExceptionToFinallyBlock]:
L4 [start finally]:
mark({ test() }) PREV:[jmp?(L2 [onExceptionToFinallyBlock])]
mark(test())
call(test, test)
L5 [finish finally]:
jmp(error) NEXT:[<ERROR>]
L3 [skipFinallyToErrorBlock]:
mark({ test() }) PREV:[jmp(L3 [skipFinallyToErrorBlock])]
mark(test())
call(test, test)
L1:
<END> NEXT:[<SINK>]
error:
<ERROR> PREV:[jmp(error)]
sink:
<SINK> PREV:[<ERROR>, <END>]
=====================
@@ -0,0 +1,7 @@
tailRecursive fun test() : Int {
try {
// do nothing
} finally {
test()
}
}
@@ -0,0 +1,37 @@
== test ==
tailRecursive fun test() : Int {
try {
// do nothing
} finally {
return test()
}
}
---------------------
L0:
<START>
mark({ try { // do nothing } finally { return test() } })
mark(try { // do nothing } finally { return test() })
jmp?(L2 [onExceptionToFinallyBlock]) NEXT:[mark({ return test() }), mark({ // do nothing })]
mark({ // do nothing })
read (Unit)
jmp(L3 [skipFinallyToErrorBlock]) NEXT:[mark({ return test() })]
L2 [onExceptionToFinallyBlock]:
L4 [start finally]:
mark({ return test() }) PREV:[jmp?(L2 [onExceptionToFinallyBlock])]
mark(test())
call(test, test)
ret(*) L1 NEXT:[<END>]
L5 [finish finally]:
- jmp(error) NEXT:[<ERROR>] PREV:[]
L3 [skipFinallyToErrorBlock]:
mark({ return test() }) PREV:[jmp(L3 [skipFinallyToErrorBlock])]
mark(test())
call(test, test)
ret(*) L1
L1:
<END> NEXT:[<SINK>] PREV:[ret(*) L1, ret(*) L1]
error:
<ERROR> PREV:[]
sink:
<SINK> PREV:[<ERROR>, <END>]
=====================
@@ -0,0 +1,7 @@
tailRecursive fun test() : Int {
try {
// do nothing
} finally {
return test()
}
}
@@ -0,0 +1,46 @@
== sum ==
tailRecursive fun sum(x: Long, sum: Long): Long {
if (x == 0.toLong()) return sum
return sum(x - 1, sum + x)
}
---------------------
L0:
<START>
v(x: Long)
w(x)
v(sum: Long)
w(sum)
mark({ if (x == 0.toLong()) return sum return sum(x - 1, sum + x) })
mark(if (x == 0.toLong()) return sum)
mark(x == 0.toLong())
r(x)
mark(0.toLong())
mark(toLong())
r(0)
call(toLong, toLong)
call(x == 0.toLong(), equals)
jf(L2) NEXT:[read (Unit), r(sum)]
r(sum)
ret(*) L1 NEXT:[<END>]
- jmp(L3) NEXT:[mark(sum(x - 1, sum + x))] PREV:[]
L2:
read (Unit) PREV:[jf(L2)]
L3:
mark(sum(x - 1, sum + x))
mark(x - 1)
r(x)
r(1)
call(-, minus)
mark(sum + x)
r(sum)
r(x)
call(+, plus)
call(sum, sum)
ret(*) L1
L1:
<END> NEXT:[<SINK>] PREV:[ret(*) L1, ret(*) L1]
error:
<ERROR> PREV:[]
sink:
<SINK> PREV:[<ERROR>, <END>]
=====================
+4
View File
@@ -0,0 +1,4 @@
tailRecursive fun sum(x: Long, sum: Long): Long {
if (x == 0.toLong()) return sum
return sum(x - 1, sum + x)
}
@@ -0,0 +1,33 @@
== foo ==
tailRecursive fun foo() {
try {
return foo()
}
catch (e: Throwable) {
}
}
---------------------
L0:
<START>
mark({ try { return foo() } catch (e: Throwable) { } })
mark(try { return foo() } catch (e: Throwable) { })
jmp?(L2 [onException]) NEXT:[v(e: Throwable), mark({ return foo() })]
mark({ return foo() })
mark(foo())
call(foo, foo)
ret(*) L1 NEXT:[<END>]
- jmp(L3 [afterCatches]) NEXT:[<END>] PREV:[]
L2 [onException]:
v(e: Throwable) PREV:[jmp?(L2 [onException])]
w(e)
mark({ })
read (Unit)
jmp(L3 [afterCatches])
L1:
L3 [afterCatches]:
<END> NEXT:[<SINK>] PREV:[ret(*) L1, jmp(L3 [afterCatches])]
error:
<ERROR> PREV:[]
sink:
<SINK> PREV:[<ERROR>, <END>]
=====================
+7
View File
@@ -0,0 +1,7 @@
tailRecursive fun foo() {
try {
return foo()
}
catch (e: Throwable) {
}
}
@@ -0,0 +1,48 @@
== test ==
fun test() : Unit {
try {
test()
} catch (any : Exception) {
test()
} finally {
test()
}
}
---------------------
L0:
<START>
mark({ try { test() } catch (any : Exception) { test() } finally { test() } })
mark(try { test() } catch (any : Exception) { test() } finally { test() })
jmp?(L2 [onException]) NEXT:[v(any : Exception), jmp?(L3 [onExceptionToFinallyBlock])]
jmp?(L3 [onExceptionToFinallyBlock]) NEXT:[mark({ test() }), mark({ test() })]
mark({ test() })
mark(test())
call(test, test)
jmp(L4 [afterCatches]) NEXT:[jmp(L5 [skipFinallyToErrorBlock])]
L2 [onException]:
v(any : Exception) PREV:[jmp?(L2 [onException])]
w(any)
mark({ test() })
mark(test())
call(test, test)
jmp(L4 [afterCatches])
L4 [afterCatches]:
jmp(L5 [skipFinallyToErrorBlock]) NEXT:[mark({ test() })] PREV:[jmp(L4 [afterCatches]), jmp(L4 [afterCatches])]
L3 [onExceptionToFinallyBlock]:
L6 [start finally]:
mark({ test() }) PREV:[jmp?(L3 [onExceptionToFinallyBlock])]
mark(test())
call(test, test)
L7 [finish finally]:
jmp(error) NEXT:[<ERROR>]
L5 [skipFinallyToErrorBlock]:
mark({ test() }) PREV:[jmp(L5 [skipFinallyToErrorBlock])]
mark(test())
call(test, test)
L1:
<END> NEXT:[<SINK>]
error:
<ERROR> PREV:[jmp(error)]
sink:
<SINK> PREV:[<ERROR>, <END>]
=====================
@@ -0,0 +1,9 @@
fun test() : Unit {
try {
test()
} catch (any : Exception) {
test()
} finally {
test()
}
}
@@ -0,0 +1,10 @@
tailRecursive fun sum(x: Long, sum: Long): Long {
if (x == 0.toLong()) return sum
return sum(x - 1, sum + x)
}
fun box() : String {
val sum = sum(1000000, 0)
if (sum != 500000500000.toLong()) return "Fail $sum"
return "OK"
}
@@ -31,7 +31,7 @@ import org.jetbrains.jet.cfg.AbstractControlFlowTest;
/** This class is generated by {@link org.jetbrains.jet.generators.tests.GenerateTests}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("compiler/testData/cfg")
@InnerTestClasses({ControlFlowTestGenerated.DeadCode.class})
@InnerTestClasses({ControlFlowTestGenerated.DeadCode.class, ControlFlowTestGenerated.TailCalls.class})
public class ControlFlowTestGenerated extends AbstractControlFlowTest {
public void testAllFilesPresentInCfg() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), "org.jetbrains.jet.generators.tests.GenerateTests", new File("compiler/testData/cfg"), Pattern.compile("^(.+)\\.kt$"), true);
@@ -245,10 +245,44 @@ public class ControlFlowTestGenerated extends AbstractControlFlowTest {
}
@TestMetadata("compiler/testData/cfg/tailCalls")
public static class TailCalls extends AbstractControlFlowTest {
public void testAllFilesPresentInTailCalls() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), "org.jetbrains.jet.generators.tests.GenerateTests", new File("compiler/testData/cfg/tailCalls"), Pattern.compile("^(.+)\\.kt$"), true);
}
@TestMetadata("finally.kt")
public void testFinally() throws Exception {
doTest("compiler/testData/cfg/tailCalls/finally.kt");
}
@TestMetadata("finallyWithReturn.kt")
public void testFinallyWithReturn() throws Exception {
doTest("compiler/testData/cfg/tailCalls/finallyWithReturn.kt");
}
@TestMetadata("sum.kt")
public void testSum() throws Exception {
doTest("compiler/testData/cfg/tailCalls/sum.kt");
}
@TestMetadata("try.kt")
public void testTry() throws Exception {
doTest("compiler/testData/cfg/tailCalls/try.kt");
}
@TestMetadata("tryCatchFinally.kt")
public void testTryCatchFinally() throws Exception {
doTest("compiler/testData/cfg/tailCalls/tryCatchFinally.kt");
}
}
public static Test suite() {
TestSuite suite = new TestSuite("ControlFlowTestGenerated");
suite.addTestSuite(ControlFlowTestGenerated.class);
suite.addTestSuite(DeadCode.class);
suite.addTestSuite(TailCalls.class);
return suite;
}
}
@@ -6883,6 +6883,11 @@ public class JetDiagnosticsTestGenerated extends AbstractDiagnosticsTestWithEage
doTest("compiler/testData/codegen/box/functions/tailRecursion/simpleReturnWithElse.kt");
}
@TestMetadata("sum.kt")
public void testSum() throws Exception {
doTest("compiler/testData/codegen/box/functions/tailRecursion/sum.kt");
}
@TestMetadata("thisReferences.kt")
public void testThisReferences() throws Exception {
doTest("compiler/testData/codegen/box/functions/tailRecursion/thisReferences.kt");
@@ -2719,6 +2719,11 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
doTest("compiler/testData/codegen/box/functions/tailRecursion/simpleReturnWithElse.kt");
}
@TestMetadata("sum.kt")
public void testSum() throws Exception {
doTest("compiler/testData/codegen/box/functions/tailRecursion/sum.kt");
}
@TestMetadata("thisReferences.kt")
public void testThisReferences() throws Exception {
doTest("compiler/testData/codegen/box/functions/tailRecursion/thisReferences.kt");