Lichen

Annotated access_plan.py

1027:dd0745ab8b8a
5 months ago Paul Boddie Reordered GCC arguments to prevent linking failures. Someone decided to change the GCC invocation or linking semantics at some point, meaning that libraries specified "too early" in the argument list no longer provide the symbols required by the program objects, whereas specifying them at the end of the argument list allows those symbols to be found and obtained.
paul@1007 1
#!/usr/bin/env python
paul@1007 2
paul@1007 3
"""
paul@1007 4
Attribute access plan translation.
paul@1007 5
paul@1007 6
Copyright (C) 2014-2018, 2023 Paul Boddie <paul@boddie.org.uk>
paul@1007 7
paul@1007 8
This program is free software; you can redistribute it and/or modify it under
paul@1007 9
the terms of the GNU General Public License as published by the Free Software
paul@1007 10
Foundation; either version 3 of the License, or (at your option) any later
paul@1007 11
version.
paul@1007 12
paul@1007 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@1007 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@1007 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@1007 16
details.
paul@1007 17
paul@1007 18
You should have received a copy of the GNU General Public License along with
paul@1007 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@1007 20
"""
paul@1007 21
paul@1007 22
from encoders import encode_access_location
paul@1007 23
paul@1007 24
class AccessPlan:
paul@1007 25
paul@1007 26
    "An attribute access plan."
paul@1007 27
paul@1007 28
    def __init__(self, name, test, test_type, base, traversed, traversal_modes,
paul@1007 29
                 remaining, context, context_test, first_method, final_method,
paul@1007 30
                 origin, accessor_kinds):
paul@1007 31
paul@1007 32
        "Initialise the plan."
paul@1007 33
paul@1007 34
        # With instance attribute initialisers, the assignment below would be
paul@1007 35
        # generated automatically.
paul@1007 36
paul@1007 37
        (
paul@1007 38
            self.name, self.test, self.test_type, self.base,
paul@1007 39
            self.traversed, self.traversal_modes, self.remaining,
paul@1007 40
            self.context, self.context_test,
paul@1007 41
            self.first_method, self.final_method,
paul@1007 42
            self.origin, self.accessor_kinds) = (
paul@1007 43
paul@1007 44
            name, test, test_type, base,
paul@1007 45
            traversed, traversal_modes, remaining,
paul@1007 46
            context, context_test,
paul@1007 47
            first_method, final_method,
paul@1007 48
            origin, accessor_kinds)
paul@1007 49
paul@1007 50
        # Define the first attribute access and subsequent accesses.
paul@1007 51
paul@1007 52
        self.first_attrname = None
paul@1007 53
        self.traversed_attrnames = traversed
paul@1007 54
        self.traversed_attrname_modes = traversal_modes
paul@1007 55
        self.remaining_attrnames = remaining
paul@1007 56
paul@1007 57
        if traversed:
paul@1007 58
            self.first_attrname = traversed[0]
paul@1007 59
            self.traversed_attrnames = traversed[1:]
paul@1007 60
            self.traversed_attrname_modes = traversal_modes[1:]
paul@1007 61
        elif remaining:
paul@1007 62
            self.first_attrname = remaining[0]
paul@1007 63
            self.remaining_attrnames = remaining[1:]
paul@1007 64
paul@1007 65
    def access_first_attribute(self):
paul@1007 66
paul@1007 67
        "Return whether the first attribute is to be accessed."
paul@1007 68
paul@1007 69
        return self.final_method in ("access", "access-invoke", "assign") or \
paul@1007 70
            self.all_subsequent_attributes()
paul@1007 71
paul@1007 72
    def assigning_first_attribute(self):
paul@1007 73
paul@1007 74
        "Return whether the first attribute access involves assignment."
paul@1007 75
paul@1007 76
        return not self.all_subsequent_attributes() and self.final_method == "assign"
paul@1007 77
paul@1007 78
    def get_first_attribute_name(self):
paul@1007 79
paul@1007 80
        "Return any first attribute name to be used in an initial access."
