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

Handle additional cases in rule_properties::check_accessor #5821

Merged
merged 2 commits into from
Mar 7, 2022

Conversation

wert310
Copy link
Contributor

@wert310 wert310 commented Feb 7, 2022

This PR adds additional safe accessor patterns to the previously accepted ones.

Instead of only allowing guarded accessors, this patch allows for an accessor to be used unguarded if all other recognizers are used in the other paths (so the only possible value at that point of the expression is the last remaining constructor). These new accepted patterns should cover all possible ways a match expression is desugared:

  • guarded branch with correct recognizer (as before)
  • if all other recognizes have been used negated in a rule the remaining unguarded accessor is safe
  • and: if all branches except for one have negated recognizers, then the remaining one is safe
  • or: if all branches except for one have recognizers the remaining one is safe
  • or: if one branch has a negated recognizer (for the current case) access in another branch is safe
  • ite: if the recognizer is in the condition, access in "then" branch is safe; the "else" branch is safe if all recognizers except for one have been used in the condition of an ite in the path to the current accessor
  • any combination of the above

This PR improves the fix of #4869 by allowing pattern matching expressions where the last branch accesses the constructor arguments. The previous version of check_accessor incorrectly returned false for the example of #4869 if the cases of the match expression are swapped:

(define-fun getValueDefault ((opt OptionInt) (default Int)) Int
 (match opt
  ((NoneInt default)
   ((SomeInt int) int))))

In that example, the match is desugared as:

(ite ((_ is (NoneInt () OptionInt)) (:var 0)) (:var 2) (valueInt (:var 0)))

where (valueInt (:var 0)) is a safe (but unguarded) accessor because it is in the else branch and it is the only remaining constructor. The previous check would not allow this accessor because it is unguarded.

(Edit: clarified second case in the above list)

@ghost
Copy link

ghost commented Feb 7, 2022

CLA assistant check
All CLA requirements met.

// if one branch is not(recognizer) then the accessor is safe
if (m.is_not(arg)) {
if (is_recognizer(to_app(arg)->get_arg(0)))
goto _continue;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the goto really required? It looks to me that you can just use continue

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 need to skip the todo.push_back, so just continue from the inner for is not enough.
I'm not really happy about using goto, to remove it I could:

  1. use std::any_of and continue
    if (m.is_or(parent)) {
        for (expr* arg : *to_app(parent))
            add_recognizer(arg);
        if (std::any_of(to_app(parent)->begin(), to_app(parent)->end(),
                        [&](expr *arg) { return m.is_not(arg, arg) && is_recognizer(arg); }))
            continue;
    }
    But this would loop twice over args
  2. use a boolean flag and then continue if the flag is true
  3. include the "or not" case in is_recognizer

Probably (1) is the cleaner one, even if it requires an additional loop/lambda.
I noticed that goto is used more than std::any_of in the codebase, so let me know which one you prefer for consistency.

recognizers.push_back(arg);
// if one branch is not(recognizer) then the accessor is safe
if (m.is_not(arg)) {
if (is_recognizer(to_app(arg)->get_arg(0)))
Copy link
Contributor

Choose a reason for hiding this comment

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

This allows formulas like (==> (is_cons x) (= y (tail x)) . I don’t consider this guarded because the second literal could be set to true even when the first is set to false. Please comment on why this is considered guarded.

Copy link
Contributor

Choose a reason for hiding this comment

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

this should be fine. It is de-morgan dual to conjunction and context based.
dually, there could be a conjunction (not (and (is_cons x) (not (= y (tail x))))
The context-sensitive analysis determines that the conjunction is true only if (is_cons x)

if (to_app(parent)->get_arg(1) == e && is_recognizer(to_app(parent)->get_arg(0)))
continue; // e is guarded
else
if (m_dt.is_recognizer(to_app(parent)->get_arg(0)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Leaves some room as the ite condition could be conjunctive. So an abstraction of this test could be to extract 'must' recognizers from an expression. Overall, a more satisfactory approach would address complete patterns for nested cases which this code doesn't address.

match (x, y) with
    a::as, [] -> 
    [], b::bs -> 
    [], [] -> 
    _, _ -> body with hd(x), tl(y)....

Copy link
Contributor

Choose a reason for hiding this comment

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

There is another bug: e could be in both branches of the ite.
Then checking that it is in the if-branch and continuing masks that it is in the else branch.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Leaves some room as the ite condition could be conjunctive

If I understand correctly, the smtlib match syntax disallows nested patterns Sec 3.6.1, Remark 4 of SMT-LIB 2.6 Standard. So such a pattern matching expression would not be possible unless it is desugared "externally" into ites with conjunctive conditions.

In our WebSpec project, we compile nested patterns into nested smtlib match expressions, so we handle this case by first desugaring it into standard smtlib expressions and then run Z3.

Does Z3 support nested patterns by creating a match expression using another frontend/API?

@wert310
Copy link
Contributor Author

wert310 commented Feb 11, 2022

Thanks for your comments!

I've modified the loop to do a full depth first search over all parents, keeping track of the size of the ctors array across each expression (so that I can remove the elements when backtracking). The search starts with ctors containing all global constructors, then for each path it should contain only the "local" constructors.

I'm using an std::tuple in the todo stack to avoid creating multiple vectors/maps. In particular I use the last element of the tuple to keep track of the visited nodes in the tree. I noticed that elsewhere in the code you are using an ast_mark: do you prefer that I edit the current solution to use an additional ast_mark for consistency, or is this solution ok?

@NikolajBjorner NikolajBjorner merged commit e3568d5 into Z3Prover:master Mar 7, 2022
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.

3 participants