-
Notifications
You must be signed in to change notification settings - Fork 425
Conversation
src/evaluators/LogicalExpression.js
Outdated
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 |
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.
"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.
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.
This is from the previous pull request. Adding a TODO like that at this point does not strike me as at all as useful.
src/utils/paths.js
Outdated
function pushPathCondition(condition: Value) { | ||
if (condition instanceof ConcreteValue) return; | ||
if (!condition.mightNotBeTrue()) return; | ||
invariant(condition instanceof AbstractValue); |
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.
Another invariant should be invariant(!condition.mightNotBeFalse())
.
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.
And both invariants should be checked before the if (condition instanceof ConcreteValue) return;
statement.
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.
It is sort of really obvious, but why not? I'll add it.
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.
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.
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.
I have no problem with the invariant(condition instanceof AbstractValue);
invariant. My comments were about other invariants that are or should go in front.
src/utils/paths.js
Outdated
if (condition.kind === "&&") { | ||
let left = condition.args[0]; | ||
let right = condition.args[1]; | ||
invariant(left instanceof AbstractValue); |
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.
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.
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.
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.
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.
A mistake, yes, but this would be a very late invariant check. Should createFromLogicalOp
disallow it as well?
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.
Indeed. I'll see to it.
src/utils/paths.js
Outdated
} | ||
|
||
function pushInversePathCondition(condition: Value) { | ||
if (condition instanceof ConcreteValue) return; |
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.
Similar comments as for pushPathCondition
apply.
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.
similar answers.
src/utils/paths.js
Outdated
invariant(right instanceof AbstractValue); | ||
pushPathCondition(left); | ||
pushPathCondition(right); | ||
} else { |
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.
Why the lack of symmetry? pushInversePathCondition
deals with both cases... How about folding both together with a boolean parameter negate
?
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.
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.
src/utils/paths.js
Outdated
let realm = condition.$Realm; | ||
let inverseCondition = AbstractValue.createFromUnaryOp(realm, "!", condition); | ||
pushPathCondition(inverseCondition); | ||
let simpliedInverseCondition = simplifyAbstractValue(realm, inverseCondition); |
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.
Spelling of simpliedInverseCondition
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.
will fix.
src/utils/paths.js
Outdated
let inverseCondition = AbstractValue.createFromUnaryOp(realm, "!", condition); | ||
pushPathCondition(inverseCondition); | ||
let simpliedInverseCondition = simplifyAbstractValue(realm, inverseCondition); | ||
if (simpliedInverseCondition instanceof AbstractValue && simpliedInverseCondition !== inverseCondition) |
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.
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.
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.
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.
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.
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); |
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.
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?
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.
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.
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.
Hm, can't find the constructor invariant. Can you point me to it?
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.
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 |
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.
Seems quite redundant with the $GetOwnProperty
code.
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.
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.
src/values/AbstractValue.js
Outdated
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; |
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.
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.
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.
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.
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.
Totally agreed that the factory should not be path sensitive. I just meant the mightNotBeTrue() kind of checks that result in simplified expressions.
src/values/AbstractValue.js
Outdated
left: void | Value, | ||
right: void | Value, | ||
loc?: ?BabelNodeSourceLocation | ||
): AbstractValue { | ||
if (left instanceof BooleanValue && right instanceof BooleanValue) { |
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.
Why special casing for BooleanValue
and not just using the more general .mightNotBeTrue
kind of machinery?
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.
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.
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.
See comments.
@hermanventer has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator. |
@hermanventer has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator. |
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
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.