paul@1007 81
paul@1007 82
        return self.first_attrname
paul@1007 83
paul@1007 84
    def all_subsequent_attributes(self):
paul@1007 85
paul@1007 86
        "Return all subsequent attribute names involved in accesses."
paul@1007 87
paul@1007 88
        return self.traversed_attrnames + self.remaining_attrnames
paul@1007 89
paul@1007 90
    def attribute_traversals(self):
paul@1007 91
paul@1007 92
        "Return a collection of (attribute name, traversal mode) tuples."
paul@1007 93
paul@1007 94
        return zip(self.traversed_attrnames, self.traversed_attrname_modes)
paul@1007 95
paul@1007 96
    def stored_accessor(self):
paul@1007 97
paul@1007 98
        "Return the variable used to obtain the accessor."
paul@1007 99
paul@1007 100
        return self.assigning_first_attribute() and "<target_accessor>" or "<accessor>"
paul@1007 101
paul@1007 102
    def stored_accessor_modifier(self):
paul@1007 103
paul@1007 104
        "Return the variable used to set the accessor."
paul@1007 105
paul@1007 106
        return self.assigning_first_attribute() and "<set_target_accessor>" or "<set_accessor>"
paul@1007 107
paul@1007 108
    def get_original_accessor(self):
paul@1007 109
paul@1007 110
        "Return the original accessor details."
paul@1007 111
paul@1007 112
        # Identify any static original accessor.
paul@1007 113
paul@1007 114
        if self.base:
paul@1007 115
            return self.base
paul@1007 116
paul@1007 117
        # Employ names as contexts unless the context needs testing and
paul@1007 118
        # potentially updating. In such cases, temporary context storage is
paul@1007 119
        # used instead.
paul@1007 120
paul@1007 121
        elif self.name and not (self.context_test == "test" and
paul@1007 122
            self.final_method in ("access-invoke", "static-invoke")):
paul@1007 123
paul@1007 124
            return "<name>"
paul@1007 125
paul@1007 126
        # Use a generic placeholder representing the access expression in
paul@1007 127
        # the general case.
paul@1007 128
paul@1007 129
        else:
paul@1007 130
            return "<expr>"
paul@1007 131
paul@1007 132
    def get_instructions(self):
paul@1007 133
paul@1007 134
        "Return a list of instructions corresponding to the plan."
paul@1007 135
paul@1007 136
        # Emit instructions by appending them to a list.
paul@1007 137
paul@1007 138
        instructions = []
paul@1007 139
        emit = instructions.append
paul@1007 140
paul@1007 141
        # Set up any initial instructions.
paul@1007 142
paul@1007 143
        accessor, context = self.process_initialisation(emit)
paul@1007 144
paul@1007 145
        # Apply any test.
paul@1007 146
paul@1007 147
        if self.test[0] == "test":
paul@1007 148
            test_accessor = accessor = ("__%s_%s_%s" % self.test, accessor, self.test_type)
paul@1007 149
        else:
paul@1007 150
            test_accessor = None
paul@1007 151
paul@1007 152
        # Perform the first or final access.
paul@1007 153
        # The access only needs performing if the resulting accessor is used.
paul@1007 154
paul@1007 155
        accessor = self.process_first_attribute(accessor, emit)
paul@1007 156
paul@1007 157
        # Perform accesses for the traversed and remaining attributes.
paul@1007 158
paul@1007 159
        accessor, context = self.process_traversed_attributes(accessor, context, emit)
paul@1007 160
        accessor, context = self.process_remaining_attributes(accessor, context, emit)
paul@1007 161
paul@1007 162
        # Make any accessor test available if not emitted.
paul@1007 163
paul@1007 164
        test_accessor = not instructions and test_accessor or None
paul@1007 165
paul@1007 166
        # Perform the access on the actual target.
paul@1007 167
paul@1007 168
        accessor = self.process_attribute_access(accessor, context, test_accessor, emit)
paul@1007 169
paul@1007 170
        # Produce an advisory instruction regarding the context.
paul@1007 171
paul@1007 172
        self.process_context_identity(context, emit)
