Lichen

Annotated access_plan.py

1043:85fda4fc3aca
5 months ago Paul Boddie Merged changes from the default branch. trailing-data
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