Lichen

Annotated resolving.py

89:c7d3a8bf2cdc
2016-10-08 Paul Boddie Moved a generic method into the common module as a function.
paul@12 1
#!/usr/bin/env python
paul@12 2
paul@12 3
"""
paul@12 4
Name resolution.
paul@12 5
paul@12 6
Copyright (C) 2016 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@12 22
from common import init_item, predefined_constants
paul@12 23
from results import AccessRef, InstanceRef, InvocationRef, LocalNameRef, \
paul@12 24
                    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@12 43
        self.resolve_literals()
paul@12 44
        self.remove_redundant_accessors()
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@28 75
            self.special[name] = self.importer.identify(value.get_origin())
paul@12 76
paul@12 77
    def check_names_used(self):
paul@12 78
paul@27 79
        "Check the external names used by each scope."
paul@12 80
paul@27 81
        for key, ref in self.name_references.items():
paul@27 82
            path, name = key.rsplit(".", 1)
paul@27 83
            self.resolve_accesses(path, name, ref)
paul@12 84
paul@27 85
    def check_invocations(self):
paul@12 86
paul@27 87
        "Find invocations amongst module data and replace their results."
paul@12 88
paul@27 89
        # Find members and replace invocation results with values. This is
paul@27 90
        # effectively the same as is done for initialised names, but refers to
paul@27 91
        # any unchanging value after initialisation.
paul@12 92
paul@27 93
        for key, ref in self.objects.items():
paul@27 94
            if ref.has_kind("<invoke>"):
paul@27 95
                ref = self.convert_invocation(ref)
paul@27 96
                self.importer.objects[key] = self.objects[key] = ref
paul@12 97
paul@31 98
        # Convert function defaults, which are effectively extra members of the
paul@31 99
        # module, and function locals.
paul@12 100
paul@31 101
        for fname, parameters in self.function_defaults.items():
paul@27 102
            l = []
paul@27 103
            for pname, ref in parameters:
paul@27 104
                if ref.has_kind("<invoke>"):
paul@27 105
                    ref = self.convert_invocation(ref)
paul@27 106
                l.append((pname, ref))
paul@27 107
            self.function_defaults[fname] = l
paul@12 108
paul@31 109
        # Convert function locals referencing invocations.
paul@31 110
paul@31 111
        for fname, names in self.function_locals.items():
paul@31 112
            for name, ref in names.items():
paul@31 113
                if ref.has_kind("<invoke>"):
paul@31 114
                    ref = self.convert_invocation(ref)
paul@31 115
                    names[name] = ref
paul@31 116
paul@27 117
    def convert_invocation(self, ref):
paul@27 118
paul@27 119
        "Convert the given invocation 'ref', handling instantiation."
paul@27 120
paul@27 121
        ref = self.importer.identify(ref.get_origin())
paul@27 122
        return ref and ref.has_kind("<class>") and ref.instance_of() or Reference("<var>")
paul@12 123
paul@12 124
    def resolve_accesses(self, path, name, ref):
paul@12 125
paul@12 126
        """
paul@12 127
        Resolve any unresolved accesses in the function at the given 'path'
paul@12 128
        for the given 'name' corresponding to the indicated 'ref'. Note that
paul@12 129
        this mechanism cannot resolve things like inherited methods because
paul@12 130
        they are not recorded as program objects in their inherited locations.
