Lichen

Annotated branching.py

90:c7ddfc4525da
2016-10-08 Paul Boddie Added some support for eliminating accessor class types where the provided attributes are invoked and are unbound methods. This uses a more sophisticated method involving usage observations that incorporate invocation information, permitting classes as accessors if paths through the code support them, even if other paths require instances as accessors to invoke methods.
paul@0 1
#!/usr/bin/env python
paul@0 2
paul@0 3
"""
paul@0 4
Track attribute usage for names.
paul@0 5
paul@0 6
Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013,
paul@0 7
              2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
paul@0 8
paul@0 9
This program is free software; you can redistribute it and/or modify it under
paul@0 10
the terms of the GNU General Public License as published by the Free Software
paul@0 11
Foundation; either version 3 of the License, or (at your option) any later
paul@0 12
version.
paul@0 13
paul@0 14
This program is distributed in the hope that it will be useful, but WITHOUT
paul@0 15
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@0 16
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@0 17
details.
paul@0 18
paul@0 19
You should have received a copy of the GNU General Public License along with
paul@0 20
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@0 21
"""
paul@0 22
paul@0 23
from common import dict_for_keys, init_item
paul@0 24
paul@0 25
class Branch:
paul@0 26
paul@0 27
    """
paul@0 28
    A control-flow branch capturing local attribute usage for names.
paul@0 29
    Branches typically begin with assignments or function parameters and are
paul@0 30
    connected to others introduced by conditional and loop nodes.
paul@0 31
paul@0 32
    Branches hosting accesses, and thus providing usage information, are
paul@0 33
    contributors to preceding branches.
paul@0 34
paul@0 35
    Branches also provide a route from accesses back to assignments which are
paul@0 36
    the ultimate suppliers of the names involved.
paul@0 37
    """
paul@0 38
paul@0 39
    def __init__(self, names, assigning=False, values=None):
paul@0 40
paul@0 41
        """
paul@0 42
        Capture attribute usage for the given 'names', with the accompanying
paul@0 43
        'values' indicating assigned values for each name, if indicated.
paul@0 44
        """
paul@0 45
paul@0 46
        self.contributors = set()
paul@0 47
        self.suppliers = {}
paul@0 48
        self.assignments = set(assigning and names or [])
paul@0 49
        self.usage = {}
paul@0 50
        self.values = {}
paul@0 51
paul@0 52
        # Initialise usage for each name.
paul@0 53
paul@0 54
        for name in names:
paul@0 55
            self.usage[name] = set()
paul@0 56
paul@0 57
        # Initialise assigned values if any were provided.
paul@0 58
paul@0 59
        if values:
paul@0 60
            for name, value in zip(names, values):
paul@0 61
                if value:
paul@0 62
                    self.values[name] = value
paul@0 63
paul@0 64
        # Computed results.
paul@0 65
paul@0 66
        self.combined_usage = None
paul@0 67
paul@0 68
    def get_assignment_sources(self, name):
paul@0 69
paul@0 70
        """
paul@0 71
        Return the sources of 'name' from this branch's assignment information,
paul@0 72
        returning a list containing only this branch itself if it is the source.
paul@0 73
        """
paul@0 74
paul@0 75
        if name in self.assignments:
paul@0 76
            return [self]
paul@0 77
        else:
paul@0 78
            return [b for b in self.get_all_suppliers(name) if name in b.assignments]
paul@0 79
paul@88 80
    def set_usage(self, name, attrname, invocation=False):
paul@0 81
paul@0 82
        """
paul@88 83
        Record usage on the given 'name' of the attribute 'attrname', noting the
paul@88 84
        invocation of the attribute if 'invocation' is set to a true value.
paul@0 85
        """
paul@0 86
paul@0 87
        if self.usage.has_key(name):
paul@88 88
            self.usage[name].add((attrname, invocation))
paul@0 89
paul@0 90
    def get_usage(self):