paul@1007 173
paul@1007 174
        # Produce an advisory instruction regarding the final attribute.
paul@1007 175
paul@1007 176
        if self.origin:
paul@1007 177
            emit(("<final_identity>", self.origin))
paul@1007 178
paul@1007 179
        return instructions
paul@1007 180
paul@1007 181
    def process_initialisation(self, emit):
paul@1007 182
paul@1007 183
        """
paul@1007 184
        Use 'emit' to generate instructions for any initialisation of attribute
paul@1007 185
        access. Return the potentially revised accessor and context indicators.
paul@1007 186
        """
paul@1007 187
paul@1007 188
        # Identify any static original accessor.
paul@1007 189
paul@1007 190
        original_accessor = self.get_original_accessor()
paul@1007 191
paul@1007 192
        # Determine whether the first access involves assignment.
paul@1007 193
paul@1007 194
        set_accessor = self.stored_accessor_modifier()
paul@1007 195
        stored_accessor = self.stored_accessor()
paul@1007 196
paul@1007 197
        # Set the context if already available.
paul@1007 198
paul@1007 199
        context = None
paul@1007 200
paul@1007 201
        if self.context == "base":
paul@1007 202
            accessor = context = (self.base,)
paul@1007 203
        elif self.context == "original-accessor":
paul@1007 204
paul@1007 205
            # Prevent re-evaluation of any dynamic expression by storing it.
paul@1007 206
paul@1007 207
            if original_accessor == "<expr>":
paul@1007 208
                if self.final_method in ("access-invoke", "static-invoke"):
paul@1007 209
                    emit(("<set_context>", original_accessor))
paul@1007 210
                    accessor = context = ("<context>",)
paul@1007 211
                else:
paul@1007 212
                    emit((set_accessor, original_accessor))
paul@1007 213
                    accessor = context = (stored_accessor,)
paul@1007 214
            else:
paul@1007 215
                accessor = context = (original_accessor,)
paul@1007 216
paul@1007 217
        # Assigning does not set the context.
paul@1007 218
paul@1007 219
        elif self.context in ("final-accessor", "unset") and self.access_first_attribute():
paul@1007 220
paul@1007 221
            # Prevent re-evaluation of any dynamic expression by storing it.
paul@1007 222
paul@1007 223
            if original_accessor == "<expr>":
paul@1007 224
                emit((set_accessor, original_accessor))
paul@1007 225
                accessor = (stored_accessor,)
paul@1007 226
            else:
paul@1007 227
                accessor = (original_accessor,)
paul@1007 228
        else:
paul@1007 229
            accessor = None
paul@1007 230
paul@1007 231
        return accessor, context
paul@1007 232
paul@1007 233
    def process_first_attribute(self, accessor, emit):
paul@1007 234
paul@1007 235
        """
paul@1007 236
        Using 'accessor', use 'emit' to generate instructions for any first
paul@1007 237
        attribute access. Return the potentially revised accessor.
paul@1007 238
        """
paul@1007 239
paul@1007 240
        if self.access_first_attribute():
paul@1007 241
            attrname = self.get_first_attribute_name()
paul@1007 242
            assigning = self.assigning_first_attribute()
paul@1007 243
paul@1007 244
            if self.first_method == "relative-class":
paul@1007 245
                if assigning:
paul@1007 246
                    emit(("__store_via_class", accessor, attrname, "<assexpr>"))
paul@1007 247
                else:
paul@1007 248
                    accessor = ("__load_via_class", accessor, attrname)
paul@1007 249
paul@1007 250
            elif self.first_method == "relative-object":
paul@1007 251
                if assigning:
paul@1007 252
                    emit(("__store_via_object", accessor, attrname, "<assexpr>"))
paul@1007 253
                else:
paul@1007 254
                    accessor = ("__load_via_object", accessor, attrname)
paul@1007 255
paul@1007 256
            elif self.first_method == "relative-object-class":
paul@1007 257
                if assigning:
paul@1007 258
                    emit(("__get_class_and_store", accessor, attrname, "<assexpr>"))
