summaryrefslogtreecommitdiff
path: root/runtime/matcher.py
blob: 4e3d2e4e06fd9ec0affa9fee0ce5497992fee624 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
"""Defines a Matcher, which can efficiently enumerate ProductionRule matches.

This file defines the Matcher class, which keeps track of all matches to a
ProductionRule (optionally satisfying some partial assignment). It is
particularly optimized for applying rules until fixedpoint is reached, see eg.
../tactic_utils.py:RuleFixedpoint.

The main benefit is that Matcher can do "differential updates:" it keeps track
of all matches, and when the structure is modified it only needs to update
matches relevant to the delta between the old structure and modified one.

This works because ProductionRules are matched against in three 'layers:'

1. MustMap, 2. TryMap, 3. NoMap.

The key observation each one is *MONOTONIC*: eg. if you add a new fact, the
MustMap assignments in the new structure are a strict superset of those of the
old structure (and the new assignments *must* make use of the added fact). So,
when new facts are added, we can keep all of the old assignments and only check
new assignments which use the new facts.

Eg. given constraints [(1, 2, 3)] and new facts [(A, B, C), (B, C, D)], we only
need to check for new assignments which map {1: A, 2: B, 3: C} or {1: B, 2: C,
3: D}.

Note that profiling shows the vast majority of time is spent in MustMap, hence
this implementation only optimizes/caches that layer (it re-computes TryMap,
NoMap on every sync). In the future we can think through how to do differential
updates to the latter two layers as well.
"""
from collections import defaultdict
# pylint: disable=import-error,no-name-in-module
import runtime.utils as utils
from runtime.utils import freezedict, thawdict
from runtime.assignment import Assignment

class Matcher:
    """Keeps track of all assignments to a ProductionRule in the runtime."""
    def __init__(self, rt, rule, partial):
        """Initialize a Matcher."""
        self.rt = rt
        self.rule = rule
        self.freeze_frame = rt.ts.freeze_frame()
        self.partial = partial.copy()
        if any(isinstance(key, str) for key in partial.keys()):
            self.partial = dict({rule.node_to_variable[key]: value
                                 for key, value in partial.items()
                                 if key in rule.node_to_variable})
        # Assignments to the 'MustMap' pattern.
        self.must_matcher = PatternMatcher(rt, self.rule.must_pattern, self.partial)
        self.must_assignments = dict()
        for assignment in self.must_matcher.assignments:
            self._add_must(assignment)

    def assignments(self):
        """Yields assignments to self.rule satisfying self.partial.

        Will only yield assignments as of the last call to sync().
        """
        node_to_variable = utils.Translator(self.rule.node_to_variable)
        for must_assignment in sorted(self.must_assignments.keys()):
            entry = self.must_assignments[must_assignment]
            if any(never.assignments for never in entry["nevers"]):
                continue
            if entry["try"] is not None and entry["try"].assignments:
                for assign in map(thawdict, sorted(entry["try"].assignments)):
                    yield Assignment(self.rule, node_to_variable.compose(assign))
            else:
                assign = thawdict(must_assignment)
                yield Assignment(self.rule, node_to_variable.compose(assign))

    def sync(self):
        """Update the assignments."""
        current = self.rt.ts.freeze_frame()
        delta = self.freeze_frame.delta_to_reach(current)
        self.freeze_frame = current

        removed, added = self.must_matcher.sync(delta)

        for assign in removed:
            del self.must_assignments[assign]

        for existing in self.must_assignments:
            entry = self.must_assignments[existing]
            invalid = False
            for never in entry["nevers"]:
                never.sync(delta)
                invalid = invalid or bool(never.assignments)
            if invalid:
                entry["try"] = None
            elif entry["try"] is not None:
                entry["try"].sync(delta)
            else:
                entry["try"] = PatternMatcher(self.rt, self.rule.try_pattern, thawdict(existing))

        for assign in added:
            self._add_must(assign)

    def _add_must(self, frozen):
        """Adds an assignment to the list of must assignments."""
        assignment = thawdict(frozen)
        self.must_assignments[frozen] = dict({
            "nevers": [],
            "try": None,
        })
        invalid = False
        entry = self.must_assignments[frozen]
        for never in sorted(self.rule.never_patterns):
            never = self.rule.never_patterns[never]
            matcher = PatternMatcher(self.rt, never, assignment)
            entry["nevers"].append(matcher)
            invalid = invalid or bool(matcher.assignments)
            # TODO we could break if invalid, but then we'd need more logic
            # elsewhere.
        if not invalid:
            entry["try"] = PatternMatcher(self.rt, self.rule.try_pattern, assignment)