paul@0 91
paul@0 92
        """
paul@0 93
        Obtain usage from this node, combined with usage observed by its
paul@0 94
        contributors. Unlike the local usage which involves only a single set of
paul@0 95
        attribute names for a given variable name, the returned usage is a set
paul@0 96
        of attribute name combinations for a given variable name. For example:
paul@0 97
paul@0 98
        {'a': set([('p', 'q', 'r'), ('p', 'r')])}
paul@0 99
        """
paul@0 100
paul@0 101
        if self.combined_usage is None:
paul@0 102
paul@0 103
            # Accumulate usage observations from contributors.
paul@0 104
paul@0 105
            all_usage = []
paul@0 106
paul@0 107
            for contributor in self.contributors:
paul@0 108
paul@0 109
                # Record any usage that can be returned.
paul@0 110
paul@0 111
                all_usage.append(contributor.get_usage())
paul@0 112
paul@0 113
            # Merge usage from the contributors.
paul@0 114
paul@0 115
            merged_usage = merge_dicts(all_usage)
paul@0 116
paul@0 117
            # Make the local usage compatible with the combined usage.
paul@0 118
paul@0 119
            usage = deepen_dict(self.usage)
paul@0 120
paul@0 121
            self.combined_usage = combine_dicts(usage, merged_usage, combine_sets)
paul@0 122
paul@0 123
        return self.combined_usage
paul@0 124
paul@0 125
    def get_all_suppliers(self, name, all_suppliers=None):
paul@0 126
paul@0 127
        "Return all branches supplying this branch with definitions of 'name'."
paul@0 128
paul@0 129
        all_suppliers = all_suppliers or set()
paul@0 130
        all_suppliers.add(self)
paul@0 131
paul@0 132
        if self.suppliers.has_key(name):
paul@0 133
            for supplier in self.suppliers[name]:
paul@0 134
                if supplier not in all_suppliers:
paul@0 135
                    supplier.get_all_suppliers(name, all_suppliers)
paul@0 136
paul@0 137
        return all_suppliers
paul@0 138
paul@0 139
    def __repr__(self):
paul@0 140
        return "Branch(%r, %r)" % (self.usage.keys(),
paul@0 141
            self.assignments and True or False)
paul@0 142
paul@0 143
class BranchTracker:
paul@0 144
paul@0 145
    """
paul@0 146
    A tracker of attribute usage for names in a namespace. This tracker directs
paul@0 147
    usage observations to branches which are the ultimate repositories of
paul@0 148
    attribute usage information.
paul@0 149
paul@0 150
    As a program unit is inspected, the branches associated with names may
paul@0 151
    change. Assignments reset the branches; control-flow operations cause
paul@0 152
    branches to be accumulated from different code paths.
paul@0 153
    """
paul@0 154
paul@0 155
    def __init__(self):
paul@0 156
paul@0 157
        # Track assignments.
paul@0 158
paul@0 159
        self.assignments = {}
paul@0 160
paul@0 161
        # Details of attributes at each active branch level.
paul@0 162
paul@0 163
        self.attribute_branches = [{}]          # stack of branches for names
paul@0 164
        self.attribute_branch_shelves = []      # stack of shelved branches
paul@0 165
paul@0 166
        # Suspended branch details plus loop details.
paul@0 167
paul@0 168
        self.suspended_broken_branches = []     # stack of lists of dicts
paul@0 169
        self.suspended_continuing_branches = [] # stack of lists of dicts
paul@0 170
paul@0 171
        # Abandoned usage, useful for reviving usage for exception handlers.
paul@0 172
paul@0 173
        self.abandoned_branches = [[]]          # stack of lists of branches
paul@0 174
paul@0 175
        # Returning branches are like abandoned branches but are only revived in
paul@0 176
        # finally clauses.
paul@0 177
paul@0 178
        self.returning_branches = [[]]
paul@0 179
paul@0 180
        # Branches active when starting loops.
paul@0 181
paul@0 182
        self.loop_branches = []
