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

effects: Do not over-taint :terminates in abstract cycle #49119

Merged
merged 1 commit into from
Mar 28, 2023

Conversation

topolarity
Copy link
Member

The idea here is to propagate non-termination-by-recursion through the Effects system naturally rather than eagerly marking all callers in a cycle as non-terminating.

Doing this is important to avoid tainting non-termination from purely abstract cycles, where inference is forced to analyze methods that do not correspond to real calls at runtime, such as _return_type

Resolves #48983.

@topolarity topolarity self-assigned this Mar 23, 2023
@topolarity topolarity force-pushed the fix-38983 branch 2 times, most recently from fb38ae9 to ca85073 Compare March 23, 2023 16:54
@giordano giordano added the compiler:effects effect analysis label Mar 23, 2023
Copy link
Member

@vtjnash vtjnash left a comment

Choose a reason for hiding this comment

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

I don't think this premise is true. This proves the absences of recursion in the type graph, but that only implies that the type information is not required to be propagated, not that their is a cycle-free call graph.

Copy link
Member

@vtjnash vtjnash left a comment

Choose a reason for hiding this comment

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

In fact, it seems the existing code is marking this property much too late, as the cycle detection occurs (and is discarded) as early as line 564 in this file.

@topolarity topolarity requested a review from Keno March 23, 2023 19:03
@Keno
Copy link
Member

Keno commented Mar 23, 2023

The cycle-detection-with-unused-result on 564 is separate and taints all effects (#48981)

@Keno
Copy link
Member

Keno commented Mar 23, 2023

I think the general argument goes something like this:
In order to have a runtime non-termination you need to either:

  1. Hit an unknown callsite where inference gives up
  2. Hit the same method instance so inference declares a cycle (but knows nothing about the runtime behavior)
  3. Have a loop
  4. Have inference not terminate either

Can you think of another case where inference does terminate, but none of these four conditions are true?

@vtjnash
Copy link
Member

vtjnash commented Mar 23, 2023

  1. be called from a method that declares a cycle is hit? I think it is true that all methods in a cycle are non-terminating (regardless of the entry point), and not just the first one.

@Keno
Copy link
Member

Keno commented Mar 23, 2023

Right, but presumably as soon as one callsite in a cycle of methods is marked as non-terminating, that should just propagate by the usual rules of effect-propagation, no?

base/compiler/typeinfer.jl Outdated Show resolved Hide resolved
@vtjnash
Copy link
Member

vtjnash commented Mar 23, 2023

Ah, right. Either the call should return and be finished, or it should report a cycle. And in particular, we can even have cycles in the inference types graph, without have cycles in the call graph (because of the behavior of the return_type function)

@Keno
Copy link
Member

Keno commented Mar 23, 2023

Yes

@aviatesk
Copy link
Member

I agree with deleting the too conservative tainting mechanism within typeinf.jl while keeping the tainting within abstract_call_method as is. And it might be better to conservatively add more tainting mechanisms within abstract_call_method_with_const_args too?

We already infer non-termination as part of the conservative effects
we assume at the point in the call-graph that recursion is detected.

As a result, it should be sound to allow this to propagate through
the Effects system naturally rather than eagerly marking our callers
as non-terminating.

Doing this is important to avoid tainting non-termination from purely
abstract cycles, where inference is forced to analyze methods that do
not correspond to real calls at runtime, such as `Base._return_type`.

Resolves JuliaLang#48983.
@topolarity
Copy link
Member Author

I agree with deleting the too conservative tainting mechanism within typeinf.jl while keeping the tainting within abstract_call_method as is.

Agreed! Just trying to put together a test case now for the gap in my original logic.

And it might be better to conservatively add more tainting mechanisms within abstract_call_method_with_const_args too?

If I understand correctly, the effects observed by abstract_call_method_with_const_args should always be a refinement of the effects observed by abstract_call_method (i.e. even if it discovers new cycles, it should not discover new non-termination)

Since we're already returning nothing instead of any Effects information in these cases, I think it might be enough to just abandon the result as we do now. What do you think?

@topolarity
Copy link
Member Author

topolarity commented Mar 27, 2023

Agreed! Just trying to put together a test case now for the gap in my original logic.

I think the original logic is actually sound

This extra tainting makes it difficult for me to come up with a test case that would evade it:

if call_result_unused(si)
add_remark!(interp, sv, RECURSION_UNUSED_MSG)
# since we don't use the result (typically),
# we have a self-cycle in the call-graph, but not in the inference graph (typically):
# break this edge now (before we record it) by returning early
# (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases)
return MethodCallResult(Any, true, true, nothing, Effects())
end

but either way I'm fine with doing this tainting in abstract_call_method

@aviatesk
Copy link
Member

If I understand correctly, the effects observed by abstract_call_method_with_const_args should always be a refinement of the effects observed by abstract_call_method (i.e. even if it discovers new cycles, it should not discover new non-termination)

Since we're already returning nothing instead of any Effects information in these cases, I think it might be enough to just abandon the result as we do now. What do you think?

Yes, the main reason why I'm suggesting it is because it's generally safe to be conservative.

I intended to taint the parent frame sv there rather than returning some effects representing effects of a callee call with their :terminate tainted. So adding something like this there:

if !(is_effect_overridden(sv, :terminates_globally) ||         # this frame is known to terminate
     is_effect_overridden(match.method, :terminates_globally)) # this edge is known to terminate
    # `abstract_call_method` may have returned this callee's effects with their `:terminates`
    # tainted already, but let's make sure to taint it here conservatively
    merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; terminates=true))