paul@12 131
        """
paul@12 132
paul@12 133
        attr_accesses = self.global_attr_accesses.get(path)
paul@12 134
        all_attrnames = attr_accesses and attr_accesses.get(name)
paul@12 135
paul@12 136
        if not all_attrnames:
paul@12 137
            return
paul@12 138
paul@12 139
        # Insist on constant accessors.
paul@12 140
paul@12 141
        if not ref.has_kind(["<class>", "<module>"]):
paul@12 142
            return
paul@12 143
paul@12 144
        found_attrnames = set()
paul@12 145
paul@12 146
        for attrnames in all_attrnames:
paul@12 147
paul@12 148
            # Start with the resolved name, adding attributes.
paul@12 149
paul@12 150
            attrs = ref.get_path()
paul@12 151
            remaining = attrnames.split(".")
paul@12 152
            last_ref = ref
paul@12 153
paul@12 154
            # Add each component, testing for a constant object.
paul@12 155
paul@12 156
            while remaining:
paul@12 157
                attrname = remaining[0]
paul@12 158
                attrs.append(attrname)
paul@12 159
                del remaining[0]
paul@12 160
paul@12 161
                # Find any constant object reference.
paul@12 162
paul@12 163
                attr_ref = self.get_resolved_object(".".join(attrs))
paul@12 164
paul@12 165
                # Non-constant accessors terminate the traversal.
paul@12 166
paul@12 167
                if not attr_ref or not attr_ref.has_kind(["<class>", "<module>", "<function>"]):
paul@12 168
paul@12 169
                    # Provide the furthest constant accessor unless the final
paul@12 170
                    # access can be resolved.
paul@12 171
paul@12 172
                    if remaining:
paul@12 173
                        remaining.insert(0, attrs.pop())
paul@12 174
                    else:
paul@12 175
                        last_ref = attr_ref
paul@12 176
                    break
paul@12 177
paul@12 178
                # Follow any reference to a constant object.
paul@12 179
                # Where the given name refers to an object in another location,
paul@12 180
                # switch to the other location in order to be able to test its
paul@12 181
                # attributes.
paul@12 182
paul@12 183
                last_ref = attr_ref
paul@12 184
                attrs = attr_ref.get_path()
paul@12 185
paul@12 186
            # Record a constant accessor only if an object was found
paul@12 187
            # that is different from the namespace involved.
paul@12 188
paul@12 189
            if last_ref:
paul@12 190
                objpath = ".".join(attrs)
paul@12 191
                if objpath != path:
paul@12 192
paul@27 193
                    if last_ref.has_kind("<invoke>"):
paul@27 194
                        last_ref = self.convert_invocation(last_ref)
paul@27 195
paul@12 196
                    # Establish a constant access.
paul@12 197
paul@12 198
                    init_item(self.const_accesses, path, dict)
paul@12 199
                    self.const_accesses[path][(name, attrnames)] = (objpath, last_ref, ".".join(remaining))
paul@12 200
paul@12 201
                    if len(attrs) > 1:
paul@12 202
                        found_attrnames.add(attrs[1])
paul@12 203
paul@12 204
        # Remove any usage records for the name.
paul@12 205
paul@12 206
        if found_attrnames:
paul@12 207
paul@12 208
            # NOTE: Should be only one name version.
paul@12 209
paul@12 210
            versions = []
paul@12 211
            for version in self.attr_usage[path][name]:
paul@12 212
                new_usage = set()
paul@12 213
                for usage in version:
paul@12 214
                    if found_attrnames.intersection(usage):
paul@12 215
                        new_usage.add(tuple(set(usage).difference(found_attrnames)))
paul@12 216
                    else:
paul@12 217
                        new_usage.add(usage)
paul@12 218
                versions.append(new_usage)
paul@12 219
paul@12 220
            self.attr_usage[path][name] = versions
paul@12 221
paul@12 222
    def resolve_initialisers(self):
paul@12 223
paul@12 224
        "Resolve initialiser values for names."
paul@12 225
paul@12 226
        # Get the initialisers in each namespace.
paul@12 227
paul@12 228
        for path, name_initialisers in self.name_initialisers.items():
paul@12 229
            const_accesses = self.const_accesses.get(path)
paul@12 230
paul@12 231
            # Resolve values for each name in a scope.
paul@12 232
paul@12 233
            for name, values in name_initialisers.items():
paul@12 234
                if path == self.name:
paul@12 235
                    assigned_path = name
paul@12 236
                else:
paul@12 237
                    assigned_path = "%s.%s" % (path, name)
paul@12 238
paul@12 239
                initialised_names = {}
paul@12 240
                aliased_names = {}
paul@12 241
paul@12 242
                for i, name_ref in enumerate(values):
paul@12 243
paul@12 244
                    # Unwrap invocations.
paul@12 245
paul@12 246
                    if isinstance(name_ref, InvocationRef):
paul@12 247
                        invocation = True
paul@12 248
                        name_ref = name_ref.name_ref
paul@12 249
                    else:
paul@12 250
                        invocation = False
paul@12 251
paul@12 252
                    # Obtain a usable reference from names or constants.
paul@12 253
paul@12 254
                    if isinstance(name_ref, ResolvedNameRef):
paul@12 255
                        if not name_ref.reference():
paul@12 256
                            continue
paul@12 257
                        ref = name_ref.reference()
paul@12 258
paul@12 259
                    # Obtain a reference from instances.
paul@12 260
paul@12 261
                    elif isinstance(name_ref, InstanceRef):
paul@12 262
                        if not name_ref.reference():
paul@12 263
                            continue
paul@12 264
                        ref = name_ref.reference()
paul@12 265
paul@12 266
                    # Resolve accesses that employ constants.
paul@12 267
paul@12 268
                    elif isinstance(name_ref, AccessRef):
paul@12 269
                        ref = None
paul@12 270
paul@12 271
                        if const_accesses:
paul@12 272
                            resolved_access = const_accesses.get((name_ref.original_name, name_ref.attrnames))
paul@12 273
                            if resolved_access:
paul@12 274
                                objpath, ref, remaining_attrnames = resolved_access
paul@12 275
                                if remaining_attrnames:
paul@12 276
                                    ref = None
paul@12 277
paul@12 278
                        # Accesses that do not employ constants cannot be resolved,
paul@12 279
                        # but they may be resolvable later.
paul@12 280
paul@12 281
                        if not ref:
paul@12 282
                            if not invocation:
paul@12 283
                                aliased_names[i] = name_ref.original_name, name_ref.attrnames, name_ref.number
paul@12 284
                            continue
paul@12 285
paul@12 286
                    # Attempt to resolve a plain name reference.
paul@12 287
paul@12 288
                    elif isinstance(name_ref, LocalNameRef):
paul@12 289
                        key = "%s.%s" % (path, name_ref.name)
paul@12 290
                        origin = self.name_references.get(key)
paul@12 291
paul@12 292
                        # Accesses that do not refer to known static objects
paul@12 293
                        # cannot be resolved, but they may be resolvable later.
paul@12 294
paul@12 295
                        if not origin:
paul@12 296
                            if not invocation:
paul@12 297
                                aliased_names[i] = name_ref.name, None, name_ref.number
paul@12 298
                            continue
paul@12 299
paul@12 300
                        ref = self.get_resolved_object(origin)
paul@12 301
                        if not ref:
paul@12 302
                            continue
paul@12 303
paul@12 304
                    elif isinstance(name_ref, NameRef):
paul@12 305
                        key = "%s.%s" % (path, name_ref.name)
paul@12 306
                        origin = self.name_references.get(key)
paul@12 307
                        if not origin:
paul@12 308
                            continue
paul@12 309
paul@12 310
                        ref = self.get_resolved_object(origin)
paul@12 311
                        if not ref:
paul@12 312
                            continue
paul@12 313
paul@12 314
                    else:
paul@12 315
                        continue
paul@12 316
paul@22 317
                    # Resolve any hidden dependencies involving external objects
paul@22 318
                    # or unresolved names referring to globals or built-ins.
paul@22 319
paul@22 320
                    if ref.has_kind("<depends>"):
paul@27 321
                        ref = self.importer.identify(ref.get_origin())
paul@22 322
paul@12 323
                    # Convert class invocations to instances.
paul@12 324
paul@58 325
                    if ref and invocation:
paul@27 326
                        ref = self.convert_invocation(ref)
paul@12 327
paul@27 328
                    if ref and not ref.has_kind("<var>"):
paul@12 329
                        initialised_names[i] = ref
paul@12 330
paul@12 331
                if initialised_names:
paul@12 332
                    self.initialised_names[assigned_path] = initialised_names
paul@12 333
                if aliased_names:
paul@12 334
                    self.aliased_names[assigned_path] = aliased_names
paul@12 335
paul@12 336
    def resolve_literals(self):
paul@12 337
paul@12 338
        "Resolve constant value types."
paul@12 339
paul@12 340
        # Get the constants defined in each namespace.
paul@12 341
paul@12 342
        for path, constants in self.constants.items():
paul@12 343
            for constant, n in constants.items():
paul@12 344
                objpath = "%s.$c%d" % (path, n)
paul@12 345
                _constant, value_type = self.constant_values[objpath]
paul@12 346
                self.initialised_names[objpath] = {0 : Reference("<instance>", value_type)}
paul@12 347
paul@12 348
        # Get the literals defined in each namespace.
paul@12 349
paul@12 350
        for path, literals in self.literals.items():
paul@12 351
            for n in range(0, literals):
paul@12 352
                objpath = "%s.$C%d" % (path, n)
paul@12 353
                value_type = self.literal_types[objpath]
paul@12 354
                self.initialised_names[objpath] = {0 : Reference("<instance>", value_type)}
paul@12 355
paul@12 356
    def remove_redundant_accessors(self):
paul@12 357
paul@12 358
        "Remove now-redundant modifier and accessor information."
paul@12 359
paul@12 360
        for path, const_accesses in self.const_accesses.items():
paul@12 361
            accesses = self.attr_accessors.get(path)
paul@12 362
            modifiers = self.attr_access_modifiers.get(path)
paul@12 363
            if not accesses:
paul@12 364
                continue
paul@12 365
            for access in const_accesses.keys():
paul@12 366
                if accesses.has_key(access):
paul@12 367
                    del accesses[access]
paul@12 368
                if modifiers and modifiers.has_key(access):
paul@12 369
                    del modifiers[access]
paul@12 370
paul@29 371
    # Object resolution.
paul@29 372
paul@29 373
    def get_resolved_object(self, path):
paul@29 374
paul@29 375
        """
paul@29 376
        Get the details of an object with the given 'path' within this module.
paul@29 377
        Where the object has not been resolved, None is returned. This differs
paul@29 378
        from the get_object method used elsewhere in that it does not return an
paul@29 379
        unresolved object reference.
paul@29 380
        """
paul@29 381
paul@29 382
        if self.objects.has_key(path):
paul@29 383
            ref = self.objects[path]
paul@29 384
            if ref.has_kind("<depends>"):
paul@29 385
                return None
paul@29 386
            else:
paul@29 387
                return ref
paul@29 388
        else:
paul@29 389
            return None
paul@29 390
paul@12 391
# vim: tabstop=4 expandtab shiftwidth=4