paul@0 183
paul@0 184
    # Structure assembly methods.
paul@0 185
paul@0 186
    def new_branchpoint(self, loop_node=False):
paul@0 187
paul@0 188
        """
paul@0 189
        Indicate that branches diverge, initialising resources dependent on
paul@0 190
        any given 'loop_node'.
paul@0 191
        """
paul@0 192
paul@0 193
        self.attribute_branch_shelves.append([])
paul@0 194
paul@0 195
        if loop_node:
paul@0 196
            self.suspended_broken_branches.append([])
paul@0 197
            self.suspended_continuing_branches.append([])
paul@0 198
paul@0 199
        # Retain a record of abandoned branches.
paul@0 200
paul@0 201
        self.abandoned_branches.append([])
paul@0 202
        self.returning_branches.append([])
paul@0 203
paul@0 204
    def new_branch(self, loop_node=False):
paul@0 205
paul@0 206
        "Create a new branch."
paul@0 207
paul@0 208
        attribute_branches = self.attribute_branches[-1]
paul@0 209
paul@0 210
        branch, new_branches = self._new_branch(attribute_branches)
paul@0 211
paul@0 212
        if branch and loop_node:
paul@0 213
            self.loop_branches.append(branch)
paul@0 214
paul@0 215
        # Start using the branch for known names.
paul@0 216
paul@0 217
        self.attribute_branches.append(new_branches)
paul@0 218
paul@0 219
    def _new_branch(self, attribute_branches):
paul@0 220
paul@0 221
        """
paul@0 222
        Define a new branch that will record attribute usage on known names from
paul@0 223
        'attribute_branches'.
paul@0 224
        """
paul@0 225
paul@0 226
        # Detect abandoned branches.
paul@0 227
paul@0 228
        if isinstance(attribute_branches, AbandonedDict):
paul@0 229
            return None, AbandonedDict()
paul@0 230
paul@0 231
        # Otherwise, define a new branch.
paul@0 232
paul@0 233
        names = attribute_branches.keys()
paul@0 234
paul@0 235
        new_branches = {}
paul@0 236
        branch = Branch(names)
paul@0 237
paul@0 238
        for name in names:
paul@0 239
            new_branches[name] = [branch]
paul@0 240
paul@0 241
        # Add this new branch as a contributor to the previously active
paul@0 242
        # branches.
paul@0 243
paul@0 244
        self._connect_branches(attribute_branches, branch)
paul@0 245
paul@0 246
        return branch, new_branches
paul@0 247
paul@0 248
    def shelve_branch(self, loop_node=False):
paul@0 249
paul@0 250
        "Retain the current branch for later merging."
paul@0 251
paul@0 252
        branches = self.attribute_branches.pop()
paul@0 253
        self.attribute_branch_shelves[-1].append(branches)
paul@0 254
paul@0 255
        # Connect any loop branch to the active branches as contributors.
paul@0 256
paul@0 257
        if loop_node:
paul@0 258
            branch = self.loop_branches.pop()
paul@0 259
            self._connect_branches(branches, branch, loop_node)
paul@0 260
paul@0 261
    def abandon_branch(self):
paul@0 262
paul@0 263
        "Abandon the current branch, retaining it for later."
paul@0 264
paul@0 265
        attribute_branches = self.attribute_branches[-1]
paul@0 266
        self._abandon_branch()
paul@0 267
        self.abandoned_branches[-1].append(attribute_branches)
paul@0 268
paul@0 269
    def abandon_returning_branch(self):
paul@0 270
paul@0 271
        "Abandon the current branch, retaining it for later."
paul@0 272
paul@0 273
        attribute_branches = self.attribute_branches[-1]
paul@0 274
        self._abandon_branch()
paul@0 275
        self.returning_branches[-1].append(attribute_branches)
paul@0 276
paul@0 277
    def suspend_broken_branch(self):
paul@0 278
paul@0 279
        "Suspend a branch for breaking out of a loop."
paul@0 280
paul@0 281
        attribute_branches = self.attribute_branches[-1]
paul@0 282
paul@0 283
        branches = self.suspended_broken_branches[-1]
paul@0 284
        branches.append(attribute_branches)
paul@0 285
        self._abandon_branch()
paul@0 286
paul@0 287
    def suspend_continuing_branch(self):
paul@0 288
paul@0 289
        "Suspend a branch for loop continuation."
paul@0 290
paul@0 291
        attribute_branches = self.attribute_branches[-1]
paul@0 292
paul@0 293
        branches = self.suspended_continuing_branches[-1]
paul@0 294
        branches.append(attribute_branches)
paul@0 295
        self._abandon_branch()
paul@0 296
paul@0 297
    def _abandon_branch(self):
paul@0 298
paul@0 299
        "Abandon the current branch."
paul@0 300
paul@0 301
        self.attribute_branches[-1] = AbandonedDict()
paul@0 302
paul@0 303
    def resume_abandoned_branches(self):
paul@0 304
paul@0 305
        """
paul@0 306
        Resume branches previously abandoned.
paul@0 307
paul@0 308
        Abandoned branches are not reset because they may not be handled by
paul@0 309
        exception handlers after all.
paul@0 310
        """
paul@0 311
paul@0 312
        current_branches = self.attribute_branches[-1]
paul@0 313
        abandoned_branches = self.abandoned_branches[-1]
paul@0 314
        merged_branches = merge_dicts(abandoned_branches + [current_branches])
paul@0 315
paul@0 316
        # Replace the combined branches with a new branch applying to all active
paul@0 317
        # names, connected to the supplying branches.
paul@0 318
paul@0 319
        branch, new_branches = self._new_branch(merged_branches)
paul@0 320
        self.attribute_branches.append(new_branches)
paul@0 321
paul@0 322
        # Although returning branches should not be considered as accumulating
paul@0 323
        # usage, they do provide sources of assignments.
paul@0 324
paul@0 325
        if branch:
paul@0 326
            for returning_branches in self.returning_branches[-1]:
paul@0 327
                self._connect_suppliers(returning_branches, branch)
paul@0 328
paul@0 329
    def resume_all_abandoned_branches(self):
paul@0 330
paul@0 331
        """
paul@0 332
        Resume branches previously abandoned including returning branches.
paul@0 333
paul@0 334
        Abandoned branches are not reset because they may not be handled by
paul@0 335
        exception handlers after all.
paul@0 336
        """
paul@0 337
paul@0 338
        current_branches = self.attribute_branches[-1]
paul@0 339
        abandoned_branches = self.abandoned_branches[-1]
paul@0 340
        returning_branches = self.returning_branches[-1]
paul@0 341
        merged_branches = merge_dicts(abandoned_branches + returning_branches + [current_branches])
paul@0 342
        self.replace_branches(merged_branches)
paul@0 343
paul@0 344
        # Return the previously-active branches for later restoration.
paul@0 345
paul@0 346
        return current_branches
paul@0 347
paul@0 348
    def resume_broken_branches(self):
paul@0 349
paul@0 350
        "Resume branches previously suspended for breaking out of a loop."
paul@0 351
paul@0 352
        suspended_branches = self.suspended_broken_branches.pop()
paul@0 353
        current_branches = self.attribute_branches[-1]
paul@0 354
paul@0 355
        # Merge suspended branches with the current branch.
paul@0 356
paul@0 357
        merged_branches = merge_dicts(suspended_branches + [current_branches])
paul@0 358
        self.replace_branches(merged_branches)
paul@0 359
paul@0 360
    def resume_continuing_branches(self):
paul@0 361
paul@0 362
        "Resume branches previously suspended for loop continuation."
paul@0 363
paul@0 364
        suspended_branches = self.suspended_continuing_branches.pop()
paul@0 365
        current_branches = self.attribute_branches[-1]
paul@0 366
paul@0 367
        # Merge suspended branches with the current branch.