paul@1007 259
                else:
paul@1007 260
                    accessor = ("__get_class_and_load", accessor, attrname)
paul@1007 261
paul@1007 262
            elif self.first_method == "check-class":
paul@1007 263
                if assigning:
paul@1007 264
                    emit(("__check_and_store_via_class", accessor, attrname, "<assexpr>"))
paul@1007 265
                else:
paul@1007 266
                    accessor = ("__check_and_load_via_class", accessor, attrname)
paul@1007 267
paul@1007 268
            elif self.first_method == "check-object":
paul@1007 269
                if assigning:
paul@1007 270
                    emit(("__check_and_store_via_object", accessor, attrname, "<assexpr>"))
paul@1007 271
                else:
paul@1007 272
                    accessor = ("__check_and_load_via_object", accessor, attrname)
paul@1007 273
paul@1007 274
            elif self.first_method == "check-object-class":
paul@1007 275
                if assigning:
paul@1007 276
                    emit(("__check_and_store_via_any", accessor, attrname, "<assexpr>"))
paul@1007 277
                else:
paul@1007 278
                    accessor = ("__check_and_load_via_any", accessor, attrname)
paul@1007 279
paul@1007 280
        return accessor
paul@1007 281
paul@1007 282
    def process_traversed_attributes(self, accessor, context, emit):
paul@1007 283
paul@1007 284
        """
paul@1007 285
        Using 'accessor' and 'context', use 'emit' to generate instructions
paul@1007 286
        for the traversed attribute accesses. Return the potentially revised
paul@1007 287
        accessor and context indicators.
paul@1007 288
        """
paul@1007 289
paul@1007 290
        # Traverse attributes using the accessor.
paul@1007 291
paul@1007 292
        num_remaining = len(self.all_subsequent_attributes())
paul@1007 293
paul@1007 294
        if self.traversed_attrnames:
paul@1007 295
            for attrname, traversal_mode in self.attribute_traversals():
paul@1007 296
                assigning = num_remaining == 1 and self.final_method == "assign"
paul@1007 297
paul@1007 298
                # Set the context, if appropriate.
paul@1007 299
paul@1007 300
                if num_remaining == 1 and self.final_method != "assign" and self.context == "final-accessor":
paul@1007 301
paul@1007 302
                    # Invoked attributes employ a separate context accessed
paul@1007 303
                    # during invocation.
paul@1007 304
paul@1007 305
                    if self.final_method in ("access-invoke", "static-invoke"):
paul@1007 306
                        emit(("<set_context>", accessor))
paul@1007 307
                        accessor = context = "<context>"
paul@1007 308
paul@1007 309
                    # A private context within the access is otherwise
paul@1007 310
                    # retained.
paul@1007 311
paul@1007 312
                    else:
paul@1007 313
                        emit(("<set_private_context>", accessor))
paul@1007 314
                        accessor = context = "<private_context>"
paul@1007 315
paul@1007 316
                # Perform the access only if not achieved directly.
paul@1007 317
paul@1007 318
                if num_remaining > 1 or self.final_method in ("access", "access-invoke", "assign"):
paul@1007 319
paul@1007 320
                    if traversal_mode == "class":
paul@1007 321
                        if assigning:
paul@1007 322
                            emit(("__store_via_class", accessor, attrname, "<assexpr>"))
paul@1007 323
                        else:
paul@1007 324
                            accessor = ("__load_via_class", accessor, attrname)
paul@1007 325
                    else:
paul@1007 326
                        if assigning:
paul@1007 327
                            emit(("__store_via_object", accessor, attrname, "<assexpr>"))
paul@1007 328
                        else:
paul@1007 329
                            accessor = ("__load_via_object", accessor, attrname)
paul@1007 330
paul@1007 331
                num_remaining -= 1
paul@1007 332
paul@1007 333
        return accessor, context
paul@1007 334
paul@1007 335
    def process_remaining_attributes(self, accessor, context, emit):
paul@1007 336
paul@1007 337
        """
paul@1007 338
        Using 'accessor' and 'context', use 'emit' to generate instructions
paul@1007 339
        for the remaining attribute accesses. Return the potentially revised
paul@1007 340
        accessor and context indicators.
paul@1007 341
        """
paul@1007 342
paul@1007 343
        remaining = self.remaining_attrnames
paul@1007 344
paul@1007 345
        if remaining:
paul@1007 346
            num_remaining = len(remaining)
paul@1007 347
paul@1007 348
            for attrname in remaining:
paul@1007 349
                assigning = num_remaining == 1 and self.final_method == "assign"
paul@1007 350
paul@1007 351
                # Set the context, if appropriate.
paul@1007 352
paul@1007 353
                if num_remaining == 1 and self.final_method != "assign" and self.context == "final-accessor":
paul@1007 354
paul@1007 355
                    # Invoked attributes employ a separate context accessed
paul@1007 356
                    # during invocation.
paul@1007 357
paul@1007 358
                    if self.final_method in ("access-invoke", "static-invoke"):
paul@1007 359
                        emit(("<set_context>", accessor))
paul@1007 360
                        accessor = context = "<context>"
paul@1007 361
paul@1007 362
                    # A private context within the access is otherwise
paul@1007 363
                    # retained.
paul@1007 364
paul@1007 365
                    else:
paul@1007 366
                        emit(("<set_private_context>", accessor))
paul@1007 367
                        accessor = context = "<private_context>"
paul@1007 368
paul@1007 369
                # Perform the access only if not achieved directly.
paul@1007 370
paul@1007 371
                if num_remaining > 1 or self.final_method in ("access", "access-invoke", "assign"):
paul@1007 372
paul@1007 373
                    # Constrain instructions involving certain special
paul@1007 374
                    # attribute names.
paul@1007 375
paul@1007 376
                    to_search = attrname == "__data__" and "object" or "any"
paul@1007 377
paul@1007 378
                    if assigning:
paul@1007 379
                        emit(("__check_and_store_via_%s" % to_search, accessor, attrname, "<assexpr>"))
paul@1007 380
                    else:
paul@1007 381
                        accessor = ("__check_and_load_via_%s" % to_search, accessor, attrname)
paul@1007 382
paul@1007 383
                num_remaining -= 1
paul@1007 384
paul@1007 385
        return accessor, context
paul@1007 386
paul@1007 387
    def process_attribute_access(self, accessor, context, test_accessor, emit):
paul@1007 388
paul@1007 389
        """
paul@1007 390
        Using 'accessor','context' and any 'test_accessor' operation, use 'emit'
paul@1007 391
        to generate instructions for the final attribute access. Return the
paul@1007 392
        potentially revised accessor.
paul@1007 393
        """
paul@1007 394
paul@1007 395
        # Define or emit the means of accessing the actual target.
paul@1007 396
paul@1007 397
        if self.final_method in ("static", "static-assign", "static-invoke"):
paul@1007 398
paul@1007 399
            if test_accessor:
paul@1007 400
                emit(test_accessor)
paul@1007 401
paul@1007 402
            # Assignments to known attributes.
paul@1007 403
paul@1007 404
            if self.final_method == "static-assign":
paul@1007 405
                parent, attrname = self.origin.rsplit(".", 1)
paul@1007 406
                emit(("__store_via_object", parent, attrname, "<assexpr>"))
paul@1007 407
paul@1007 408
            # Invoked attributes employ a separate context.
paul@1007 409
paul@1007 410
            elif self.final_method in ("static", "static-invoke"):
paul@1007 411
                accessor = ("__load_static_ignore", self.origin)
paul@1007 412
paul@1007 413
        # Wrap accesses in context operations.
paul@1007 414
paul@1007 415
        if self.context_test == "test":
paul@1007 416
paul@1007 417
            # Test and combine the context with static attribute details.
paul@1007 418
paul@1007 419
            if self.final_method == "static":
paul@1007 420
                emit(("__load_static_test", context, self.origin))
paul@1007 421
paul@1007 422
            # Test the context, storing it separately if required for the
