Lichen

Annotated resolving.py

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