paul@0 368
paul@0 369
        merged_branches = merge_dicts(suspended_branches + [current_branches])
paul@0 370
        self.replace_branches(merged_branches)
paul@0 371
paul@0 372
    def replace_branches(self, merged_branches):
paul@0 373
paul@0 374
        """
paul@0 375
        Replace the 'merged_branches' with a new branch applying to all active
paul@0 376
        names, connected to the supplying branches.
paul@0 377
        """
paul@0 378
paul@0 379
        branch, new_branches = self._new_branch(merged_branches)
paul@0 380
        self.attribute_branches[-1] = new_branches
paul@0 381
paul@0 382
    def restore_active_branches(self, branches):
paul@0 383
paul@0 384
        "Restore the active 'branches'."
paul@0 385
paul@0 386
        self.attribute_branches[-1] = branches
paul@0 387
paul@0 388
    def merge_branches(self):
paul@0 389
paul@0 390
        "Merge branches."
paul@0 391
paul@0 392
        # Combine the attribute branches. This ensures that a list of branches
paul@0 393
        # affected by attribute usage is maintained for the current branch.
paul@0 394
paul@0 395
        all_shelved_branches = self.attribute_branch_shelves.pop()
paul@0 396
        merged_branches = merge_dicts(all_shelved_branches, missing=make_missing)
paul@0 397
        self.replace_branches(merged_branches)
paul@0 398
paul@0 399
        # Abandoned branches are retained for exception handling purposes.
paul@0 400
paul@0 401
        all_abandoned_branches = self.abandoned_branches.pop()
paul@0 402
        new_abandoned_branches = merge_dicts(all_abandoned_branches)
paul@0 403
        self.abandoned_branches[-1].append(new_abandoned_branches)
paul@0 404
paul@0 405
        # Returning branches are retained for finally clauses.
paul@0 406
paul@0 407
        all_returning_branches = self.returning_branches.pop()
paul@0 408
        new_returning_branches = merge_dicts(all_returning_branches)
paul@0 409
        self.returning_branches[-1].append(new_returning_branches)
paul@0 410
paul@0 411
    # Internal structure assembly methods.
paul@0 412
paul@0 413
    def _connect_branches(self, attribute_branches, contributor, loop_node=False):
paul@0 414
paul@0 415
        """
paul@0 416
        Given the 'attribute_branches' mapping, connect the branches referenced
paul@0 417
        in the mapping to the given 'contributor' branch. If 'loop_node' is
paul@0 418
        set to a true value, connect only the branches so that the 'contributor'
paul@0 419
        references the nodes supplying it with name information.
paul@0 420
        """
paul@0 421
paul@0 422
        all_branches = self._connect_suppliers(attribute_branches, contributor)
paul@0 423
        if not loop_node:
paul@0 424
            self._connect_contributor(contributor, all_branches)
paul@0 425
paul@0 426
    def _connect_suppliers(self, attribute_branches, contributor):
paul@0 427
paul@0 428
        "Connect the 'attribute_branches' to the given 'contributor'."
paul@0 429
paul@0 430
        # Gather branches involved with all known names into a single set.
paul@0 431
paul@0 432
        all_branches = set()
paul@0 433
paul@0 434
        for name, branches in attribute_branches.items():
paul@0 435
            all_branches.update(branches)
paul@0 436
paul@0 437
            # Also note receiving branches on the contributor.
paul@0 438
paul@0 439
            for branch in branches:
paul@0 440
                init_item(contributor.suppliers, name, set)
paul@0 441
                contributor.suppliers[name].add(branch)
paul@0 442
paul@0 443
        return all_branches
paul@0 444
paul@0 445
    def _connect_contributor(self, contributor, branches):
paul@0 446
paul@0 447
        "Connect the given 'contributor' branch to the given 'branches'."
paul@0 448
paul@0 449
        for branch in branches:
paul@0 450
            branch.contributors.add(contributor)
paul@0 451
paul@0 452
    # Attribute usage methods.
