Skip to content
This repository has been archived by the owner on Feb 12, 2022. It is now read-only.

Simplify path conditions #970

Closed
wants to merge 1 commit into from
Closed

Simplify path conditions #970

wants to merge 1 commit into from

Conversation

hermanventer
Copy link
Contributor

Break up, simplify and refine path conditions. Also special case the property values obtained from joined objects to use the object's join condition, which is more likely to match the a path condition than the artificial "ob === ob1 ? ob1.prop : ob2.prop" that arises from the general case.

Finally, beef up the implementation of implies, to handle more complicated path conditions by breaking up && and || conditions.

if (lval instanceof Value && compl2 instanceof Value) {
// joinEffects does the right thing for the side effects of the second expression but for the result the join
// produces a conditional expressions of the form (a ? b : a) for a && b and (a ? a : b) for a || b
// Rather than look for this pattern everywhere, we override this behavior and replace the completion with
Copy link
Contributor

Choose a reason for hiding this comment

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

"Rather than look for this pattern everywhere" - I would phrase that as a TODO: In some future, all such expressions should immediately simplify down to some normal form, and it should be impossible to construct values that are not in normal form.

Copy link
Contributor Author

@hermanventer hermanventer Sep 14, 2017

Choose a reason for hiding this comment

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

This is from the previous pull request. Adding a TODO like that at this point does not strike me as at all as useful.

function pushPathCondition(condition: Value) {
if (condition instanceof ConcreteValue) return;
if (!condition.mightNotBeTrue()) return;
invariant(condition instanceof AbstractValue);
Copy link
Contributor

Choose a reason for hiding this comment

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

Another invariant should be invariant(!condition.mightNotBeFalse()).

Copy link
Contributor

Choose a reason for hiding this comment

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

And both invariants should be checked before the if (condition instanceof ConcreteValue) return; statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is sort of really obvious, but why not? I'll add it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The invariant in line 45 is there to convince Flow that a Value that is not a ConcreteValue is actually an AbstractValue. It is inconvenient to insist that the condition may not known to be true and there is not much to be gained from checking that it is not, since it naturally arises from refinement of existing path conditions. It should, however, always be the case that a path condition cannot be known to be definitely false, since that would contract the implicit assertion that path conditions are always true in the path they guard. That invariant I can add.

Copy link
Contributor

Choose a reason for hiding this comment

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

I have no problem with the invariant(condition instanceof AbstractValue); invariant. My comments were about other invariants that are or should go in front.

if (condition.kind === "&&") {
let left = condition.args[0];
let right = condition.args[1];
invariant(left instanceof AbstractValue);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't get why that invariant is guaranteed to hold.

  • Anyone can create abstract values with the "&&", as we haven't locked that up. But that maybe secondary...
  • createFromLogicalOp seems to happy construct conditions where one operand is a concrete value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add a comment. The reason is that if the first argument is concrete, the second argument will never be evaluated and hence it is a mistake to make such an abstract value.

Copy link
Contributor

Choose a reason for hiding this comment

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

A mistake, yes, but this would be a very late invariant check. Should createFromLogicalOp disallow it as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed. I'll see to it.

}

function pushInversePathCondition(condition: Value) {
if (condition instanceof ConcreteValue) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar comments as for pushPathCondition apply.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

similar answers.

invariant(right instanceof AbstractValue);
pushPathCondition(left);
pushPathCondition(right);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the lack of symmetry? pushInversePathCondition deals with both cases... How about folding both together with a boolean parameter negate?

Copy link
Contributor Author

@hermanventer hermanventer Sep 15, 2017

Choose a reason for hiding this comment

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

I did initially fold both together and I found that the code became so unreadable that I was not confident it was correct.

There lack of symmetry, such as it is, is because pushPathCondition gets a ready made condition that can be pushed as is, whereas pushInversePathCondition has to invert the condition before pushing it.

let realm = condition.$Realm;
let inverseCondition = AbstractValue.createFromUnaryOp(realm, "!", condition);
pushPathCondition(inverseCondition);
let simpliedInverseCondition = simplifyAbstractValue(realm, inverseCondition);
Copy link
Contributor

Choose a reason for hiding this comment

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

Spelling of simpliedInverseCondition

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will fix.

let inverseCondition = AbstractValue.createFromUnaryOp(realm, "!", condition);
pushPathCondition(inverseCondition);
let simpliedInverseCondition = simplifyAbstractValue(realm, inverseCondition);
if (simpliedInverseCondition instanceof AbstractValue && simpliedInverseCondition !== inverseCondition)
Copy link
Contributor

Choose a reason for hiding this comment

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

It's strange to see this one-off case. Maybe this is to solve a little issue at hand, but I don't want to see our reasoning infrastructure being randomized.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll remove the check for AbstractValue; that is no longer needed. The equality check is there to avoid duplication in the case where an expression failed to actually simplify.

Copy link
Contributor

Choose a reason for hiding this comment

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

What I meant with the one-off is that only here you also add the simplified path condition in addition to a non-simplified path condition. All the other pieces that get pushed might not be simplified. Overall, I am confused whether pushed path conditions are simplified or not. I don't see enforcement or comments, just this one special treatment.

// use this join condition for the join of the two property values
let [cond, ob1, ob2] = this.args;
invariant(cond instanceof AbstractValue);
invariant(ob1 instanceof ObjectValue);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are the args guaranteed to be of this shape? Hard to tell anything given that anyone can set the .kind property... But in general, if it's a conditional value, why can't there be more structure to the arguments?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We know this value in AbstractObjectValue, so both operands must be objects, otherwise invariants would fail in the constructor. The condition should be Abstract, because otherwise there would be no need for the join. The comment in line 134 already speaks to this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, can't find the constructor invariant. Can you point me to it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Every operation on an abstract object value that retrieves elements from its values collection assert that all of the elements are objects, so this is a maintained invariant of the class. The constructor, however, does not check the invariant. There is no good reason why it shouldn't (and I expected that it does), so I'll add a check.

@@ -261,6 +279,24 @@ export default class AbstractObjectValue extends AbstractValue {
return cv.$Get(P, Receiver);
}
invariant(false);
} else if (this.kind === "conditional") {
// this is the join of two concrete objects
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems quite redundant with the $GetOwnProperty code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is, but it is also different in subtle ways that make it easier to read in its redundant form. I may come back to this, but at the moment I would prefer to keep things this way.

let [left, right] = this.args;
let refinedLeft = left instanceof AbstractValue ? left.refineWithPathCondition() : left;
let refinedRight = right instanceof AbstractValue ? right.refineWithPathCondition() : right;
if (!refinedLeft.mightNotBeTrue()) return refinedRight;
Copy link
Contributor

@NTillmann NTillmann Sep 15, 2017

Choose a reason for hiding this comment

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

This logic here seems more general than the place where it sits: createFromLogicalOp could do this kind of normalization to benefit anyone who attempts to build logical operations.

Interestingly, createFromConditionalOp already does some simplification.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't want the abstract value factory functions to be path sensitive. There is probably room for an in depth design discussion about all this, but it is a bit premature right now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Totally agreed that the factory should not be path sensitive. I just meant the mightNotBeTrue() kind of checks that result in simplified expressions.

left: void | Value,
right: void | Value,
loc?: ?BabelNodeSourceLocation
): AbstractValue {
if (left instanceof BooleanValue && right instanceof BooleanValue) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why special casing for BooleanValue and not just using the more general .mightNotBeTrue kind of machinery?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is more specific than it needs to be. I'll generalize it, but I can't really think of a valid use case for the more general form.

Copy link
Contributor

@NTillmann NTillmann left a comment

Choose a reason for hiding this comment

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

See comments.

@facebook-github-bot
Copy link

@hermanventer has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@facebook-github-bot
Copy link

@hermanventer has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@hermanventer hermanventer deleted the refine3 branch September 20, 2017 23:27
jwli229 pushed a commit that referenced this pull request Oct 7, 2017
Summary:
Break up, simplify and refine path conditions. Also special case the property values obtained from joined objects to use the object's join condition, which is more likely to match the a path condition than the artificial "ob === ob1 ? ob1.prop : ob2.prop" that arises from the general case.

Finally, beef up the implementation of implies, to handle more complicated path conditions by breaking up && and || conditions.
Closes #970

Differential Revision: D5857033

Pulled By: hermanventer

fbshipit-source-id: 5436d251378decf6de549a3d70e9605664d569e5
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants