Lichen

Annotated resolving.py

1037:2ef21d25c6e5
5 months ago Paul Boddie Merged changes from the default branch. trailing-data
paul@12 1
#!/usr/bin/env python
paul@12 2
paul@12 3
"""
paul@12 4
Name resolution.
paul@12 5
paul@983 6
Copyright (C) 2016, 2017 Paul Boddie <paul@boddie.org.uk>
paul@12 7
paul@12 8
This program is free software; you can redistribute it and/or modify it under
paul@12 9
the terms of the GNU General Public License as published by the Free Software
paul@12 10
Foundation; either version 3 of the License, or (at your option) any later
paul@12 11
version.
paul@12 12
paul@12 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@12 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@12 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@12 16
details.
paul@12 17
paul@12 18
You should have received a copy of the GNU General Public License along with
paul@12 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@12 20
"""
paul@12 21
paul@135 22
from common import init_item
paul@12 23
from results import AccessRef, InstanceRef, InvocationRef, LocalNameRef, \
paul@794 24
                    MultipleRef, NameRef, ResolvedNameRef
paul@12 25
from referencing import Reference
paul@12 26
import sys
paul@12 27
paul@12 28
class NameResolving:
paul@12 29
paul@12 30
    "Resolving names mix-in for inspected modules."
paul@12 31
paul@12 32
    # Post-inspection resolution activities.
paul@12 33
paul@12 34
    def resolve(self):
paul@12 35
paul@12 36
        "Resolve dependencies and complete definitions."
paul@12 37
paul@12 38
        self.resolve_class_bases()
paul@21 39
        self.check_special()
paul@12 40
        self.check_names_used()
paul@27 41
        self.check_invocations()
paul@12 42
        self.resolve_initialisers()
paul@703 43
        self.resolve_return_values()
paul@12 44
        self.resolve_literals()
paul@12 45
paul@12 46
    def resolve_class_bases(self):
paul@12 47
paul@12 48
        "Resolve all class bases since some of them may have been deferred."
paul@12 49
paul@12 50
        for name, bases in self.classes.items():
paul@12 51
            resolved = []
paul@12 52
            bad = []
paul@12 53
paul@12 54
            for base in bases:
paul@27 55
                ref = self.importer.identify(base.get_origin())
paul@12 56
paul@12 57
                # Obtain the origin of the base class reference.
paul@12 58
paul@12 59
                if not ref or not ref.has_kind("<class>"):
paul@12 60
                    bad.append(base)
paul@12 61
                    break
paul@12 62
paul@12 63
                resolved.append(ref)
paul@12 64
paul@12 65
            if bad:
paul@72 66
                print >>sys.stderr, "Bases of class %s were not classes: %s" % (name, ", ".join(map(str, bad)))
paul@12 67
            else:
paul@12 68
                self.importer.classes[name] = self.classes[name] = resolved
paul@12 69
paul@12 70
    def check_special(self):
paul@12 71
paul@12 72
        "Check special names."
paul@12 73
paul@12 74
        for name, value in self.special.items():
paul@423 75
            ref, paths = value
paul@983 76
            self.special[name] = self.importer.identify(ref.get_origin()), paths
paul@12 77
paul@12 78
    def check_names_used(self):
paul@12 79
paul@27 80
        "Check the external names used by each scope."
paul@12 81
paul@27 82
        for key, ref in self.name_references.items():
paul@27 83
            path, name = key.rsplit(".", 1)
paul@27 84
            self.resolve_accesses(path, name, ref)
paul@12 85
paul@27 86
    def check_invocations(self):
paul@12 87
paul@27 88
        "Find invocations amongst module data and replace their results."
paul@12 89
paul@27 90
        # Find members and replace invocation results with values. This is
paul@27 91
        # effectively the same as is done for initialised names, but refers to
paul@27 92
        # any unchanging value after initialisation.
paul@12 93
paul@27 94
        for key, ref in self.objects.items():
paul@27 95
            if ref.has_kind("<invoke>"):
paul@27 96
                ref = self.convert_invocation(ref)
paul@27 97
                self.importer.objects[key] = self.objects[key] = ref
paul@12 98
paul@169 99
        # Convert name references.
paul@169 100
paul@169 101
        for key, ref in self.name_references.items():
paul@169 102
            if ref.has_kind("<invoke>"):
paul@169 103
                ref = self.convert_invocation(ref)
paul@169 104
                self.importer.all_name_references[key] = self.name_references[key] = ref
paul@169 105
paul@31 106
        # Convert function defaults, which are effectively extra members of the
paul@31 107
        # module, and function locals.
paul@12 108
paul@31 109
        for fname, parameters in self.function_defaults.items():
paul@27 110
            l = []
paul@27 111
            for pname, ref in parameters:
paul@27 112
                if ref.has_kind("<invoke>"):
paul@27 113
                    ref = self.convert_invocation(ref)
paul@27 114
                l.append((pname, ref))
paul@129 115
            self.importer.function_defaults[fname] = self.function_defaults[fname] = l
paul@12 116
paul@31 117
        # Convert function locals referencing invocations.
paul@31 118
paul@31 119
        for fname, names in self.function_locals.items():
paul@31 120
            for name, ref in names.items():
paul@31 121
                if ref.has_kind("<invoke>"):
paul@31 122
                    ref = self.convert_invocation(ref)
paul@31 123
                    names[name] = ref
paul@31 124
paul@27 125
    def convert_invocation(self, ref):
paul@27 126
paul@27 127
        "Convert the given invocation 'ref', handling instantiation."
paul@27 128
paul@175 129
        alias = ref.get_name()
paul@27 130
        ref = self.importer.identify(ref.get_origin())
paul@175 131
        ref = ref and ref.has_kind("<class>") and ref.instance_of() or Reference("<var>")
paul@175 132
        return ref and ref.alias(alias) or None
paul@12 133
paul@12 134
    def resolve_accesses(self, path, name, ref):
paul@12 135
paul@12 136
        """
paul@12 137
        Resolve any unresolved accesses in the function at the given 'path'
paul@12 138
        for the given 'name' corresponding to the indicated 'ref'. Note that
paul@12 139
        this mechanism cannot resolve things like inherited methods because
paul@12 140
        they are not recorded as program objects in their inherited locations.
paul@12 141
        """
paul@12 142
paul@12 143
        attr_accesses = self.global_attr_accesses.get(path)
paul@12 144
        all_attrnames = attr_accesses and attr_accesses.get(name)
paul@12 145
paul@12 146
        if not all_attrnames:
paul@12 147
            return
paul@12 148
paul@12 149
        # Insist on constant accessors.
paul@12 150
paul@12 151
        if not ref.has_kind(["<class>", "<module>"]):
paul@12 152
            return
paul@12 153
paul@12 154
        found_attrnames = set()
paul@12 155
paul@12 156
        for attrnames in all_attrnames:
paul@12 157
paul@12 158
            # Start with the resolved name, adding attributes.
paul@12 159
paul@12 160
            attrs = ref.get_path()
paul@12 161
            remaining = attrnames.split(".")
paul@12 162
            last_ref = ref
paul@12 163
paul@12 164
            # Add each component, testing for a constant object.
paul@12 165
paul@12 166
            while remaining:
paul@12 167
                attrname = remaining[0]
paul@12 168
                attrs.append(attrname)
paul@12 169
                del remaining[0]
paul@12 170
paul@12 171
                # Find any constant object reference.
paul@12 172
paul@12 173
                attr_ref = self.get_resolved_object(".".join(attrs))
paul@12 174
paul@12 175
                # Non-constant accessors terminate the traversal.
paul@12 176
paul@12 177
                if not attr_ref or not attr_ref.has_kind(["<class>", "<module>", "<function>"]):
paul@12 178
paul@12 179
                    # Provide the furthest constant accessor unless the final
paul@12 180
                    # access can be resolved.
paul@12 181
paul@12 182
                    if remaining:
paul@12 183
                        remaining.insert(0, attrs.pop())
paul@12 184
                    else:
paul@12 185
                        last_ref = attr_ref
paul@12 186
                    break
paul@12 187
paul@12 188
                # Follow any reference to a constant object.
paul@12 189
                # Where the given name refers to an object in another location,
paul@12 190
                # switch to the other location in order to be able to test its
paul@12 191
                # attributes.
paul@12 192
paul@12 193
                last_ref = attr_ref
paul@12 194
                attrs = attr_ref.get_path()
paul@12 195
paul@12 196
            # Record a constant accessor only if an object was found
paul@12 197
            # that is different from the namespace involved.
paul@12 198
paul@12 199
            if last_ref:
paul@12 200
                objpath = ".".join(attrs)
paul@12 201
                if objpath != path:
paul@12 202
paul@27 203
                    if last_ref.has_kind("<invoke>"):
paul@27 204
                        last_ref = self.convert_invocation(last_ref)
paul@27 205
paul@12 206
                    # Establish a constant access.
paul@12 207
paul@12 208
                    init_item(self.const_accesses, path, dict)
paul@12 209
                    self.const_accesses[path][(name, attrnames)] = (objpath, last_ref, ".".join(remaining))
paul@12 210
paul@12 211
                    if len(attrs) > 1:
paul@12 212
                        found_attrnames.add(attrs[1])
paul@12 213
paul@12 214
        # Remove any usage records for the name.
paul@12 215
paul@12 216
        if found_attrnames:
paul@12 217
paul@12 218
            # NOTE: Should be only one name version.
paul@12 219
paul@12 220
            versions = []
paul@12 221
            for version in self.attr_usage[path][name]:
paul@12 222
                new_usage = set()
paul@12 223
                for usage in version:
paul@12 224
                    if found_attrnames.intersection(usage):
paul@12 225
                        new_usage.add(tuple(set(usage).difference(found_attrnames)))
paul@12 226
                    else:
paul@12 227
                        new_usage.add(usage)
paul@12 228
                versions.append(new_usage)
paul@12 229
paul@12 230
            self.attr_usage[path][name] = versions
paul@12 231
paul@12 232
    def resolve_initialisers(self):
paul@12 233
paul@12 234
        "Resolve initialiser values for names."
paul@12 235
paul@12 236
        # Get the initialisers in each namespace.
paul@12 237
paul@12 238
        for path, name_initialisers in self.name_initialisers.items():
paul@12 239
paul@12 240
            # Resolve values for each name in a scope.
paul@12 241
paul@12 242
            for name, values in name_initialisers.items():
paul@793 243
                self._resolve_values(path, name, values)
paul@12 244
paul@703 245
    def resolve_return_values(self):
paul@703 246
paul@703 247
        "Resolve return values using name references."
paul@703 248
paul@703 249
        # Get the return values from each namespace.
paul@703 250
paul@703 251
        for path, values in self.return_values.items():
paul@731 252
paul@731 253
            # Resolve each return value provided by the scope.
paul@703 254
paul@793 255
            self._resolve_values(path, "$return", values)
paul@793 256
paul@793 257
    def _resolve_values(self, path, name, values):
paul@793 258
paul@793 259
        """
paul@793 260
        Resolve in 'path' the references for 'name' involving the given
paul@793 261
        'values'.
paul@793 262
        """
paul@793 263
paul@793 264
        initialised_names = {}
paul@793 265
        aliased_names = {}
paul@703 266
paul@793 267
        for i, name_ref in enumerate(values):
paul@793 268
            initialised_ref, _aliased_names = self.resolve_reference(path, name_ref)
paul@793 269
            if initialised_ref:
paul@793 270
                initialised_names[i] = initialised_ref
paul@793 271
            if _aliased_names:
paul@793 272
                aliased_names[i] = _aliased_names
paul@703 273
paul@793 274
        if initialised_names:
paul@793 275
            self.initialised_names[(path, name)] = initialised_names
paul@793 276
        if aliased_names:
paul@793 277
            self.aliased_names[(path, name)] = aliased_names
paul@703 278
paul@703 279
    def resolve_reference(self, path, name_ref):
paul@703 280
paul@703 281
        """
paul@703 282
        Within the namespace 'path', resolve the given 'name_ref', returning any
paul@703 283
        initialised reference, along with any aliased name information.
paul@703 284
        """
paul@703 285
paul@703 286
        initialised_ref = None
paul@742 287
        aliased_names = None
paul@703 288
        no_reference = None, None
paul@703 289
paul@794 290
        # Attempt to obtain a coherent reference from multiple outcomes.
paul@794 291
paul@794 292
        if isinstance(name_ref, MultipleRef):
paul@794 293
            refs = set()
paul@794 294
            aliases = []
paul@794 295
paul@794 296
            for result in name_ref.results:
paul@794 297
                _initialised_ref, _aliased_names = self.resolve_reference(path, result)
paul@794 298
paul@794 299
                # Unsuitable references at any level cause the result to yield
paul@794 300
                # no reference.
paul@794 301
paul@794 302
                if not _initialised_ref:
paul@794 303
                    refs = None
paul@794 304
                elif refs is not None:
paul@794 305
                    refs.add(_initialised_ref)
paul@794 306
paul@794 307
                if not _aliased_names:
paul@794 308
                    aliases = None
paul@794 309
                elif aliases is not None:
paul@794 310
                    aliases += _aliased_names
paul@794 311
paul@794 312
            # Only unambiguous references are returned as initialising
paul@794 313
            # references.
paul@794 314
paul@794 315
            if refs and len(refs) == 1:
paul@794 316
                return list(refs)[0], aliases
paul@794 317
            else:
paul@794 318
                return None, aliases
paul@794 319
paul@703 320
        # Unwrap invocations.
paul@703 321
paul@703 322
        if isinstance(name_ref, InvocationRef):
paul@703 323
            invocation = True
paul@703 324
            name_ref = name_ref.name_ref
paul@703 325
        else:
paul@703 326
            invocation = False
paul@703 327
paul@794 328
        const_accesses = self.const_accesses.get(path)
paul@794 329
paul@703 330
        # Obtain a usable reference from names or constants.
paul@703 331
paul@703 332
        if isinstance(name_ref, ResolvedNameRef):
paul@703 333
            if not name_ref.reference():
paul@703 334
                return no_reference
paul@703 335
            ref = name_ref.reference()
paul@703 336
paul@703 337
        # Obtain a reference from instances.
paul@703 338
paul@703 339
        elif isinstance(name_ref, InstanceRef):
paul@703 340
            if not name_ref.reference():
paul@703 341
                return no_reference
paul@703 342
            ref = name_ref.reference()
paul@703 343
paul@703 344
        # Resolve accesses that employ constants.
paul@703 345
paul@703 346
        elif isinstance(name_ref, AccessRef):
paul@703 347
            ref = None
paul@703 348
paul@703 349
            if const_accesses:
paul@703 350
                resolved_access = const_accesses.get((name_ref.original_name, name_ref.attrnames))
paul@703 351
                if resolved_access:
paul@703 352
                    objpath, ref, remaining_attrnames = resolved_access
paul@703 353
                    if remaining_attrnames:
paul@703 354
                        ref = None
paul@703 355
paul@703 356
            # Accesses that do not employ constants cannot be resolved,
paul@703 357
            # but they may be resolvable later.
paul@703 358
paul@703 359
            if not ref:
paul@703 360
paul@711 361
                # Record the path used for tracking purposes
paul@711 362
                # alongside original name, attribute and access
paul@711 363
                # number details.
paul@703 364
paul@742 365
                aliased_names = [(path, name_ref.original_name, name_ref.attrnames, name_ref.number)]
paul@703 366
paul@742 367
                return None, aliased_names
paul@703 368
paul@703 369
        # Attempt to resolve a plain name reference.
paul@703 370
paul@703 371
        elif isinstance(name_ref, LocalNameRef):
paul@703 372
            key = "%s.%s" % (path, name_ref.name)
paul@703 373
            ref = self.name_references.get(key)
paul@703 374
paul@703 375
            # Accesses that do not refer to known static objects
paul@703 376
            # cannot be resolved, but they may be resolvable later.
paul@703 377
paul@703 378
            if not ref:
paul@703 379
paul@711 380
                # Record the path used for tracking purposes
paul@711 381
                # alongside original name, attribute and access
paul@711 382
                # number details.
paul@703 383
paul@742 384
                aliased_names = [(path, name_ref.name, None, name_ref.number)]
paul@703 385
paul@742 386
                return None, aliased_names
paul@703 387
paul@703 388
            ref = self.get_resolved_object(ref.get_origin())
paul@703 389
            if not ref:
paul@703 390
                return no_reference
paul@703 391
paul@703 392
        elif isinstance(name_ref, NameRef):
paul@703 393
            key = "%s.%s" % (path, name_ref.name)
paul@703 394
            ref = self.name_references.get(key)
paul@703 395
paul@703 396
            ref = ref and self.get_resolved_object(ref.get_origin())
paul@703 397
            if not ref:
paul@703 398
                return no_reference
paul@703 399
paul@703 400
        else:
paul@703 401
            return no_reference
paul@703 402
paul@703 403
        # Resolve any hidden dependencies involving external objects
paul@703 404
        # or unresolved names referring to globals or built-ins.
paul@703 405
paul@703 406
        if ref.has_kind("<depends>"):
paul@703 407
            ref = self.importer.identify(ref.get_origin())
paul@703 408
paul@703 409
        # Convert class invocations to instances.
paul@703 410
paul@742 411
        if ref and (invocation or ref.has_kind("<invoke>")):
paul@742 412
            target_ref = ref
paul@742 413
            ref = self.convert_invocation(target_ref)
paul@742 414
paul@742 415
            if not ref or ref.has_kind("<var>"):
paul@742 416
                aliased_names = self.get_aliases_for_target(target_ref.get_origin())
paul@742 417
            else:
paul@742 418
                initialised_ref = ref
paul@703 419
paul@742 420
        elif ref and not ref.has_kind("<var>"):
paul@703 421
            initialised_ref = ref
paul@742 422
        
paul@742 423
        return initialised_ref, aliased_names
paul@703 424
paul@742 425
    def get_aliases_for_target(self, path):
paul@742 426
paul@742 427
        "Return a list of return value locations for the given 'path'."
paul@742 428
paul@742 429
        return_values = self.importer.all_return_values.get(path)
paul@742 430
        locations = []
paul@742 431
paul@742 432
        if return_values:
paul@742 433
            for version in range(0, len(return_values)):
paul@742 434
                locations.append((path, "$return", None, version))
paul@742 435
paul@742 436
        return locations
paul@703 437
paul@12 438
    def resolve_literals(self):
paul@12 439
paul@12 440
        "Resolve constant value types."
paul@12 441
paul@12 442
        # Get the constants defined in each namespace.
paul@12 443
paul@12 444
        for path, constants in self.constants.items():
paul@12 445
            for constant, n in constants.items():
paul@12 446
                objpath = "%s.$c%d" % (path, n)
paul@406 447
                _constant, value_type, encoding = self.constant_values[objpath]
paul@681 448
                self.initialised_names[(path, objpath)] = {0 : Reference("<instance>", value_type)}
paul@12 449
paul@12 450
        # Get the literals defined in each namespace.
paul@12 451
paul@12 452
        for path, literals in self.literals.items():
paul@12 453
            for n in range(0, literals):
paul@12 454
                objpath = "%s.$C%d" % (path, n)
paul@12 455
                value_type = self.literal_types[objpath]
paul@681 456
                self.initialised_names[(path, objpath)] = {0 : Reference("<instance>", value_type)}
paul@12 457
paul@29 458
    # Object resolution.
paul@29 459
paul@792 460
    def get_resolved_object(self, path, defer=False):
paul@29 461
paul@29 462
        """
paul@29 463
        Get the details of an object with the given 'path' within this module.
paul@29 464
        Where the object has not been resolved, None is returned. This differs
paul@29 465
        from the get_object method used elsewhere in that it does not return an
paul@29 466
        unresolved object reference.
paul@29 467
        """
paul@29 468
paul@29 469
        if self.objects.has_key(path):
paul@29 470
            ref = self.objects[path]
paul@792 471
            if not defer and ref.has_kind("<depends>"):
paul@29 472
                return None
paul@29 473
            else:
paul@29 474
                return ref
paul@29 475
        else:
paul@29 476
            return None
paul@29 477
paul@12 478
# vim: tabstop=4 expandtab shiftwidth=4