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

Fix: Tail recursion not activated if cannot #2969

Merged
merged 10 commits into from
Nov 3, 2022

Conversation

MikaelMayer
Copy link
Member

@MikaelMayer MikaelMayer commented Nov 1, 2022

This PR fixes #2726 and fixes #2706
I converted contracts to if-conditions, since the next case is the default non-recursive case.

By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.

@MikaelMayer MikaelMayer self-assigned this Nov 1, 2022
} else if (expr is BinaryExpr bin && bin.AccumulatesForTailRecursion != BinaryExpr.AccumulationOperand.None) {
Contract.Assert(accumulatorVar != null);
Contract.Assert(enclosingFunction != null);
Contract.Assert(enclosingFunction.IsAccumulatorTailRecursive);
Copy link
Member

Choose a reason for hiding this comment

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

My interpretation of the purpose of these calls to Contract.Assert is to check properties that are expected to always hold if the surrounding conditional expression is true. Although adding them to the conditional will prevent a crash, I'm wondering whether you've done any deeper investigation into why they're not always true in this case. I worry that there could be a deeper bug lurking that this just temporarily hides away.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question.
So here is the story as I understand it. The following code is a heuristic to check if a function is tail-recursive or not. It goes from bottom-up.
Look at u.size() + 1 in the code.
For binary operators, if the left operation is tail-recursive (which it is u.size()), and if the right operand is Triviallytailrecursive (1 is trivially tail recursive), then the binaryExpr's accumulator AccumulatesForTailRecursion is set to BinaryExpr.AccumulationOperand.Right, which makes sense. If the function is tail-recursive, the accumulation operand is the right one. But at this point, we haven't determined if the function is really tail-recursive or not.

if (accumulationOp != Function.TailStatus.TriviallyTailRecursive) {
var s0 = CheckTailRecursiveExpr(bin.E0, enclosingFunction, false, reportErrors);
Function.TailStatus s1;
switch (s0) {
case Function.TailStatus.NotTailRecursive:
// Any errors have already been reported, but still descend down bin.E1 (possibly reporting
// more errors) before returning with NotTailRecursive
s1 = CheckTailRecursiveExpr(bin.E1, enclosingFunction, false, reportErrors);
return s0;
case Function.TailStatus.TriviallyTailRecursive:
// We are in a state that would allow AcculumateLeftTailRecursive. See what bin.E1 is like:
if (accumulatesOnlyOnRight) {
s1 = CheckHasNoRecursiveCall(bin.E1, enclosingFunction, reportErrors);
} else {
s1 = CheckTailRecursiveExpr(bin.E1, enclosingFunction, false, reportErrors);
}
if (s1 == Function.TailStatus.TailRecursive) {
bin.AccumulatesForTailRecursion = BinaryExpr.AccumulationOperand.Left;
} else {
Contract.Assert(s1 == Function.TailStatus.TriviallyTailRecursive || s1 == Function.TailStatus.NotTailRecursive);
return s1;
}
return accumulationOp;
case Function.TailStatus.TailRecursive:
// We are in a state that would allow right-accumulative tail recursion. Check that the enclosing
// function is not mentioned in bin.E1.
s1 = CheckHasNoRecursiveCall(bin.E1, enclosingFunction, reportErrors);
if (s1 == Function.TailStatus.TriviallyTailRecursive) {
bin.AccumulatesForTailRecursion = BinaryExpr.AccumulationOperand.Right;
if (accumulationOp == Function.TailStatus.AccumulateLeft_Concat) {

What makes no sense is, in the function SinglePassCompiler.TrExprOpt, to convert a binary expression to a tail-recursive equivalent only on the basis that BinaryExpr.AccumulationOperand is not None, because, as we've seen earlier, this was a conditional accumulation. There was another branch l.size() + r.size() that would eventually make the function not tail-recursive.

Copy link
Member

Choose a reason for hiding this comment

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

I think that makes sense. It perhaps would be a good idea at some point to change the way the accumulation operand is set (e.g., have it be None if the function isn't tail recursive), but I think it clarifies that the additional condition seems like the right approach.

atomb
atomb previously approved these changes Nov 3, 2022
Copy link
Member

@atomb atomb left a comment

Choose a reason for hiding this comment

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

I'm convinced that this is the right solution now! :)

} else if (expr is BinaryExpr bin && bin.AccumulatesForTailRecursion != BinaryExpr.AccumulationOperand.None) {
Contract.Assert(accumulatorVar != null);
Contract.Assert(enclosingFunction != null);
Contract.Assert(enclosingFunction.IsAccumulatorTailRecursive);
Copy link
Member

Choose a reason for hiding this comment

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

I think that makes sense. It perhaps would be a good idea at some point to change the way the accumulation operand is set (e.g., have it be None if the function isn't tail recursive), but I think it clarifies that the additional condition seems like the right approach.

@MikaelMayer MikaelMayer enabled auto-merge (squash) November 3, 2022 19:32
&& bin.AccumulatesForTailRecursion != BinaryExpr.AccumulationOperand.None
&& accumulatorVar != null
&& enclosingFunction != null
&& enclosingFunction.IsAccumulatorTailRecursive) {
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 surprising to me that the whole invariant needs to be converted to a check. Are these really all independent parameters?

Comment on lines 2656 to 2657
&& enclosingFunction != null
&& enclosingFunction.IsAccumulatorTailRecursive) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
&& enclosingFunction != null
&& enclosingFunction.IsAccumulatorTailRecursive) {
&& enclosingFunction is { IsAccumulatorTailRecursive: true }) {

Copy link
Member

Choose a reason for hiding this comment

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

(Just a question of taste, so don't feel that you have to apply this one)

docs/dev/news/2726.fix Outdated Show resolved Hide resolved
@MikaelMayer MikaelMayer merged commit e13b129 into master Nov 3, 2022
@MikaelMayer MikaelMayer deleted the fix-2726-crash-too-much-tailrecursion branch November 3, 2022 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Crash during compilation in any language Crash on compiling recursive code
3 participants