paul@1007 423
            # immediately invoked static attribute.
paul@1007 424
paul@1007 425
            elif self.final_method == "static-invoke":
paul@1007 426
                emit(("<test_context_static>", context, self.origin))
paul@1007 427
paul@1007 428
            # Test the context, storing it separately if required for an
paul@1007 429
            # immediately invoked attribute.
paul@1007 430
paul@1007 431
            elif self.final_method == "access-invoke":
paul@1007 432
                emit(("<test_context_revert>", context, accessor))
paul@1007 433
paul@1007 434
            # Test the context and update the attribute details if
paul@1007 435
            # appropriate.
paul@1007 436
paul@1007 437
            else:
paul@1007 438
                emit(("__test_context", context, accessor))
paul@1007 439
paul@1007 440
        elif self.context_test == "replace":
paul@1007 441
paul@1007 442
            # Produce an object with updated context.
paul@1007 443
paul@1007 444
            if self.final_method == "static":
paul@1007 445
                emit(("__load_static_replace", context, self.origin))
paul@1007 446
paul@1007 447
            # Omit the context update operation where the target is static
paul@1007 448
            # and the context is recorded separately.
paul@1007 449
paul@1007 450
            elif self.final_method == "static-invoke":
paul@1007 451
                pass
paul@1007 452
paul@1007 453
            # If a separate context is used for an immediate invocation,
paul@1007 454
            # produce the attribute details unchanged.
paul@1007 455
paul@1007 456
            elif self.final_method == "access-invoke":
paul@1007 457
                emit(accessor)
paul@1007 458
paul@1007 459
            # Update the context in the attribute details.
paul@1007 460
paul@1007 461
            else:
paul@1007 462
                emit(("__update_context", context, accessor))
paul@1007 463
paul@1007 464
        # Omit the accessor for assignments and for invocations of static
paul@1007 465
        # targets. Otherwise, emit the accessor which may involve the
paul@1007 466
        # invocation of a test.
paul@1007 467
paul@1007 468
        elif self.final_method not in ("assign", "static-assign", "static-invoke"):
paul@1007 469
            emit(accessor)
paul@1007 470
paul@1007 471
        return accessor
paul@1007 472
paul@1007 473
    def process_context_identity(self, context, emit):
paul@1007 474
paul@1007 475
        """
paul@1007 476
        Using 'context', use 'emit' to generate instructions to test the context
paul@1007 477
        identity.
paul@1007 478
        """
paul@1007 479
paul@1007 480
        if context:
paul@1007 481
paul@1007 482
            # Only verify the context for invocation purposes if a suitable
paul@1007 483
            # test has been performed.
paul@1007 484
paul@1007 485
            if self.context_test in ("ignore", "replace") or \
paul@1007 486
               self.final_method in ("access-invoke", "static-invoke"):
paul@1007 487
paul@1007 488
                emit(("<context_identity_verified>", context))
paul@1007 489
            else:
paul@1007 490
                emit(("<context_identity>", context))
paul@1007 491
paul@1007 492
    def write(self, f, location):
paul@1007 493
paul@1007 494
        "Write the plan to file 'f' with the given 'location' information."
paul@1007 495
paul@1007 496
        print >>f, encode_access_location(location), \
paul@1007 497
                   self.name or "{}", \
paul@1007 498
                   self.test and "-".join(self.test) or "{}", \
paul@1007 499
                   self.test_type or "{}", \
paul@1007 500
                   self.base or "{}", \
paul@1007 501
                   ".".join(self.traversed_attrnames) or "{}", \
paul@1007 502
                   ".".join(self.traversed_attrname_modes) or "{}", \
paul@1007 503
                   ".".join(self.remaining_attrnames) or "{}", \
paul@1007 504
                   self.context, self.context_test, \
paul@1007 505
                   self.first_method, self.final_method, self.origin or "{}", \
paul@1007 506
                   ",".join(self.accessor_kinds)
paul@1007 507
paul@1007 508
# vim: tabstop=4 expandtab shiftwidth=4