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