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 # Access via the accessor's class. 245 246 if self.first_method == "relative-class": 247 if assigning: 248 emit(("<set_attr_ref>", ("__get_class_attr_ref", accessor, attrname))) 249 emit(("__store_via_attr_ref", "<attr_ref>", "<assexpr>")) 250 else: 251 accessor = ("__load_via_class", accessor, attrname) 252 253 # Access via the accessor itself. 254 255 elif self.first_method == "relative-object": 256 if assigning: 257 emit(("<set_attr_ref>", ("__get_object_attr_ref", accessor, attrname))) 258 emit(("__store_via_attr_ref", "<attr_ref>", "<assexpr>")) 259 else: 260 accessor = ("__load_via_object", accessor, attrname) 261 262 # Access via a class accessor or the accessor's class. 263 264 elif self.first_method == "relative-object-class": 265 if assigning: 266 emit(("__raise_type_error",)) 267 else: 268 accessor = ("__get_class_and_load", accessor, attrname) 269 270 # Access via the accessor's class. 271 272 elif self.first_method == "check-class": 273 if assigning: 274 emit(("__raise_type_error",)) 275 else: 276 accessor = ("__check_and_load_via_class", accessor, attrname) 277 278 # Access via the accessor itself. 279 280 elif self.first_method == "check-object": 281 if assigning: 282 emit(("<set_attr_ref>", ("__check_and_get_object_attr_ref", accessor, attrname))) 283 emit(("__store_via_attr_ref", "<attr_ref>", "<assexpr>")) 284 else: 285 accessor = ("__check_and_load_via_object", accessor, attrname) 286 287 # Access via a class accessor or the accessor's class. 288 # Here, only access via the accessor is supported. 289 290 elif self.first_method == "check-object-class": 291 if assigning: 292 emit(("<set_attr_ref>", ("__check_and_get_object_attr_ref", accessor, attrname))) 293 emit(("__store_via_attr_ref", "<attr_ref>", "<assexpr>")) 294 else: 295 accessor = ("__check_and_load_via_any", accessor, attrname) 296 297 return accessor 298 299 def process_traversed_attributes(self, accessor, context, emit): 300 301 """ 302 Using 'accessor' and 'context', use 'emit' to generate instructions 303 for the traversed attribute accesses. Return the potentially revised 304 accessor and context indicators. 305 """ 306 307 # Traverse attributes using the accessor. 308 309 num_remaining = len(self.all_subsequent_attributes()) 310 311 if self.traversed_attrnames: 312 for attrname, traversal_mode in self.attribute_traversals(): 313 assigning = num_remaining == 1 and self.final_method == "assign" 314 315 # Set the context, if appropriate. 316 317 if num_remaining == 1 and self.final_method != "assign" and self.context == "final-accessor": 318 319 # Invoked attributes employ a separate context accessed 320 # during invocation. 321 322 if self.final_method in ("access-invoke", "static-invoke"): 323 emit(("<set_context>", accessor)) 324 accessor = context = "<context>" 325 326 # A private context within the access is otherwise 327 # retained. 328 329 else: 330 emit(("<set_private_context>", accessor)) 331 accessor = context = "<private_context>" 332 333 # Perform the access only if not achieved directly. 334 335 if num_remaining > 1 or self.final_method in ("access", "access-invoke", "assign"): 336 337 if traversal_mode == "class": 338 if assigning: 339 emit(("<set_attr_ref>", ("__get_class_attr_ref", accessor, attrname))) 340 emit(("__store_via_attr_ref", "<attr_ref>", "<assexpr>")) 341 else: 342 accessor = ("__load_via_class", accessor, attrname) 343 else: 344 if assigning: 345 emit(("<set_attr_ref>", ("__get_object_attr_ref", accessor, attrname))) 346 emit(("__store_via_attr_ref", "<attr_ref>", "<assexpr>")) 347 else: 348 accessor = ("__load_via_object", accessor, attrname) 349 350 num_remaining -= 1 351 352 return accessor, context 353 354 def process_remaining_attributes(self, accessor, context, emit): 355 356 """ 357 Using 'accessor' and 'context', use 'emit' to generate instructions 358 for the remaining attribute accesses. Return the potentially revised 359 accessor and context indicators. 360 """ 361 362 remaining = self.remaining_attrnames 363 364 if remaining: 365 num_remaining = len(remaining) 366 367 for attrname in remaining: 368 assigning = num_remaining == 1 and self.final_method == "assign" 369 370 # Set the context, if appropriate. 371 372 if num_remaining == 1 and self.final_method != "assign" and self.context == "final-accessor": 373 374 # Invoked attributes employ a separate context accessed 375 # during invocation. 376 377 if self.final_method in ("access-invoke", "static-invoke"): 378 emit(("<set_context>", accessor)) 379 accessor = context = "<context>" 380 381 # A private context within the access is otherwise 382 # retained. 383 384 else: 385 emit(("<set_private_context>", accessor)) 386 accessor = context = "<private_context>" 387 388 # Perform the access only if not achieved directly. 389 390 if num_remaining > 1 or self.final_method in ("access", "access-invoke", "assign"): 391 392 # Constrain instructions involving certain special 393 # attribute names. 394 395 to_search = attrname != "__data__" and "any" or "object" 396 397 if assigning: 398 emit(("<set_attr_ref>", ("__check_and_get_object_attr_ref", accessor, attrname))) 399 emit(("__store_via_attr_ref", "<attr_ref>", "<assexpr>")) 400 else: 401 accessor = ("__check_and_load_via_%s" % to_search, accessor, attrname) 402 403 num_remaining -= 1 404 405 return accessor, context 406 407 def process_attribute_access(self, accessor, context, test_accessor, emit): 408 409 """ 410 Using 'accessor','context' and any 'test_accessor' operation, use 'emit' 411 to generate instructions for the final attribute access. Return the 412 potentially revised accessor. 413 """ 414 415 # Define or emit the means of accessing the actual target. 416 417 if self.final_method in ("static", "static-assign", "static-invoke"): 418 419 if test_accessor: 420 emit(test_accessor) 421 422 # Assignments to known attributes. 423 424 if self.final_method == "static-assign": 425 parent, attrname = self.origin.rsplit(".", 1) 426 emit(("<set_attr_ref>", ("__get_object_attr_ref", parent, attrname))) 427 emit(("__store_via_attr_ref", "<attr_ref>", "<assexpr>")) 428 429 # Invoked attributes employ a separate context. 430 431 elif self.final_method in ("static", "static-invoke"): 432 accessor = ("__load_static_ignore", self.origin) 433 434 # Wrap accesses in context operations. 435 436 if self.context_test == "test": 437 438 # Test and combine the context with static attribute details. 439 440 if self.final_method == "static": 441 emit(("__load_static_test", context, self.origin)) 442 443 # Test the context, storing it separately if required for the 444 # immediately invoked static attribute. 445 446 elif self.final_method == "static-invoke": 447 emit(("<test_context_static>", context, self.origin)) 448 449 # Test the context, storing it separately if required for an 450 # immediately invoked attribute. 451 452 elif self.final_method == "access-invoke": 453 emit(("<test_context_revert>", context, accessor)) 454 455 # Test the context and update the attribute details if 456 # appropriate. 457 458 else: 459 emit(("__test_context", context, accessor)) 460 461 elif self.context_test == "replace": 462 463 # Produce an object with updated context. 464 465 if self.final_method == "static": 466 emit(("__load_static_replace", context, self.origin)) 467 468 # Omit the context update operation where the target is static 469 # and the context is recorded separately. 470 471 elif self.final_method == "static-invoke": 472 pass 473 474 # If a separate context is used for an immediate invocation, 475 # produce the attribute details unchanged. 476 477 elif self.final_method == "access-invoke": 478 emit(accessor) 479 480 # Update the context in the attribute details. 481 482 else: 483 emit(("__update_context", context, accessor)) 484 485 # Omit the accessor for assignments and for invocations of static 486 # targets. Otherwise, emit the accessor which may involve the 487 # invocation of a test. 488 489 elif self.final_method not in ("assign", "static-assign", "static-invoke"): 490 emit(accessor) 491 492 return accessor 493 494 def process_context_identity(self, context, emit): 495 496 """ 497 Using 'context', use 'emit' to generate instructions to test the context 498 identity. 499 """ 500 501 if context: 502 503 # Only verify the context for invocation purposes if a suitable 504 # test has been performed. 505 506 if self.context_test in ("ignore", "replace") or \ 507 self.final_method in ("access-invoke", "static-invoke"): 508 509 emit(("<context_identity_verified>", context)) 510 else: 511 emit(("<context_identity>", context)) 512 513 def write(self, f, location): 514 515 "Write the plan to file 'f' with the given 'location' information." 516 517 print >>f, encode_access_location(location), \ 518 self.name or "{}", \ 519 self.test and "-".join(self.test) or "{}", \ 520 self.test_type or "{}", \ 521 self.base or "{}", \ 522 ".".join(self.traversed_attrnames) or "{}", \ 523 ".".join(self.traversed_attrname_modes) or "{}", \ 524 ".".join(self.remaining_attrnames) or "{}", \ 525 self.context, self.context_test, \ 526 self.first_method, self.final_method, self.origin or "{}", \ 527 ",".join(self.accessor_kinds) 528 529 # vim: tabstop=4 expandtab shiftwidth=4