paul@0 453
paul@0 454
    def tracking_name(self, name):
paul@0 455
paul@0 456
        """
paul@0 457
        Return whether 'name' is being tracked, returning all branches doing so
paul@0 458
        if it is.
paul@0 459
        """
paul@0 460
paul@1 461
        return self.assignments.has_key(name) and self.have_name(name)
paul@0 462
paul@0 463
    def have_name(self, name):
paul@0 464
paul@1 465
        "Return whether 'name' is known."
paul@0 466
paul@0 467
        return self.attribute_branches[-1].get(name)
paul@0 468
paul@0 469
    def assign_names(self, names, values=None):
paul@0 470
paul@0 471
        """
paul@0 472
        Define the start of usage tracking for the given 'names', each being
paul@0 473
        assigned with the corresponding 'values' if indicated.
paul@0 474
        """
paul@0 475
paul@0 476
        branches = self.attribute_branches[-1]
paul@0 477
        branch = Branch(names, True, values)
paul@0 478
paul@0 479
        for name in names:
paul@0 480
            branches[name] = [branch]
paul@0 481
            init_item(self.assignments, name, list)
paul@0 482
            self.assignments[name].append(branch)
paul@0 483
paul@0 484
        return branch
paul@0 485
paul@88 486
    def use_attribute(self, name, attrname, invocation=False):
paul@0 487
paul@0 488
        """
paul@0 489
        Indicate the use on the given 'name' of an attribute with the given
paul@88 490
        'attrname', optionally involving an invocation of the attribute if
paul@88 491
        'invocation' is set to a true value.
paul@0 492
paul@0 493
        Return all branches that support 'name'.
paul@0 494
        """
paul@0 495
paul@0 496
        branches = self.attribute_branches[-1]
paul@0 497
paul@0 498
        # Add the usage to all current branches.
paul@0 499
paul@0 500
        if branches.has_key(name):
paul@0 501
            for branch in branches[name]:
paul@88 502
                branch.set_usage(name, attrname, invocation)
paul@0 503
            return branches[name]
paul@0 504
        else:
paul@0 505
            return None
paul@0 506
paul@0 507
    # Query methods.
paul@0 508
paul@0 509
    def get_assignment_positions_for_branches(self, name, branches, missing=True):
paul@0 510
paul@0 511
        """
paul@0 512
        Return the positions of assignments involving the given 'name' affected
paul@0 513
        by the given 'branches'. If 'missing' is set to a false value, branches
paul@0 514
        with missing name details will be excluded instead of contributing the
paul@0 515
        value None to the list of positions.
paul@0 516
        """
paul@0 517
paul@0 518
        if not branches:
paul@0 519
            return [None]
paul@0 520
paul@0 521
        positions = set()
paul@0 522
        assignments = self.assignments[name]
paul@0 523
paul@0 524
        for assignment in self.get_assignments_for_branches(name, branches):
paul@0 525
paul@0 526
            # Use None to indicate a branch without assignment information.
paul@0 527
paul@0 528
            if missing and isinstance(assignment, MissingBranch):
paul@0 529
                positions.add(None)
paul@0 530
            else:
paul@0 531
                pos = assignments.index(assignment)
paul@0 532
                positions.add(pos)
paul@0 533
paul@0 534
        positions = list(positions)
paul@0 535
        positions.sort()
paul@0 536
        return positions
paul@0 537
paul@0 538
    def get_assignments_for_branches(self, name, branches, missing=True):
paul@0 539
paul@0 540
        """
paul@0 541
        Return the origins of assignments involving the given 'name' affected
paul@0 542
        by the given 'branches'. The origins are a list of branches where names
paul@0 543
        are defined using assignments. If 'missing' is set to a false value,
paul@0 544
        branches with missing name details are excluded.
paul@0 545
        """
paul@0 546
paul@0 547
        all_branches = []
paul@0 548
        assignments = self.assignments[name]
paul@0 549
paul@0 550
        # Obtain the assignments recorded for each branch.
