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 = 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. 418 """ 419 420 # Define or emit the means of accessing the actual target. 421 422 if self.final_method in ("static", "static-assign", "static-invoke"): 423 424 if test_accessor: 425 emit(test_accessor) 426 427 # Assignments to known attributes. 428 429 if self.final_method == "static-assign": 430 parent, attrname = self.origin.rsplit(".", 1) 431 store = attrname != "__data__" and "__store_via_attr_ref" or "__store_via_attr_ref__" 432 emit(("<set_attr_ref>", ("__get_object_attr_ref", parent, attrname))) 433 emit((store, "<attr_ref>", "<assexpr>")) 434 435 # Invoked attributes employ a separate context. 436 437 elif self.final_method in ("static", "static-invoke"): 438 accessor = ("__load_static_ignore", self.origin) 439 440 # Wrap accesses in context operations. 441 442 if self.context_test == "test": 443 444 # Test and combine the context with static attribute details. 445 446 if self.final_method == "static": 447 emit(("__load_static_test", context, self.origin)) 448 449 # Test the context, storing it separately if required for the 450 # immediately invoked static attribute. 451 452 elif self.final_method == "static-invoke": 453 emit(("<test_context_static>", context, self.origin)) 454 455 # Test the context, storing it separately if required for an 456 # immediately invoked attribute. 457 458 elif self.final_method == "access-invoke": 459 emit(("<test_context_revert>", context, accessor)) 460 461 # Test the context and update the attribute details if 462 # appropriate. 463 464 else: 465 emit(("__test_context", context, accessor)) 466 467 elif self.context_test == "replace": 468 469 # Produce an object with updated context. 470 471 if self.final_method == "static": 472 emit(("__load_static_replace", context, self.origin)) 473 474 # Omit the context update operation where the target is static 475 # and the context is recorded separately. 476 477 elif self.final_method == "static-invoke": 478 pass 479 480 # If a separate context is used for an immediate invocation, 481 # produce the attribute details unchanged. 482 483 elif self.final_method == "access-invoke": 484 emit(accessor) 485 486 # Update the context in the attribute details. 487 488 else: 489 emit(("__update_context", context, accessor)) 490 491 # Omit the accessor for assignments and for invocations of static 492 # targets. Otherwise, emit the accessor which may involve the 493 # invocation of a test. 494 495 elif self.final_method not in ("assign", "static-assign", "static-invoke"): 496 emit(accessor) 497 498 return accessor 499 500 def process_context_identity(self, context, emit): 501 502 """ 503 Using 'context', use 'emit' to generate instructions to test the context 504 identity. 505 """ 506 507 if context: 508 509 # Only verify the context for invocation purposes if a suitable 510 # test has been performed. 511 512 if self.context_test in ("ignore", "replace") or \ 513 self.final_method in ("access-invoke", "static-invoke"): 514 515 emit(("<context_identity_verified>", context)) 516 else: 517 emit(("<context_identity>", context)) 518 519 def write(self, f, location): 520 521 "Write the plan to file 'f' with the given 'location' information." 522 523 print >>f, encode_access_location(location), \ 524 self.name or "{}", \ 525 self.test and "-".join(self.test) or "{}", \ 526 self.test_type or "{}", \ 527 self.base or "{}", \ 528 ".".join(self.traversed_attrnames) or "{}", \ 529 ".".join(self.traversed_attrname_modes) or "{}", \ 530 ".".join(self.remaining_attrnames) or "{}", \ 531 self.context, self.context_test, \ 532 self.first_method, self.final_method, self.origin or "{}", \ 533 ",".join(self.accessor_kinds) 534 535 # vim: tabstop=4 expandtab shiftwidth=4