end

This would be safe because while we usually expect our analysis to be monotonic, i.e. results from constprop' absint is more accurate that results from ordinary absint in terms of both type and effects domain, this assumption may not always hold true. In some cases const prop' may sometimes discovr out new cycles that were not detected by abstract_call_method. In such cases (I'm not sure if it ever happens though) it would be better to taint :terminate here conservatively.
Having said that, it is worth noting that such cases are pretty rare, and in most cases, I guess abstract_call_method should almost always have returned effects with their :terminates tainted when we hit these conditions in abstract_call_method_with_const_args. So you can leave this for future if you prefer not to be overly conservative in this PR.

@topolarity
Copy link
Member Author

In some cases const prop' may sometimes discovr out new cycles that were not detected by abstract_call_method. In such cases (I'm not sure if it ever happens though) it would be better to taint :terminate here conservatively.

Yeah, I see what you're saying. At the same time, my devil's advocate position would be that conservatively tainting in anticipation of a bug elsewhere in the code just makes that bug harder to find later, especially since the conservative solution here would be an incomplete band-aid.

If abstract_call_method fails to discover a cycle / report non-termination correctly, we definitely can't count on abstract_call_method_with_const_args to do it correctly for us in general, right?

If that reasoning stands, my preference would be to leave out the extra tainting in _with_const_args, but I'm happy to defer to others if there's a strong intuition from more experienced folks than myself

@aviatesk
Copy link
Member

If abstract_call_method fails to discover a cycle / report non-termination correctly, we definitely can't count on abstract_call_method_with_const_args to do it correctly for us in general, right?

Yeah, you're right. I remember we encountered some cases when with_const_args derives less accurate type information than abstract_call_method, but it's probably better to try to make it discover cycles correctly at least.

@aviatesk aviatesk merged commit 2da4c3e into JuliaLang:master Mar 28, 2023
@topolarity topolarity deleted the fix-38983 branch March 28, 2023 14:32
@vtjnash
Copy link
Member

vtjnash commented Mar 28, 2023

If I understand correctly, the effects observed by abstract_call_method_with_const_args should always be a refinement of the effects observed by abstract_call_method (i.e. even if it discovers new cycles, it should not discover new non-termination)

This is correct. The result of inference between these 2 calls is the intersection of their results. Thus if either can show that it terminates, then it terminates, even if the other one couldn't prove that.

Xnartharax pushed a commit to Xnartharax/julia that referenced this pull request Apr 19, 2023
…9119)

We already infer non-termination as part of the conservative effects
we assume at the point in the call-graph that recursion is detected.

As a result, it should be sound to allow this to propagate through
the Effects system naturally rather than eagerly marking our callers
as non-terminating.

Doing this is important to avoid tainting non-termination from purely
abstract cycles, where inference is forced to analyze methods that do
not correspond to real calls at runtime, such as `Base._return_type`.

Resolves JuliaLang#48983.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler:effects effect analysis
Projects
None yet
Development

Successfully merging this pull request may close these issues.

_return_type spuriously taints termination effect
5 participants