paul@0 551
paul@0 552
        for branch in branches:
paul@0 553
paul@0 554
            # Find the branch representing the definition of some names in the
paul@0 555
            # scope's assignments, making sure that the given name is involved.
paul@0 556
paul@0 557
            for assignment in branch.get_assignment_sources(name):
paul@0 558
paul@0 559
                # Capture branches without assignment information as well as
paul@0 560
                # genuine assignment branches.
paul@0 561
paul@0 562
                if assignment in assignments or missing and isinstance(assignment, MissingBranch):
paul@0 563
                    all_branches.append(assignment)
paul@0 564
paul@0 565
        return all_branches
paul@0 566
paul@0 567
    def get_all_usage(self):
paul@0 568
paul@0 569
        """
paul@0 570
        Convert usage observations from the tracker to a simple mapping of
paul@0 571
        names to sets of attribute names.
paul@0 572
        """
paul@0 573
paul@0 574
        d = {}
paul@0 575
        for name, branches in self.assignments.items():
paul@0 576
            d[name] = self.get_usage_from_branches_for_name(branches, name)
paul@0 577
        return d
paul@0 578
paul@0 579
    def get_usage_from_branches_for_name(self, branches, name):
paul@0 580
paul@0 581
        """
paul@0 582
        Convert usage observations from the 'branches' to a simple list of
paul@0 583
        usage sets for the given 'name'.
paul@0 584
        """
paul@0 585
paul@0 586
        l = []
paul@0 587
        for branch in branches:
paul@0 588
            l.append(branch.get_usage()[name])
paul@0 589
        return l
paul@0 590
paul@0 591
    def get_all_values(self):
paul@0 592
paul@0 593
        "Return a mapping from names to lists of assigned values."
paul@0 594
paul@0 595
        d = {}
paul@0 596
        for name, branches in self.assignments.items():
paul@0 597
            d[name] = [branch.values.get(name) for branch in branches]
paul@0 598
        return d
paul@0 599
paul@0 600
# Special objects.
paul@0 601
paul@0 602
class AbandonedDict(dict):
paul@0 603
paul@0 604
    "A dictionary representing mappings in an abandoned branch."
paul@0 605
paul@0 606
    def __repr__(self):
paul@0 607
        return "AbandonedDict()"
paul@0 608
paul@0 609
class MissingBranch(Branch):
paul@0 610
paul@0 611
    "A branch introduced during dictionary merging."
paul@0 612
paul@0 613
    def __repr__(self):
paul@0 614
        return "MissingBranch(%r, %r)" % (self.usage.keys(),
paul@0 615
            self.assignments and True or False)
paul@0 616
paul@0 617
def make_missing(name):
paul@0 618
paul@0 619
    "Make a special branch indicating missing name information."
paul@0 620
paul@0 621
    return set([MissingBranch([name], True)])
paul@0 622
paul@0 623
# Dictionary utilities.
paul@0 624
paul@0 625
def merge_dicts(dicts, ignored=AbandonedDict, missing=None):
paul@0 626
paul@0 627
    """
paul@0 628
    Merge the given 'dicts' mapping keys to sets of values.
paul@0 629
paul@0 630
    Where 'ignored' is specified, any dictionary of the given type is ignored.
paul@0 631
    Where all dictionaries to be merged are of the given type, an instance of
paul@0 632
    the type is returned as the merged dictionary.
paul@0 633
paul@0 634
    Where 'missing' is specified, it provides a callable that produces a set of
paul@0 635
    suitable values for a given name.
paul@0 636
    """
paul@0 637
paul@0 638
    new_dict = {}
paul@0 639
    all_names = set()
paul@0 640
paul@0 641
    # Determine all known names.
paul@0 642
paul@0 643
    for old_dict in dicts:
paul@0 644
        all_names.update(old_dict.keys())
paul@0 645
paul@0 646
    # Merge the dictionaries, looking for all known names in each one.