class PatternMatcher:
    """Keeps track of assignments to a single Pattern (existential formula).
    """
    def __init__(self, rt, pattern, partial):
        """Initialize the PatternMatcher.

        @pattern should be a Pattern instance, while @partial should be the
        same partial assignment on the owning Matcher instance.
        """
        self.rt = rt
        self.pattern = pattern
        self.partial = partial.copy()

        self.assignments = set()
        # Maps fact |-> set({assignments})
        self.assignments_relying_on_fact = defaultdict(set)
        # Maps assignment |-> set({facts})
        self.facts_used_in_assignment = defaultdict(set)

        # Initializes must, full.
        self.full_sync()

    def full_sync(self):
        """Initializes self.must, self.full (non-differential).
        """
        for assignment in self.pattern.assignments(self.partial):
            frozen = freezedict(assignment)
            for constraint in self.pattern.constraints:
                fact = tuple(assignment.get(arg, arg) for arg in constraint)
                self.assignments_relying_on_fact[fact].add(frozen)
                self.facts_used_in_assignment[frozen].add(fact)
            self.assignments.add(frozen)

    def sync(self, delta):
        """Updates the set of known assignments to match the current structure.
        """
        removed, added = set(), set()

        if not self.pattern.constraints:
            return removed, added

        # First, see if there are any assignments which rely on removed facts.
        for fact in delta.remove_facts:
            for relying in self.assignments_relying_on_fact[fact].copy():
                # NOTE: this call modifies assignments_relying_on_fact, which
                # is why we take a copy.
                self.remove_assignment(relying)
                removed.add(relying)
            self.assignments_relying_on_fact.pop(fact)

        # For all of the new facts, we find new assignments using them.
        partials = set()
        for fact in delta.add_facts:
            for constraint in self.pattern.constraints:
                assignment = self.unify(constraint, fact)
                if assignment:
                    partials.add(freezedict(assignment))
        partials -= set({freezedict(self.partial)})
        partials = [partial for partial in map(thawdict, sorted(partials))
                    if self.pattern.is_partial(partial)]
        for partial in partials:
            for new_must in self.pattern.assignments(partial):
                frozen = freezedict(new_must)
                if frozen not in self.assignments:
                    self.add_assignment(new_must, frozen)
                    added.add(frozen)

        assert not added & removed
        return removed, added

    def remove_assignment(self, frozen):
        """Remove an existing assignment from the set of valid assignments.

        Called by sync() when it is determined that the assignment is no longer
        valid.
        """
        self.assignments.remove(frozen)
        for fact in self.facts_used_in_assignment.pop(frozen):
            self.assignments_relying_on_fact[fact].remove(frozen)

    def add_assignment(self, assignment, frozen):
        """Add a newly-valid assignment to the set of valid assignments.
        """
        assert frozen not in self.assignments

        self.assignments.add(frozen)

        for constraint in self.pattern.constraints:
            fact = tuple(assignment.get(arg, arg) for arg in constraint)
            self.assignments_relying_on_fact[fact].add(frozen)
            self.facts_used_in_assignment[frozen].add(fact)

    def unify(self, constraint, fact):
        """Unifies a constraint with a fact into an assignment.

        Eg. constraint = (1, 2, 3), fact = (A, B, C) -> {1:A, 2:B, 3:C}.
        """
        assignment = self.partial.copy()
        inverse = defaultdict(list)
        for arg, var in zip(fact, constraint):
            if not isinstance(var, int):
                # Constants must match.
                if arg != var:
                    break
                continue
            if var in assignment and assignment[var] != arg:
                # Variables must match.
                break
            if any(other_var not in self.pattern.maybe_equal[var]
                   for other_var in inverse[arg]):
                break
            assignment[var] = arg
            inverse[arg].append(var)
        else:
            # We didn't break; @assignment is correct!
            return assignment
        return None

class OneOffMatcher:
    """Drop-in replacement for Matcher which does not save state."""
    def __init__(self, rt, rule, partial):
        """Initialize the OneOffMatcher."""
        self.rt = rt
        self.rule = rule
        self.partial = partial.copy()
        if any(isinstance(key, str) for key in partial.keys()):
            self.partial = dict({rule.node_to_variable[key]: value
                                 for key, value in partial.items()
                                 if key in rule.node_to_variable})

    def assignments(self):
        """Solves for and yields valid rule assignments."""
        node_to_variable = utils.Translator(self.rule.node_to_variable)

        must_assignments = self.rule.must_pattern.assignments(self.partial)
        for must_assignment in must_assignments:
            if self.rule.invalid(must_assignment):
                continue

            try_assignments = self.rule.try_pattern.assignments(must_assignment)
            any_assigned = False
            for try_assignment in try_assignments:
                any_assigned = True
                yield Assignment(
                    self.rule, node_to_variable.compose(try_assignment))
            # If there are no try_constraints, then try_assignments will still
            # output the 'trivial match,' and so it will have already been
            # checked against invalid in the above loop.
            if not self.rule.try_pattern.constraints:
                continue
            if not any_assigned:
                yield Assignment(
                    self.rule, node_to_variable.compose(must_assignment))

    def sync(self):
        """No-op for the OneOffMatcher, which is always up-to-date."""
generated by cgit on debian on lair
contact matthew@masot.net with questions or feedback