paul@0 647
paul@0 648
    have_dicts = False
paul@0 649
paul@0 650
    for old_dict in dicts:
paul@0 651
paul@0 652
        # Abandoned dictionaries should not contribute information.
paul@0 653
paul@0 654
        if isinstance(old_dict, ignored):
paul@0 655
            continue
paul@0 656
        else:
paul@0 657
            have_dicts = True
paul@0 658
paul@0 659
        for name in all_names:
paul@0 660
paul@0 661
            # Find branches providing each name.
paul@0 662
paul@0 663
            if old_dict.has_key(name):
paul@0 664
                values = old_dict[name]
paul@0 665
paul@0 666
            # Branches not providing names may indicate usage before assignment.
paul@0 667
paul@0 668
            elif missing:
paul@0 669
                values = missing(name)
paul@0 670
            else:
paul@0 671
                continue
paul@0 672
paul@0 673
            # Initialise mappings in the resulting dictionary.
paul@0 674
paul@0 675
            if not new_dict.has_key(name):
paul@0 676
                new_dict[name] = set(values)
paul@0 677
            else:
paul@0 678
                new_dict[name].update(values)
paul@0 679
paul@0 680
    # Where no dictionaries contributed, all branches were abandoned.
paul@0 681
paul@0 682
    if have_dicts:
paul@0 683
        return new_dict
paul@0 684
    else:
paul@0 685
        return ignored()
paul@0 686
paul@0 687
def deepen_dict(d):
paul@0 688
paul@0 689
    """
paul@0 690
    Return a version of dictionary 'd' with its values converted to sets
paul@0 691
    containing each original value as a single element in each new value.
paul@0 692
    Original values are assumed to be sequences. Thus...
paul@0 693
paul@0 694
    {"self" : ("x", "y")}
paul@0 695
paul@0 696
    ...would become...
paul@0 697
paul@0 698
    {"self" : set([("x", "y")])}
paul@0 699
paul@0 700
    ...allowing other such values to be added to the set alongside the original
paul@0 701
    value.
paul@0 702
    """
paul@0 703
paul@0 704
    l = []
paul@0 705
paul@0 706
    for key, value in d.items():
paul@0 707
paul@0 708
        # Sort the attribute name details for stable comparisons.
paul@0 709
paul@0 710
        value = list(value)
paul@0 711
        value.sort()
paul@0 712
        l.append((key, set([tuple(value)])))
paul@0 713
paul@0 714
    return dict(l)
paul@0 715
paul@0 716
def combine_sets(s1, s2):
paul@0 717
paul@0 718
    "Combine elements from sets 's1' and 's2'."
paul@0 719
paul@0 720
    if not s1:
paul@0 721
        return s2
paul@0 722
    elif not s2:
paul@0 723
        return s1
paul@0 724
paul@0 725
    s = set()
paul@0 726
paul@0 727
    for i1 in s1:
paul@0 728
        for i2 in s2:
paul@0 729
paul@0 730
            # Sort the attribute name details for stable comparisons.
paul@0 731
paul@0 732
            l = list(set(i1 + i2))
paul@0 733
            l.sort()
paul@0 734
            s.add(tuple(l))
paul@0 735
paul@0 736
    return s
paul@0 737
paul@0 738
def combine_dicts(d1, d2, combine=combine_sets):
paul@0 739
paul@0 740
    """
paul@0 741
    Combine dictionaries 'd1' and 'd2' such that the values for common keys
paul@0 742
    are themselves combined in the result.
paul@0 743
    """
paul@0 744
paul@0 745
    d = {}
paul@0 746
paul@0 747
    for key in d1.keys():
paul@0 748
        if d2.has_key(key):
paul@0 749
            d[key] = combine(d1[key], d2[key])
paul@0 750
        else:
paul@0 751
            d[key] = d1[key]
paul@0 752
paul@0 753
    return d
paul@0 754
paul@0 755
# vim: tabstop=4 expandtab shiftwidth=4